mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-20 06:53:27 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8818c3a9b3 | |||
| 5a2009b0c0 | |||
| 14a44cfef1 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Title generation and compaction summaries now build their model requests through the shared session request boundary, gaining unsupported-media filtering and image bounds while explicitly opting out of session context hooks: conversation-shaping plugins do not observe housekeeping requests. Title requests gain the session prompt cache key, and compaction summaries in forked sessions reuse the fork root's prompt cache key instead of the fork's own.
|
||||
@@ -190,7 +190,7 @@
|
||||
"solid-js": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.110",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -522,7 +522,7 @@
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.15",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1084,10 +1084,10 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-rc.110",
|
||||
"@effect/platform-node": "4.0.0-rc.110",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.110",
|
||||
"@effect/opentelemetry": "4.0.0-beta.107",
|
||||
"@effect/platform-node": "4.0.0-beta.107",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -1124,7 +1124,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
@@ -1571,13 +1571,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.110", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.110" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-8Uum1ikIAK2DjATeLseS71HZKVI/dduFYypvMhiO68RTli1xq9yaoTI1Y0IThPn2sI0i86ZdpFaKQx1hdw2tWw=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.107", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-beta.107" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-WxR3OEcwVtckNYGxvERA4kiS8cb2B46lSWxQw8P6dCCzW0j0VC7hkWyzryJ16MVXfI/5xQHS3r5j9mud+JVvsg=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.110", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.110", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-rc.110", "redis": ">=5.0.0 <7.0.0" } }, "sha512-poj6VxTc2kRDowUHgjGXAZL2nWHTyGi/18607jK+pV9A9CH/wqZ4NeYj38rij95j5SiZ3U7Y1iOsk2Vfnr4GEw=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.107", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.107", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.110", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-rc.110" } }, "sha512-P8EZloxS7RtCOSL3VSBPyqSenUm93xLbXf9jkWoy67cibZ2wgZ9nAou9iN8f1i4vzgxUsWtDuy6BEmg67np6Zw=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.107", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-y6BqcRi86BfTJv+tvDrob4ozYVHxxlHYcn/zIQqZjXI9CvKnkgD6ng+38G1o45c4f2ucU+6HRI9POCmFdMoVGA=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.110", "", { "peerDependencies": { "effect": "^4.0.0-rc.110" } }, "sha512-Lam3gY1xszjQS/cebdLbWbtR21FtQI4ke7QHqbBQ6AyVJF0CGUtS/ePL9zNq6wHNmJ95FUDGS6W+Qx/l6RPSzA=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.107", "", { "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-BuSSCUoXz6JSR30wT4Y0ukmJKvIFhm/gROwEeA0nLO5QYf33CdTunwX+q35+/MNOOLT8jL3xaDsz5R5aAF1vYQ=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
@@ -2503,16 +2503,6 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ=="],
|
||||
|
||||
"@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="],
|
||||
|
||||
"@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="],
|
||||
|
||||
"@redis/json": ["@redis/json@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ=="],
|
||||
|
||||
"@redis/search": ["@redis/search@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw=="],
|
||||
|
||||
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.8.1", "", {}, "sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig=="],
|
||||
|
||||
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
|
||||
@@ -3611,7 +3601,7 @@
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
|
||||
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
|
||||
|
||||
@@ -3889,7 +3879,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"effect": ["effect@4.0.0-rc.110", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "msgpackr": "^2.0.4" } }, "sha512-ega6FTJ8CS2of7tHZiADvgyJyV999Q6tZ9juE56V81O0jw6flRwydaPNtyfvP2a5LL9PZrse8A1jnNUD5sWVHg=="],
|
||||
"effect": ["effect@4.0.0-beta.107", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4551,6 +4541,8 @@
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
|
||||
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
|
||||
|
||||
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
|
||||
|
||||
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
|
||||
@@ -5233,8 +5225,6 @@
|
||||
|
||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||
|
||||
"redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
@@ -6789,8 +6779,6 @@
|
||||
|
||||
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"ioredis/cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
|
||||
"js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||
|
||||
+5
-5
@@ -37,10 +37,10 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-rc.110",
|
||||
"@effect/platform-node": "4.0.0-rc.110",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.110",
|
||||
"@effect/opentelemetry": "4.0.0-beta.107",
|
||||
"@effect/platform-node": "4.0.0-beta.107",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -71,7 +71,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@opencode-ai/protocol": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.110",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -111,7 +111,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"])
|
||||
const durable = events[1]
|
||||
if (durable?.type !== "session.model.selected") throw new Error("Expected model event")
|
||||
expect(durable.created).toBe(1_717_171_717_000)
|
||||
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
|
||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
||||
})
|
||||
|
||||
@@ -219,7 +219,9 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
const logged = Array.from(result.log)
|
||||
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
|
||||
expect(logged[0]?.type === "session.model.selected" && logged[0].created).toBe(1_717_171_717_000)
|
||||
expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
|
||||
1_717_171_717_000,
|
||||
)
|
||||
expect(logged.at(-1)).toEqual(synced)
|
||||
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionCompaction from "./compaction.js"
|
||||
|
||||
import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { LLMClient, AIError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
|
||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
@@ -9,17 +9,12 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { llmClient } from "../effect/app-node-platform.js"
|
||||
import { SessionEvent } from "./event.js"
|
||||
import type { SessionMessage } from "./message.js"
|
||||
import { SessionModelHeaders } from "./model-headers.js"
|
||||
import { SessionModelHook } from "./model-hook.js"
|
||||
import { SessionModelHttp } from "./model-http.js"
|
||||
import { SessionPromptCacheKey } from "./prompt-cache-key.js"
|
||||
import { App } from "../app.js"
|
||||
import { SessionModelRequest } from "./model-request.js"
|
||||
import { SessionRunnerModel } from "./runner/model.js"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
import { toSessionError } from "./to-session-error.js"
|
||||
import { Token } from "../util/token.js"
|
||||
import { SessionUsage } from "./usage.js"
|
||||
import { PluginHooks } from "../plugin/hooks.js"
|
||||
import { Agent } from "../agent.js"
|
||||
import { State } from "../state.js"
|
||||
|
||||
@@ -70,13 +65,12 @@ export type Draft = {
|
||||
}
|
||||
|
||||
type Dependencies = {
|
||||
readonly app: App.Info
|
||||
readonly bus: Bus.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest, options?: StreamOptions) => Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
readonly models: SessionRunnerModel.Interface
|
||||
readonly hooks: PluginHooks.Interface
|
||||
readonly modelRequests: SessionModelRequest.Interface
|
||||
}
|
||||
|
||||
export type AutoInput = {
|
||||
@@ -85,6 +79,8 @@ export type AutoInput = {
|
||||
readonly resolved: SessionRunnerModel.Resolved
|
||||
}
|
||||
|
||||
type RequiredInput = Pick<AutoInput, "messages" | "resolved">
|
||||
|
||||
export type ManualInput = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
@@ -92,8 +88,6 @@ export type ManualInput = {
|
||||
readonly started?: boolean
|
||||
}
|
||||
|
||||
type RequiredInput = Pick<AutoInput, "messages" | "resolved">
|
||||
|
||||
type Plan = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly resolved: SessionRunnerModel.Resolved
|
||||
@@ -266,65 +260,51 @@ const make = (dependencies: Dependencies) => {
|
||||
})
|
||||
: Effect.void,
|
||||
)
|
||||
const request = yield* SessionModelHook.apply(
|
||||
dependencies.hooks,
|
||||
{ sessionID: plan.session.id, agent: Agent.ID.make("compaction"), model: plan.resolved.ref },
|
||||
LLM.request({
|
||||
model: plan.resolved.model,
|
||||
promptCacheKey: SessionPromptCacheKey.make(plan.session.id),
|
||||
http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) },
|
||||
messages: [Message.user(plan.prompt)],
|
||||
tools: [],
|
||||
const prepared = yield* dependencies.modelRequests.prepare({
|
||||
scope: { session: plan.session, agentID: Agent.ID.make("compaction"), model: plan.resolved },
|
||||
transcript: { system: [], messages: [Message.user(plan.prompt)] },
|
||||
contextHooks: false,
|
||||
})
|
||||
yield* dependencies.llm.stream(prepared.request, prepared.options).pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (LLMEvent.is.providerError(event))
|
||||
failure = {
|
||||
type: event.classification === "context-overflow" ? "provider.invalid-request" : "provider.error",
|
||||
message: event.message,
|
||||
}
|
||||
if (LLMEvent.is.textDelta(event)) {
|
||||
chunks.push(event.text)
|
||||
return dependencies.bus.publish(SessionEvent.Compaction.Delta, {
|
||||
sessionID: plan.session.id,
|
||||
text: event.text,
|
||||
})
|
||||
}
|
||||
if (LLMEvent.is.stepFinish(event)) {
|
||||
const step = SessionUsage.record(event.usage, plan.resolved.cost)
|
||||
usage = usage ? SessionUsage.add(usage, step) : step
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
)
|
||||
yield* dependencies.llm
|
||||
.stream(request, {
|
||||
http: SessionModelHttp.middleware(dependencies.hooks, {
|
||||
sessionID: plan.session.id,
|
||||
agent: Agent.ID.make("compaction"),
|
||||
model: plan.resolved.ref,
|
||||
Effect.catchTag("AI.Error", (error) =>
|
||||
Effect.sync(() => {
|
||||
failure = toSessionError(error)
|
||||
}),
|
||||
})
|
||||
.pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (LLMEvent.is.providerError(event))
|
||||
failure = {
|
||||
type: event.classification === "context-overflow" ? "provider.invalid-request" : "provider.error",
|
||||
message: event.message,
|
||||
}
|
||||
if (LLMEvent.is.textDelta(event)) {
|
||||
chunks.push(event.text)
|
||||
return dependencies.bus.publish(SessionEvent.Compaction.Delta, {
|
||||
sessionID: plan.session.id,
|
||||
text: event.text,
|
||||
})
|
||||
}
|
||||
if (LLMEvent.is.stepFinish(event)) {
|
||||
const step = SessionUsage.record(event.usage, plan.resolved.cost)
|
||||
usage = usage ? SessionUsage.add(usage, step) : step
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.catchTag("AI.Error", (error) =>
|
||||
Effect.sync(() => {
|
||||
failure = toSessionError(error)
|
||||
}),
|
||||
),
|
||||
Effect.onInterrupt(() =>
|
||||
recordUsage.pipe(
|
||||
Effect.andThen(
|
||||
plan.reason === "auto"
|
||||
? failed({
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
||||
inputID: plan.inputID,
|
||||
}).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
),
|
||||
Effect.onInterrupt(() =>
|
||||
recordUsage.pipe(
|
||||
Effect.andThen(
|
||||
plan.reason === "auto"
|
||||
? failed({
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
||||
inputID: plan.inputID,
|
||||
}).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
yield* recordUsage
|
||||
const summary = chunks.join("")
|
||||
if (failure || !summary.trim()) {
|
||||
@@ -425,14 +405,13 @@ export const layer = Layer.effect(
|
||||
const bus = yield* Bus.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const app = yield* App.Metadata
|
||||
const hooks = yield* PluginHooks.Service
|
||||
return make({ bus, llm, models, app, hooks })
|
||||
const modelRequests = yield* SessionModelRequest.Service
|
||||
return make({ bus, llm, models, modelRequests })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Bus.node, llmClient, SessionRunnerModel.node, App.node, PluginHooks.node],
|
||||
deps: [Bus.node, llmClient, SessionRunnerModel.node, SessionModelRequest.node],
|
||||
})
|
||||
|
||||
@@ -61,13 +61,19 @@ interface PrepareInput {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly agentID: Agent.ID
|
||||
readonly model: SessionRunnerModel.Resolved
|
||||
readonly tools: Tool.Snapshot
|
||||
/** Omitted for housekeeping requests that carry no tools. */
|
||||
readonly tools?: Tool.Snapshot
|
||||
}
|
||||
readonly transcript: {
|
||||
readonly system: Array<SystemPart>
|
||||
readonly messages: Array<Message>
|
||||
}
|
||||
readonly toolChoice?: LLM.RequestInput["toolChoice"]
|
||||
/**
|
||||
* Session context hooks shape agent conversations. Housekeeping callers
|
||||
* (title, compaction) opt out: their transcripts pass through unchanged.
|
||||
*/
|
||||
readonly contextHooks?: false
|
||||
/** Stateful Session WebSocket channels require an explicit durable-runner opt-in. */
|
||||
readonly webSocket?: "session"
|
||||
}
|
||||
@@ -209,7 +215,10 @@ export const layer = Layer.effect(
|
||||
const session = input.scope.session
|
||||
const resolved = input.scope.model
|
||||
const model = resolved.model
|
||||
const tools = input.scope.tools
|
||||
const tools = input.scope.tools ?? {
|
||||
definitions: [],
|
||||
execute: () => new Tool.Error({ message: "Tools are not available for this request" }),
|
||||
}
|
||||
const registry = new Map(tools.definitions.map((tool) => [tool.name, tool]))
|
||||
// The definition objects we hand to hooks, mapped back to their tools. Hooks rename a
|
||||
// tool by moving its definition to a new key; recognizing the object recovers the tool.
|
||||
@@ -219,14 +228,18 @@ export const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
// Hooks mutate this record in place: edit descriptions and schemas, rename, or remove.
|
||||
const context = yield* hooks.trigger("session", "context", {
|
||||
sessionID: session.id,
|
||||
agent: input.scope.agentID,
|
||||
model: resolved.ref,
|
||||
system: input.transcript.system,
|
||||
messages: input.transcript.messages,
|
||||
tools: Object.fromEntries(Array.from(given, ([definition, tool]) => [tool.name, definition])),
|
||||
})
|
||||
const definitions = Object.fromEntries(Array.from(given, ([definition, tool]) => [tool.name, definition]))
|
||||
const context =
|
||||
input.contextHooks === false
|
||||
? { system: input.transcript.system, messages: input.transcript.messages, tools: definitions }
|
||||
: yield* hooks.trigger("session", "context", {
|
||||
sessionID: session.id,
|
||||
agent: input.scope.agentID,
|
||||
model: resolved.ref,
|
||||
system: input.transcript.system,
|
||||
messages: input.transcript.messages,
|
||||
tools: definitions,
|
||||
})
|
||||
// Match each surviving entry back to its tool, by recognizing a moved definition or
|
||||
// by key. Identity wins so a definition moved onto another tool's name still executes
|
||||
// the tool it describes. Entries matching neither were invented by a hook and dropped.
|
||||
|
||||
@@ -315,7 +315,6 @@ const layer = Layer.effect(
|
||||
const loaded = yield* context.load(selected)
|
||||
const { session, agent } = loaded
|
||||
const resolved = loaded.model
|
||||
const model = resolved.model
|
||||
// Make room: history must fit the context window before the call. A pending manual
|
||||
// compaction owns this instead; the runner executes it between steps.
|
||||
const compactionInput = { session, messages: loaded.messages, resolved }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionTitle from "./title.js"
|
||||
|
||||
import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { LLMClient, AIError, LLMEvent, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||
import { Context, DateTime, Effect, Layer, Stream } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
@@ -8,14 +8,10 @@ import { Database } from "../database/database.js"
|
||||
import { Bus } from "../bus.js"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { isExactRootFallback } from "@opencode-ai/util/session-title-fallback"
|
||||
import { App } from "../app.js"
|
||||
import { llmClient } from "../effect/app-node-platform.js"
|
||||
import { PluginHooks } from "../plugin/hooks.js"
|
||||
import { SessionEvent } from "./event.js"
|
||||
import { SessionHistory } from "./history.js"
|
||||
import { SessionModelHeaders } from "./model-headers.js"
|
||||
import { SessionModelHook } from "./model-hook.js"
|
||||
import { SessionModelHttp } from "./model-http.js"
|
||||
import { SessionModelRequest } from "./model-request.js"
|
||||
import { SessionRunnerModel } from "./runner/model.js"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
import { SessionUsage } from "./usage.js"
|
||||
@@ -25,15 +21,14 @@ const MAX_LENGTH = 100
|
||||
const titleChanged = Symbol("Session title changed")
|
||||
|
||||
type Dependencies = {
|
||||
readonly app: App.Info
|
||||
readonly bus: Bus.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest, options?: StreamOptions) => Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
readonly agents: Agent.Interface
|
||||
readonly models: SessionRunnerModel.Interface
|
||||
readonly modelRequests: SessionModelRequest.Interface
|
||||
readonly store: SessionStore.Interface
|
||||
readonly hooks: PluginHooks.Interface
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
@@ -81,39 +76,28 @@ const make = (dependencies: Dependencies) => {
|
||||
})
|
||||
: Effect.void,
|
||||
)
|
||||
const request = yield* SessionModelHook.apply(
|
||||
dependencies.hooks,
|
||||
{ sessionID: session.id, agent: agent.id, model: resolved.ref },
|
||||
LLM.request({
|
||||
model: resolved.model,
|
||||
http: { headers: SessionModelHeaders.make(session, dependencies.app) },
|
||||
system: agent.system,
|
||||
const prepared = yield* dependencies.modelRequests.prepare({
|
||||
scope: { session, agentID: agent.id, model: resolved },
|
||||
transcript: {
|
||||
system: agent.system ? [SystemPart.make(agent.system)] : [],
|
||||
messages: [Message.user(firstUser.text)],
|
||||
tools: [],
|
||||
},
|
||||
contextHooks: false,
|
||||
})
|
||||
const streamed = yield* dependencies.llm.stream(prepared.request, prepared.options).pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (LLMEvent.is.providerError(event)) failed = true
|
||||
if (LLMEvent.is.textDelta(event)) chunks.push(event.text)
|
||||
if (LLMEvent.is.stepFinish(event)) {
|
||||
const step = SessionUsage.record(event.usage, resolved.cost)
|
||||
usage = usage ? SessionUsage.add(usage, step) : step
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.as(true),
|
||||
Effect.catchTag("AI.Error", () => Effect.succeed(false)),
|
||||
Effect.onInterrupt(() => recordUsage.pipe(Effect.asVoid)),
|
||||
)
|
||||
const streamed = yield* dependencies.llm
|
||||
.stream(request, {
|
||||
http: SessionModelHttp.middleware(dependencies.hooks, {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
model: resolved.ref,
|
||||
}),
|
||||
})
|
||||
.pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (LLMEvent.is.providerError(event)) failed = true
|
||||
if (LLMEvent.is.textDelta(event)) chunks.push(event.text)
|
||||
if (LLMEvent.is.stepFinish(event)) {
|
||||
const step = SessionUsage.record(event.usage, resolved.cost)
|
||||
usage = usage ? SessionUsage.add(usage, step) : step
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.as(true),
|
||||
Effect.catchTag("AI.Error", () => Effect.succeed(false)),
|
||||
Effect.onInterrupt(() => recordUsage.pipe(Effect.asVoid)),
|
||||
)
|
||||
yield* recordUsage
|
||||
if (!streamed || failed) return
|
||||
const title = chunks
|
||||
@@ -146,11 +130,10 @@ export const layer = Layer.effect(
|
||||
const llm = yield* LLMClient.Service
|
||||
const agents = yield* Agent.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const modelRequests = yield* SessionModelRequest.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const database = yield* Database.Service
|
||||
const app = yield* App.Metadata
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const title = make({ bus, llm, agents, models, store, app, hooks })
|
||||
const title = make({ bus, llm, agents, models, modelRequests, store })
|
||||
return Service.of({
|
||||
generateForFirstPrompt: (sessionID) => title.generateForFirstPrompt(database.db, sessionID),
|
||||
})
|
||||
@@ -165,9 +148,8 @@ export const node = makeLocationNode({
|
||||
llmClient,
|
||||
Agent.node,
|
||||
SessionRunnerModel.node,
|
||||
SessionModelRequest.node,
|
||||
SessionStore.node,
|
||||
Database.node,
|
||||
App.node,
|
||||
PluginHooks.node,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as Shell from "./shell.js"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Deferred, Duration, Effect, Fiber, Latch, Layer, Schema, Schedule, Stream } from "effect"
|
||||
import { Context, Deferred, Duration, Effect, Fiber, Layer, Schema, Schedule, Stream } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { produce } from "immer"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
@@ -286,7 +286,7 @@ const layer = () =>
|
||||
sessions.set(id, session)
|
||||
|
||||
const stream = createWriteStream(file)
|
||||
const outputDone = Latch.makeUnsafe()
|
||||
const outputDone = Deferred.makeUnsafe<void>()
|
||||
const pump = handle.all.pipe(
|
||||
Stream.runForEach((chunk: Uint8Array) =>
|
||||
Effect.sync(() => {
|
||||
@@ -304,8 +304,8 @@ const layer = () =>
|
||||
stream.end(() => resolve())
|
||||
}),
|
||||
)
|
||||
yield* outputDone.open
|
||||
}).pipe(Effect.catch(() => outputDone.open)),
|
||||
yield* Deferred.succeed(outputDone, undefined)
|
||||
}).pipe(Effect.catch(() => Deferred.succeed(outputDone, undefined))),
|
||||
)
|
||||
yield* Effect.promise(
|
||||
() =>
|
||||
@@ -324,7 +324,7 @@ const layer = () =>
|
||||
draft.time.completed = Date.now()
|
||||
})
|
||||
yield* beforeWait
|
||||
yield* outputDone.await
|
||||
yield* Deferred.await(outputDone)
|
||||
// Resolve waiters with the terminal Info before any retention eviction, so an evicted
|
||||
// session still reports success rather than the removal NotFoundError. This runs before
|
||||
// the timeout-fiber interrupt below, which on the timeout path would otherwise cancel
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { LLMClient, LLMEvent, LanguageModel, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
@@ -19,6 +19,7 @@ import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { App } from "@opencode-ai/core/app"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
@@ -76,7 +77,14 @@ const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
})
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]),
|
||||
LayerNode.group([
|
||||
Database.node,
|
||||
Bus.node,
|
||||
SessionProjector.node,
|
||||
SessionStore.node,
|
||||
PluginHooks.node,
|
||||
SessionCompaction.node,
|
||||
]),
|
||||
[
|
||||
[Bus.node, Bus.configured({ persist: true })],
|
||||
[llmClient, client],
|
||||
@@ -174,6 +182,35 @@ it.effect("auto compaction reserves a buffer below the prompt ceiling", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
/** Seeds the global project plus one session row, returning the projected session. */
|
||||
const insertSession = (id: Session.ID, overrides?: Partial<typeof SessionTable.$inferInsert>) =>
|
||||
Effect.gen(function* () {
|
||||
const db = (yield* Database.Service).db
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id,
|
||||
project_id: Project.ID.global,
|
||||
slug: id,
|
||||
directory: "/project",
|
||||
title: id,
|
||||
version: "test",
|
||||
...overrides,
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const store = yield* SessionStore.Service
|
||||
return yield* store
|
||||
.get(id)
|
||||
.pipe(Effect.flatMap((session) => (session ? Effect.succeed(session) : Effect.die(`session missing: ${id}`))))
|
||||
})
|
||||
|
||||
it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
@@ -189,33 +226,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
text: "Manual compaction should include this short conversation.",
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
}
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
parent_id: parentID,
|
||||
slug: "manual-compaction",
|
||||
directory: "/project",
|
||||
title: "Manual compaction",
|
||||
version: "test",
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
const session = yield* store
|
||||
.get(sessionID)
|
||||
.pipe(
|
||||
Effect.flatMap((session) =>
|
||||
session ? Effect.succeed(session) : Effect.die("manual compaction test session missing"),
|
||||
),
|
||||
)
|
||||
const session = yield* insertSession(sessionID, { parent_id: parentID })
|
||||
|
||||
const delta = yield* bus
|
||||
.subscribe(SessionEvent.Compaction.Delta)
|
||||
@@ -265,3 +276,66 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("forked session compaction reuses the fork root prompt cache key", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
const sessionID = Session.ID.make("ses_fork_compaction")
|
||||
const rootID = Session.ID.make("ses_fork_compaction_root")
|
||||
const session = yield* insertSession(sessionID, {
|
||||
fork_session_id: rootID,
|
||||
fork_boundary: { type: "before", messageID: SessionMessage.ID.create() },
|
||||
})
|
||||
expect(
|
||||
yield* compaction.compactManual({
|
||||
session,
|
||||
messages: [
|
||||
{
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user",
|
||||
text: "Summarize the forked conversation.",
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
},
|
||||
],
|
||||
inputID: SessionMessage.ID.make("msg_fork_compaction"),
|
||||
}),
|
||||
).toEqual({ status: "completed" })
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.promptCacheKey).toBe(rootID)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps session context hooks away from compaction requests", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
// Conversation-shaping hooks must not observe housekeeping requests: compaction
|
||||
// opts out of context hooks, so the transcript passes through unchanged.
|
||||
const hooks = yield* PluginHooks.Service
|
||||
yield* hooks.register("session", "context", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.system.push(SystemPart.make("Injected conversation context"))
|
||||
}),
|
||||
)
|
||||
const session = yield* insertSession(Session.ID.make("ses_hook_compaction"))
|
||||
expect(
|
||||
yield* compaction.compactManual({
|
||||
session,
|
||||
messages: [
|
||||
{
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user",
|
||||
text: "Summarize this conversation.",
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
},
|
||||
],
|
||||
inputID: SessionMessage.ID.make("msg_hook_compaction"),
|
||||
}),
|
||||
).toEqual({ status: "completed" })
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.system).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { LLMClient, LLMEvent, LanguageModel, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
@@ -14,6 +14,7 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
@@ -78,7 +79,15 @@ const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
})
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Agent.node, SessionTitle.node]),
|
||||
LayerNode.group([
|
||||
Database.node,
|
||||
Bus.node,
|
||||
SessionProjector.node,
|
||||
SessionStore.node,
|
||||
Agent.node,
|
||||
PluginHooks.node,
|
||||
SessionTitle.node,
|
||||
]),
|
||||
[
|
||||
[llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
@@ -155,6 +164,9 @@ it.effect("generates a title from the sole user message and renames the session"
|
||||
"x-opencode-session": sessionID,
|
||||
"x-opencode-client": "opencode",
|
||||
})
|
||||
expect(requests[0]?.promptCacheKey).toBe(sessionID)
|
||||
expect(requests[0]?.tools).toEqual([])
|
||||
expect(requests[0]?.system.map((part) => part.text)).toEqual(["You are a title generator."])
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
|
||||
const renamed = yield* store.get(sessionID)
|
||||
expect(renamed?.title).toBe("Generated Title")
|
||||
@@ -323,6 +335,38 @@ it.effect("retries after a failed title request", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps session context hooks away from title requests", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
titleStream = successfulTitle
|
||||
const agentService = yield* Agent.Service
|
||||
yield* agentService.transform((editor) => {
|
||||
editor.update(Agent.ID.make("title"), (agent) => {
|
||||
agent.mode = "primary"
|
||||
agent.hidden = true
|
||||
agent.system = "You are a title generator."
|
||||
})
|
||||
})
|
||||
// Conversation-shaping hooks must not observe housekeeping requests: title
|
||||
// generation opts out of context hooks, so the transcript passes through unchanged.
|
||||
const hooks = yield* PluginHooks.Service
|
||||
yield* hooks.register("session", "context", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.system.push(SystemPart.make("Keep titles in sentence case."))
|
||||
}),
|
||||
)
|
||||
const sessionID = Session.ID.make("ses_title_context_hook")
|
||||
yield* insertSession(sessionID)
|
||||
yield* prompt(sessionID, "Hook this title request")
|
||||
|
||||
const title = yield* SessionTitle.Service
|
||||
yield* title.generateForFirstPrompt(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.system.map((part) => part.text)).toEqual(["You are a title generator."])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves a manual rename completed while generation is in flight", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110"
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "catalog:"
|
||||
|
||||
@@ -1376,7 +1376,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
<StartupLoading ready={plugins.ready} />
|
||||
</Show>
|
||||
<Show when={showReconnecting()}>
|
||||
<Reconnecting managed={client.restart !== undefined} />
|
||||
<Reconnecting />
|
||||
</Show>
|
||||
<MigrationOverlay />
|
||||
<Toast />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RGBA } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function Reconnecting(props: { managed?: boolean }) {
|
||||
export function Reconnecting() {
|
||||
const theme = useTheme("elevated")
|
||||
|
||||
return (
|
||||
@@ -28,12 +28,8 @@ export function Reconnecting(props: { managed?: boolean }) {
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
>
|
||||
<Spinner color={theme.text.default}>{props.managed ? "Restarting service..." : "Connection lost..."}</Spinner>
|
||||
<text fg={theme.text.subdued}>
|
||||
{props.managed
|
||||
? "Your session will resume automatically."
|
||||
: "Reconnecting to the server automatically."}
|
||||
</text>
|
||||
<Spinner color={theme.text.default}>Restarting service...</Spinner>
|
||||
<text fg={theme.text.subdued}>Your session will resume automatically.</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user