| author | |
| committer | |
| log | 5fa35d211682ee02e34fba0711838ca431ed003b |
| tree | 33fb22342572af5a01d6132e943fcc7d2a7d4d95 |
| parent | e4e9fa9a0b1a77e393ab6cffd75ec22dbb258bdc |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>11 files changed, 185 insertions(+), 36 deletions(-)
apps/server/src/provider/Layers/AntigravityAdapter.ts+1| ... | @@ -1245,6 +1245,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi | ... | @@ -1245,6 +1245,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi |
| 1245 | return { | 1245 | return { |
| 1246 | provider: PROVIDER, | 1246 | provider: PROVIDER, |
| 1247 | capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false }, | 1247 | capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false }, |
| 1248 | compaction: { type: "slash-command", command: "/compact" }, | ||
| 1248 | startSession, | 1249 | startSession, |
| 1249 | sendTurn, | 1250 | sendTurn, |
| 1250 | interruptTurn, | 1251 | interruptTurn, |
apps/server/src/provider/Layers/ClaudeAdapter.ts+1| ... | @@ -5075,6 +5075,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -5075,6 +5075,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 5075 | capabilities: { | 5075 | capabilities: { |
| 5076 | sessionModelSwitch: "in-session", | 5076 | sessionModelSwitch: "in-session", |
| 5077 | }, | 5077 | }, |
| 5078 | compaction: { type: "slash-command", command: "/compact" }, | ||
| 5078 | startSession, | 5079 | startSession, |
| 5079 | sendTurn, | 5080 | sendTurn, |
| 5080 | interruptTurn, | 5081 | interruptTurn, |
apps/server/src/provider/Layers/CodexAdapter.test.ts+2-1| ... | @@ -353,7 +353,8 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { | ... | @@ -353,7 +353,8 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { |
| 353 | Stream.runHead, | 353 | Stream.runHead, |
| 354 | Effect.forkChild, | 354 | Effect.forkChild, |
| 355 | ); | 355 | ); |
| 356 | yield* adapter.compactThread!(threadId); | 356 | NodeAssert.ok(adapter.compaction?.type === "native"); |
| 357 | yield* adapter.compaction.start(threadId); | ||
| 357 | yield* runtime.emit({ | 358 | yield* runtime.emit({ |
| 358 | id: asEventId("evt-compaction-item-completed"), | 359 | id: asEventId("evt-compaction-item-completed"), |
| 359 | kind: "notification", | 360 | kind: "notification", |
apps/server/src/provider/Layers/CodexAdapter.ts+7-9| ... | @@ -2504,14 +2504,12 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -2504,14 +2504,12 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2504 | ), | 2504 | ), |
| 2505 | ); | 2505 | ); |
| 2506 | 2506 | ||
| 2507 | const compactThread: NonNullable<CodexAdapterShape["compactThread"]> = Effect.fn("compactThread")( | 2507 | const compactThread = Effect.fn("compactThread")(function* (threadId: ThreadId) { |
| 2508 | function* (threadId) { | 2508 | const session = yield* requireSession(threadId); |
| 2509 | const session = yield* requireSession(threadId); | 2509 | yield* session.runtime.compactThread.pipe( |
| 2510 | yield* session.runtime.compactThread.pipe( | 2510 | Effect.mapError((cause) => mapCodexRuntimeError(threadId, "thread/compact/start", cause)), |
| 2511 | Effect.mapError((cause) => mapCodexRuntimeError(threadId, "thread/compact/start", cause)), | 2511 | ); |
| 2512 | ); | 2512 | }); |
| 2513 | }, | ||
| 2514 | ); | ||
| 2515 | 2513 | ||
| 2516 | const readThread: CodexAdapterShape["readThread"] = (threadId) => | 2514 | const readThread: CodexAdapterShape["readThread"] = (threadId) => |
| 2517 | requireSession(threadId).pipe( | 2515 | requireSession(threadId).pipe( |
| ... | @@ -2658,7 +2656,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -2658,7 +2656,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2658 | }, | 2656 | }, |
| 2659 | startSession, | 2657 | startSession, |
| 2660 | sendTurn, | 2658 | sendTurn, |
| 2661 | compactThread, | 2659 | compaction: { type: "native", start: compactThread }, |
| 2662 | interruptTurn, | 2660 | interruptTurn, |
| 2663 | readThread, | 2661 | readThread, |
| 2664 | rollbackThread, | 2662 | rollbackThread, |
apps/server/src/provider/Layers/CursorAdapter.ts+1| ... | @@ -1212,6 +1212,7 @@ export function makeCursorAdapter( | ... | @@ -1212,6 +1212,7 @@ export function makeCursorAdapter( |
| 1212 | return { | 1212 | return { |
| 1213 | provider: PROVIDER, | 1213 | provider: PROVIDER, |
| 1214 | capabilities: { sessionModelSwitch: "in-session" }, | 1214 | capabilities: { sessionModelSwitch: "in-session" }, |
| 1215 | compaction: { type: "slash-command", command: "/compress" }, | ||
| 1215 | startSession, | 1216 | startSession, |
| 1216 | sendTurn, | 1217 | sendTurn, |
| 1217 | interruptTurn, | 1218 | interruptTurn, |
apps/server/src/provider/Layers/GrokAdapter.ts+1| ... | @@ -2128,6 +2128,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte | ... | @@ -2128,6 +2128,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte |
| 2128 | return { | 2128 | return { |
| 2129 | provider: PROVIDER, | 2129 | provider: PROVIDER, |
| 2130 | capabilities: { sessionModelSwitch: "in-session" }, | 2130 | capabilities: { sessionModelSwitch: "in-session" }, |
| 2131 | compaction: { type: "slash-command", command: "/compact" }, | ||
| 2131 | startSession, | 2132 | startSession, |
| 2132 | sendTurn, | 2133 | sendTurn, |
| 2133 | interruptTurn, | 2134 | interruptTurn, |
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+2-1| ... | @@ -1080,7 +1080,8 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { | ... | @@ -1080,7 +1080,8 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 1080 | threadId, | 1080 | threadId, |
| 1081 | runtimeMode: "full-access", | 1081 | runtimeMode: "full-access", |
| 1082 | }); | 1082 | }); |
| 1083 | yield* adapter.compactThread!( | 1083 | NodeAssert.ok(adapter.compaction?.type === "native"); |
| 1084 | yield* adapter.compaction.start( | ||
| 1084 | threadId, | 1085 | threadId, |
| 1085 | createModelSelection(ProviderInstanceId.make("opencode"), "openai/gpt-5"), | 1086 | createModelSelection(ProviderInstanceId.make("opencode"), "openai/gpt-5"), |
| 1086 | ); | 1087 | ); |
apps/server/src/provider/Layers/OpenCodeAdapter.ts+6-4| ... | @@ -4,6 +4,7 @@ import { | ... | @@ -4,6 +4,7 @@ import { |
| 4 | ProviderDriverKind, | 4 | ProviderDriverKind, |
| 5 | ProviderInstanceId, | 5 | ProviderInstanceId, |
| 6 | type ProviderRuntimeEvent, | 6 | type ProviderRuntimeEvent, |
| 7 | type ProviderSendTurnInput, | ||
| 7 | type ProviderSession, | 8 | type ProviderSession, |
| 8 | RuntimeItemId, | 9 | RuntimeItemId, |
| 9 | RuntimeRequestId, | 10 | RuntimeRequestId, |
| ... | @@ -3426,9 +3427,10 @@ export function makeOpenCodeAdapter( | ... | @@ -3426,9 +3427,10 @@ export function makeOpenCodeAdapter( |
| 3426 | ); | 3427 | ); |
| 3427 | }); | 3428 | }); |
| 3428 | 3429 | ||
| 3429 | const compactThread: NonNullable<OpenCodeAdapterShape["compactThread"]> = Effect.fn( | 3430 | const compactThread = Effect.fn("compactThread")(function* ( |
| 3430 | "compactThread", | 3431 | threadId: ThreadId, |
| 3431 | )(function* (threadId, requestedModelSelection) { | 3432 | requestedModelSelection?: ProviderSendTurnInput["modelSelection"], |
| 3433 | ) { | ||
| 3432 | const context = yield* ensureSessionContext(sessions, threadId); | 3434 | const context = yield* ensureSessionContext(sessions, threadId); |
| 3433 | yield* awaitOpenCodeContextReady(context); | 3435 | yield* awaitOpenCodeContextReady(context); |
| 3434 | const modelSelection = | 3436 | const modelSelection = |
| ... | @@ -3843,7 +3845,7 @@ export function makeOpenCodeAdapter( | ... | @@ -3843,7 +3845,7 @@ export function makeOpenCodeAdapter( |
| 3843 | }, | 3845 | }, |
| 3844 | startSession, | 3846 | startSession, |
| 3845 | sendTurn, | 3847 | sendTurn, |
| 3846 | compactThread, | 3848 | compaction: { type: "native", start: compactThread }, |
| 3847 | interruptTurn, | 3849 | interruptTurn, |
| 3848 | respondToRequest, | 3850 | respondToRequest, |
| 3849 | respondToUserInput, | 3851 | respondToUserInput, |
apps/server/src/provider/Layers/ProviderService.test.ts+127-2| ... | @@ -190,7 +190,7 @@ function makeFakeCodexAdapter( | ... | @@ -190,7 +190,7 @@ function makeFakeCodexAdapter( |
| 190 | Effect.void, | 190 | Effect.void, |
| 191 | ); | 191 | ); |
| 192 | 192 | ||
| 193 | const compactThread = vi.fn((threadId: ThreadId) => | 193 | const compactThread = vi.fn((threadId: ThreadId): Effect.Effect<void, ProviderAdapterError> => |
| 194 | Effect.sync(() => | 194 | Effect.sync(() => |
| 195 | emit({ | 195 | emit({ |
| 196 | type: "thread.state.changed", | 196 | type: "thread.state.changed", |
| ... | @@ -278,7 +278,13 @@ function makeFakeCodexAdapter( | ... | @@ -278,7 +278,13 @@ function makeFakeCodexAdapter( |
| 278 | }, | 278 | }, |
| 279 | startSession, | 279 | startSession, |
| 280 | sendTurn, | 280 | sendTurn, |
| 281 | ...(provider === CODEX_DRIVER ? { compactThread } : {}), | 281 | ...(provider === CODEX_DRIVER |
| 282 | ? { compaction: { type: "native", start: compactThread } } | ||
| 283 | : provider === CURSOR_DRIVER | ||
| 284 | ? { compaction: { type: "slash-command", command: "/compress" } } | ||
| 285 | : provider === CLAUDE_AGENT_DRIVER | ||
| 286 | ? { compaction: { type: "slash-command", command: "/compact" } } | ||
| 287 | : {}), | ||
| 282 | interruptTurn, | 288 | interruptTurn, |
| 283 | respondToRequest, | 289 | respondToRequest, |
| 284 | respondToUserInput, | 290 | respondToUserInput, |
| ... | @@ -981,6 +987,125 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance | ... | @@ -981,6 +987,125 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance |
| 981 | 987 | ||
| 982 | const routing = makeProviderServiceLayer(); | 988 | const routing = makeProviderServiceLayer(); |
| 983 | 989 | ||
| 990 | const customCompactionDriver = ProviderDriverKind.make("custom-compaction-provider"); | ||
| 991 | const nativeCompactionInstanceId = ProviderInstanceId.make("native-compaction"); | ||
| 992 | const slashCompactionInstanceId = ProviderInstanceId.make("slash-compaction"); | ||
| 993 | const unsupportedCompactionInstanceId = ProviderInstanceId.make("unsupported-compaction"); | ||
| 994 | const customNativeCompaction = makeFakeCodexAdapter(customCompactionDriver); | ||
| 995 | const customSlashCompaction = makeFakeCodexAdapter(customCompactionDriver); | ||
| 996 | const unsupportedCompaction = makeFakeCodexAdapter(customCompactionDriver); | ||
| 997 | const declaredCompaction = makeProviderServiceLayer({ | ||
| 998 | registry: makeStaticInstanceRegistry([ | ||
| 999 | [ | ||
| 1000 | nativeCompactionInstanceId, | ||
| 1001 | { | ||
| 1002 | ...customNativeCompaction.adapter, | ||
| 1003 | compaction: { type: "native", start: customNativeCompaction.compactThread }, | ||
| 1004 | }, | ||
| 1005 | ], | ||
| 1006 | [ | ||
| 1007 | slashCompactionInstanceId, | ||
| 1008 | { | ||
| 1009 | ...customSlashCompaction.adapter, | ||
| 1010 | compaction: { type: "slash-command", command: "/reduce-context" }, | ||
| 1011 | }, | ||
| 1012 | ], | ||
| 1013 | [unsupportedCompactionInstanceId, unsupportedCompaction.adapter], | ||
| 1014 | ]), | ||
| 1015 | }); | ||
| 1016 | |||
| 1017 | declaredCompaction.layer("ProviderService declared compaction", (it) => { | ||
| 1018 | it.effect("starts declared native compaction instead of sending a prompt", () => | ||
| 1019 | Effect.gen(function* () { | ||
| 1020 | const provider = yield* ProviderService.ProviderService; | ||
| 1021 | const threadId = asThreadId("custom-native-compaction"); | ||
| 1022 | const requestId = MessageId.make("custom-native-request"); | ||
| 1023 | yield* provider.startSession(threadId, { | ||
| 1024 | providerInstanceId: nativeCompactionInstanceId, | ||
| 1025 | threadId, | ||
| 1026 | runtimeMode: "full-access", | ||
| 1027 | }); | ||
| 1028 | const compactedEventFiber = yield* provider.streamEvents.pipe( | ||
| 1029 | Stream.filter( | ||
| 1030 | (event) => event.threadId === threadId && event.type === "thread.state.changed", | ||
| 1031 | ), | ||
| 1032 | Stream.runHead, | ||
| 1033 | Effect.forkChild({ startImmediately: true }), | ||
| 1034 | ); | ||
| 1035 | yield* advanceTestClock(50); | ||
| 1036 | yield* provider.compactThread(threadId, undefined, requestId); | ||
| 1037 | const compacted = Option.getOrThrow(yield* Fiber.join(compactedEventFiber)); | ||
| 1038 | assert.equal(compacted.requestId, String(requestId)); | ||
| 1039 | assert.equal(customNativeCompaction.compactThread.mock.calls.length, 1); | ||
| 1040 | assert.equal(customNativeCompaction.sendTurn.mock.calls.length, 0); | ||
| 1041 | yield* provider.stopSession({ threadId }); | ||
| 1042 | }), | ||
| 1043 | ); | ||
| 1044 | |||
| 1045 | it.effect("sends the declared slash command as the compaction turn", () => | ||
| 1046 | Effect.gen(function* () { | ||
| 1047 | const provider = yield* ProviderService.ProviderService; | ||
| 1048 | const threadId = asThreadId("custom-slash-compaction"); | ||
| 1049 | const requestId = MessageId.make("custom-slash-request"); | ||
| 1050 | const modelSelection = createModelSelection(slashCompactionInstanceId, "custom-model"); | ||
| 1051 | yield* provider.startSession(threadId, { | ||
| 1052 | providerInstanceId: slashCompactionInstanceId, | ||
| 1053 | threadId, | ||
| 1054 | runtimeMode: "full-access", | ||
| 1055 | }); | ||
| 1056 | const compactedEventFiber = yield* provider.streamEvents.pipe( | ||
| 1057 | Stream.filter( | ||
| 1058 | (event) => event.threadId === threadId && event.type === "thread.state.changed", | ||
| 1059 | ), | ||
| 1060 | Stream.runHead, | ||
| 1061 | Effect.forkChild({ startImmediately: true }), | ||
| 1062 | ); | ||
| 1063 | const compactFiber = yield* provider | ||
| 1064 | .compactThread(threadId, modelSelection, requestId) | ||
| 1065 | .pipe(Effect.forkChild); | ||
| 1066 | yield* advanceTestClock(50); | ||
| 1067 | customSlashCompaction.emit({ | ||
| 1068 | type: "turn.completed", | ||
| 1069 | eventId: asEventId("custom-slash-completed"), | ||
| 1070 | provider: customCompactionDriver, | ||
| 1071 | createdAt: "2026-01-01T00:00:01.000Z", | ||
| 1072 | threadId, | ||
| 1073 | turnId: asTurnId(`turn-${threadId}`), | ||
| 1074 | payload: { state: "completed" }, | ||
| 1075 | }); | ||
| 1076 | yield* Fiber.join(compactFiber); | ||
| 1077 | const compacted = Option.getOrThrow(yield* Fiber.join(compactedEventFiber)); | ||
| 1078 | assert.equal(compacted.requestId, String(requestId)); | ||
| 1079 | assert.equal(customSlashCompaction.compactThread.mock.calls.length, 0); | ||
| 1080 | assert.equal(customSlashCompaction.sendTurn.mock.calls.length, 1); | ||
| 1081 | assert.equal(customSlashCompaction.sendTurn.mock.calls[0]?.[0].input, "/reduce-context"); | ||
| 1082 | assert.deepEqual( | ||
| 1083 | customSlashCompaction.sendTurn.mock.calls[0]?.[0].modelSelection, | ||
| 1084 | modelSelection, | ||
| 1085 | ); | ||
| 1086 | yield* provider.stopSession({ threadId }); | ||
| 1087 | }), | ||
| 1088 | ); | ||
| 1089 | |||
| 1090 | it.effect("rejects compaction for adapters without a declared strategy", () => | ||
| 1091 | Effect.gen(function* () { | ||
| 1092 | const provider = yield* ProviderService.ProviderService; | ||
| 1093 | const threadId = asThreadId("custom-unsupported-compaction"); | ||
| 1094 | yield* provider.startSession(threadId, { | ||
| 1095 | providerInstanceId: unsupportedCompactionInstanceId, | ||
| 1096 | threadId, | ||
| 1097 | runtimeMode: "full-access", | ||
| 1098 | }); | ||
| 1099 | const failure = yield* provider.compactThread(threadId).pipe(Effect.flip); | ||
| 1100 | assert.instanceOf(failure, ProviderValidationError); | ||
| 1101 | assert.include(failure.message, "does not support context compaction"); | ||
| 1102 | assert.equal(unsupportedCompaction.sendTurn.mock.calls.length, 0); | ||
| 1103 | assert.equal(unsupportedCompaction.compactThread.mock.calls.length, 0); | ||
| 1104 | yield* provider.stopSession({ threadId }); | ||
| 1105 | }), | ||
| 1106 | ); | ||
| 1107 | }); | ||
| 1108 | |||
| 984 | const antigravityDriver = ProviderDriverKind.make("antigravity"); | 1109 | const antigravityDriver = ProviderDriverKind.make("antigravity"); |
| 985 | const replacementAntigravity = makeFakeCodexAdapter(antigravityDriver); | 1110 | const replacementAntigravity = makeFakeCodexAdapter(antigravityDriver); |
| 986 | const originalAntigravityInstanceId = ProviderInstanceId.make("antigravity-personal"); | 1111 | const originalAntigravityInstanceId = ProviderInstanceId.make("antigravity-personal"); |
apps/server/src/provider/Layers/ProviderService.ts+20-15| ... | @@ -76,6 +76,9 @@ import * as ServerSettings from "../../serverSettings.ts"; | ... | @@ -76,6 +76,9 @@ import * as ServerSettings from "../../serverSettings.ts"; |
| 76 | import * as ProjectionSnapshotQuery from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; | 76 | import * as ProjectionSnapshotQuery from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; |
| 77 | const isModelSelection = Schema.is(ModelSelection); | 77 | const isModelSelection = Schema.is(ModelSelection); |
| 78 | 78 | ||
| 79 | /** How long a manual context compaction may run before ProviderService gives up on it. */ | ||
| 80 | const COMPACTION_COMPLETION_TIMEOUT = "10 minutes"; | ||
| 81 | |||
| 79 | interface PendingCompaction { | 82 | interface PendingCompaction { |
| 80 | readonly completion: Deferred.Deferred<string>; | 83 | readonly completion: Deferred.Deferred<string>; |
| 81 | readonly native: boolean; | 84 | readonly native: boolean; |
| ... | @@ -1522,18 +1525,24 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1522,18 +1525,24 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1522 | "provider.thread_id": threadId, | 1525 | "provider.thread_id": threadId, |
| 1523 | }); | 1526 | }); |
| 1524 | yield* McpSessionRegistry.touchActiveMcpThread(threadId); | 1527 | yield* McpSessionRegistry.touchActiveMcpThread(threadId); |
| 1525 | const nativeCompaction = routed.adapter.compactThread; | 1528 | const compaction = routed.adapter.compaction; |
| 1529 | if (compaction === undefined) { | ||
| 1530 | return yield* toValidationError( | ||
| 1531 | "ProviderService.compactThread", | ||
| 1532 | `Provider '${routed.adapter.provider}' does not support context compaction.`, | ||
| 1533 | ); | ||
| 1534 | } | ||
| 1526 | const completion = yield* Deferred.make<string>(); | 1535 | const completion = yield* Deferred.make<string>(); |
| 1527 | const pending: PendingCompaction = { | 1536 | const pending: PendingCompaction = { |
| 1528 | completion, | 1537 | completion, |
| 1529 | native: nativeCompaction !== undefined, | 1538 | native: compaction.type === "native", |
| 1530 | providerInstanceId: routed.instanceId, | 1539 | providerInstanceId: routed.instanceId, |
| 1531 | requestId, | 1540 | requestId, |
| 1532 | earlyEvents: [], | 1541 | earlyEvents: [], |
| 1533 | compactedEventObserved: false, | 1542 | compactedEventObserved: false, |
| 1534 | expectedTurnId: undefined, | 1543 | expectedTurnId: undefined, |
| 1535 | }; | 1544 | }; |
| 1536 | if (nativeCompaction !== undefined && timedOutNativeCompactions.has(threadId)) { | 1545 | if (compaction.type === "native" && timedOutNativeCompactions.has(threadId)) { |
| 1537 | return yield* new ProviderAdapterRequestError({ | 1546 | return yield* new ProviderAdapterRequestError({ |
| 1538 | provider: routed.adapter.provider, | 1547 | provider: routed.adapter.provider, |
| 1539 | method: "thread/compact", | 1548 | method: "thread/compact", |
| ... | @@ -1558,14 +1567,10 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1558,14 +1567,10 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1558 | pendingCompactions.delete(threadId); | 1567 | pendingCompactions.delete(threadId); |
| 1559 | } | 1568 | } |
| 1560 | }); | 1569 | }); |
| 1561 | const nativeCompletionTimeout = | ||
| 1562 | routed.adapter.provider === "codex" || routed.adapter.provider === "opencode" | ||
| 1563 | ? "10 minutes" | ||
| 1564 | : "30 seconds"; | ||
| 1565 | const awaitNativeCompaction = (start: Effect.Effect<void, ProviderAdapterError>) => | 1570 | const awaitNativeCompaction = (start: Effect.Effect<void, ProviderAdapterError>) => |
| 1566 | start.pipe( | 1571 | start.pipe( |
| 1567 | Effect.andThen(Deferred.await(completion)), | 1572 | Effect.andThen(Deferred.await(completion)), |
| 1568 | Effect.timeout(nativeCompletionTimeout), | 1573 | Effect.timeout(COMPACTION_COMPLETION_TIMEOUT), |
| 1569 | Effect.catchTag("TimeoutError", (cause) => | 1574 | Effect.catchTag("TimeoutError", (cause) => |
| 1570 | Effect.sync(() => { | 1575 | Effect.sync(() => { |
| 1571 | timedOutNativeCompactions.add(threadId); | 1576 | timedOutNativeCompactions.add(threadId); |
| ... | @@ -1575,7 +1580,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1575,7 +1580,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1575 | new ProviderAdapterRequestError({ | 1580 | new ProviderAdapterRequestError({ |
| 1576 | provider: routed.adapter.provider, | 1581 | provider: routed.adapter.provider, |
| 1577 | method: "thread/compact", | 1582 | method: "thread/compact", |
| 1578 | detail: `Provider did not report completed context compaction within ${nativeCompletionTimeout}.`, | 1583 | detail: `Provider did not report completed context compaction within ${COMPACTION_COMPLETION_TIMEOUT}.`, |
| 1579 | cause, | 1584 | cause, |
| 1580 | }), | 1585 | }), |
| 1581 | ), | 1586 | ), |
| ... | @@ -1584,24 +1589,24 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1584,24 +1589,24 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1584 | ), | 1589 | ), |
| 1585 | ); | 1590 | ); |
| 1586 | const awaitFallbackCompaction = Deferred.await(completion).pipe( | 1591 | const awaitFallbackCompaction = Deferred.await(completion).pipe( |
| 1587 | Effect.timeout("10 minutes"), | 1592 | Effect.timeout(COMPACTION_COMPLETION_TIMEOUT), |
| 1588 | Effect.mapError( | 1593 | Effect.mapError( |
| 1589 | (cause) => | 1594 | (cause) => |
| 1590 | new ProviderAdapterRequestError({ | 1595 | new ProviderAdapterRequestError({ |
| 1591 | provider: routed.adapter.provider, | 1596 | provider: routed.adapter.provider, |
| 1592 | method: "turn/start", | 1597 | method: "turn/start", |
| 1593 | detail: "Provider did not finish context compaction within 10 minutes.", | 1598 | detail: `Provider did not finish context compaction within ${COMPACTION_COMPLETION_TIMEOUT}.`, |
| 1594 | cause, | 1599 | cause, |
| 1595 | }), | 1600 | }), |
| 1596 | ), | 1601 | ), |
| 1597 | ); | 1602 | ); |
| 1598 | const terminal = yield* ( | 1603 | const terminal = yield* ( |
| 1599 | nativeCompaction | 1604 | compaction.type === "native" |
| 1600 | ? awaitNativeCompaction(nativeCompaction(routed.threadId, modelSelection)) | 1605 | ? awaitNativeCompaction(compaction.start(routed.threadId, modelSelection)) |
| 1601 | : Effect.gen(function* () { | 1606 | : Effect.gen(function* () { |
| 1602 | const turn = yield* sendTurn({ | 1607 | const turn = yield* sendTurn({ |
| 1603 | threadId, | 1608 | threadId, |
| 1604 | input: routed.adapter.provider === "cursor" ? "/compress" : "/compact", | 1609 | input: compaction.command, |
| 1605 | ...(modelSelection !== undefined ? { modelSelection } : {}), | 1610 | ...(modelSelection !== undefined ? { modelSelection } : {}), |
| 1606 | }).pipe( | 1611 | }).pipe( |
| 1607 | Effect.onError(() => | 1612 | Effect.onError(() => |
| ... | @@ -1621,7 +1626,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1621,7 +1626,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1621 | if (terminal !== "completed") { | 1626 | if (terminal !== "completed") { |
| 1622 | return yield* new ProviderAdapterRequestError({ | 1627 | return yield* new ProviderAdapterRequestError({ |
| 1623 | provider: routed.adapter.provider, | 1628 | provider: routed.adapter.provider, |
| 1624 | method: nativeCompaction ? "thread/compact" : "turn/start", | 1629 | method: compaction.type === "native" ? "thread/compact" : "turn/start", |
| 1625 | detail: `Context compaction ended with ${terminal}.`, | 1630 | detail: `Context compaction ended with ${terminal}.`, |
| 1626 | }); | 1631 | }); |
| 1627 | } | 1632 | } |
apps/server/src/provider/Services/ProviderAdapter.ts+17-4| ... | @@ -27,6 +27,21 @@ import type * as Stream from "effect/Stream"; | ... | @@ -27,6 +27,21 @@ import type * as Stream from "effect/Stream"; |
| 27 | 27 | ||
| 28 | export type ProviderSessionModelSwitchMode = "in-session" | "unsupported"; | 28 | export type ProviderSessionModelSwitchMode = "in-session" | "unsupported"; |
| 29 | 29 | ||
| 30 | /** | ||
| 31 | * How ProviderService runs manual context compaction for an adapter. | ||
| 32 | * Native adapters expose a start call and must emit a compacted thread state | ||
| 33 | * when they finish. Slash-command adapters get the command sent as a turn. | ||
| 34 | */ | ||
| 35 | export type ProviderCompaction<TError> = | ||
| 36 | | { | ||
| 37 | readonly type: "native"; | ||
| 38 | readonly start: ( | ||
| 39 | threadId: ThreadId, | ||
| 40 | modelSelection?: ProviderSendTurnInput["modelSelection"], | ||
| 41 | ) => Effect.Effect<void, TError>; | ||
| 42 | } | ||
| 43 | | { readonly type: "slash-command"; readonly command: `/${string}` }; | ||
| 44 | |||
| 30 | export interface ProviderAdapterCapabilities { | 45 | export interface ProviderAdapterCapabilities { |
| 31 | /** | 46 | /** |
| 32 | * Declares whether changing the model on an existing session is supported. | 47 | * Declares whether changing the model on an existing session is supported. |
| ... | @@ -70,10 +85,8 @@ export interface ProviderAdapterShape<TError> { | ... | @@ -70,10 +85,8 @@ export interface ProviderAdapterShape<TError> { |
| 70 | input: ProviderSendTurnInput, | 85 | input: ProviderSendTurnInput, |
| 71 | ) => Effect.Effect<ProviderTurnStartResult, TError>; | 86 | ) => Effect.Effect<ProviderTurnStartResult, TError>; |
| 72 | 87 | ||
| 73 | readonly compactThread?: ( | 88 | /** Omitted when this adapter does not support manual context compaction. */ |
| 74 | threadId: ThreadId, | 89 | readonly compaction?: ProviderCompaction<TError>; |
| 75 | modelSelection?: ProviderSendTurnInput["modelSelection"], | ||
| 76 | ) => Effect.Effect<void, TError>; | ||
| 77 | 90 | ||
| 78 | /** | 91 | /** |
| 79 | * Interrupt an active turn. | 92 | * Interrupt an active turn. |