authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-06 02:59:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 02:59:21-07:00
log5fa35d211682ee02e34fba0711838ca431ed003b
tree33fb22342572af5a01d6132e943fcc7d2a7d4d95
parente4e9fa9a0b1a77e393ab6cffd75ec22dbb258bdc
signature Signed by PGP key B5690EEEBB952194

refactor(server): let adapters declare context compaction (#10112)

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 );
25062506
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 );
25152513
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 });
34283429
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 );
192192
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
981987
982const routing = makeProviderServiceLayer();988const routing = makeProviderServiceLayer();
983989
990const customCompactionDriver = ProviderDriverKind.make("custom-compaction-provider");
991const nativeCompactionInstanceId = ProviderInstanceId.make("native-compaction");
992const slashCompactionInstanceId = ProviderInstanceId.make("slash-compaction");
993const unsupportedCompactionInstanceId = ProviderInstanceId.make("unsupported-compaction");
994const customNativeCompaction = makeFakeCodexAdapter(customCompactionDriver);
995const customSlashCompaction = makeFakeCodexAdapter(customCompactionDriver);
996const unsupportedCompaction = makeFakeCodexAdapter(customCompactionDriver);
997const 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
1017declaredCompaction.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
984const antigravityDriver = ProviderDriverKind.make("antigravity");1109const antigravityDriver = ProviderDriverKind.make("antigravity");
985const replacementAntigravity = makeFakeCodexAdapter(antigravityDriver);1110const replacementAntigravity = makeFakeCodexAdapter(antigravityDriver);
986const originalAntigravityInstanceId = ProviderInstanceId.make("antigravity-personal");1111const 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";
76import * as ProjectionSnapshotQuery from "../../orchestration/Services/ProjectionSnapshotQuery.ts";76import * as ProjectionSnapshotQuery from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
77const isModelSelection = Schema.is(ModelSelection);77const isModelSelection = Schema.is(ModelSelection);
7878
79/** How long a manual context compaction may run before ProviderService gives up on it. */
80const COMPACTION_COMPLETION_TIMEOUT = "10 minutes";
81
79interface PendingCompaction {82interface 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 nativeCompaction1604 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";
2727
28export type ProviderSessionModelSwitchMode = "in-session" | "unsupported";28export type ProviderSessionModelSwitchMode = "in-session" | "unsupported";
2929
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 */
35export 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
30export interface ProviderAdapterCapabilities {45export 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>;
7287
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>;
7790
78 /**91 /**
79 * Interrupt an active turn.92 * Interrupt an active turn.