tinker-agent 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +81 -11
  3. package/package.json +5 -3
  4. package/src/agent/runtime-context-capabilities.ts +19 -0
  5. package/src/agent/runtime-context-events.ts +127 -0
  6. package/src/agent/runtime-context-maintenance.ts +780 -0
  7. package/src/agent/runtime-hosted-session.ts +443 -0
  8. package/src/agent/runtime-interactions.ts +291 -0
  9. package/src/agent/runtime-prompt-scheduler.ts +182 -0
  10. package/src/agent/runtime-session-contracts.ts +317 -0
  11. package/src/agent/runtime-session.ts +250 -2130
  12. package/src/agent/runtime-skills.ts +544 -0
  13. package/src/cli/command-line.ts +26 -2
  14. package/src/cli/connect-runner.tsx +26 -0
  15. package/src/cli/main.ts +26 -0
  16. package/src/cli/output.ts +1 -1
  17. package/src/cli/public-cli-contract.ts +18 -0
  18. package/src/cli/public-config-contract.ts +1 -1
  19. package/src/cli/runner-dependencies.ts +6 -5
  20. package/src/cli/serve-runner.ts +45 -0
  21. package/src/cli/serve-runtime.ts +100 -0
  22. package/src/context/context-automation-policy.ts +12 -118
  23. package/src/context/context-swap-renderer.ts +14 -0
  24. package/src/events/types.ts +12 -0
  25. package/src/memory/memory-get-tool.ts +1 -1
  26. package/src/observation/observation-builder.ts +128 -48
  27. package/src/remote/client.ts +350 -0
  28. package/src/remote/config.ts +95 -0
  29. package/src/remote/http-server.ts +240 -0
  30. package/src/remote/protocol.ts +228 -0
  31. package/src/remote/service-store.ts +175 -0
  32. package/src/remote/service.ts +219 -0
  33. package/src/remote/sync-hub.ts +95 -0
  34. package/src/session/remote-history-reader.ts +143 -0
  35. package/src/session/resume-projection.ts +47 -21
  36. package/src/session/session-history-access.ts +238 -0
  37. package/src/session/session-store-context-readers.ts +183 -0
  38. package/src/session/session-store-ledger-writer.ts +315 -0
  39. package/src/session/session-store-record-writer.ts +318 -0
  40. package/src/session/session-store-recovery.ts +225 -0
  41. package/src/session/session-store-revisions.ts +1004 -0
  42. package/src/session/session-store-sql.ts +40 -0
  43. package/src/session/session-store-validation.ts +657 -0
  44. package/src/session/session-store.ts +756 -3186
  45. package/src/tools/bash-task.ts +46 -18
  46. package/src/tools/bash.ts +44 -2
  47. package/src/tools/glob.ts +107 -19
  48. package/src/tools/grep-output.ts +130 -0
  49. package/src/tools/grep-pagination.ts +73 -0
  50. package/src/tools/grep-path.ts +11 -0
  51. package/src/tools/grep-snippets.ts +111 -0
  52. package/src/tools/grep.ts +139 -154
  53. package/src/tools/read.ts +0 -9
  54. package/src/tools/recall.ts +106 -50
  55. package/src/tools/registry.ts +4 -6
  56. package/src/tools/ripgrep.ts +19 -26
  57. package/src/tools/shell-process.ts +30 -4
  58. package/src/tools/task-output-range.ts +146 -0
  59. package/src/tools/task-output-tool.ts +35 -5
  60. package/src/tools/task-output.ts +35 -0
  61. package/src/tools/task-stop.ts +2 -1
  62. package/src/tools/task-tool-args.ts +34 -0
  63. package/src/tools/terminal-screen.ts +11 -2
  64. package/src/tools/types.ts +39 -2
  65. package/src/tui/event-store.ts +23 -5
  66. package/src/tui/remote-app.tsx +210 -0
@@ -1,118 +1,108 @@
1
1
  import path from "node:path";
2
+ import { assertContextMaintenanceCapabilities } from "./runtime-context-capabilities";
3
+ import { CompiledContextError } from "../context/compiled-context-validator";
4
+ import {
5
+ DEFAULT_CONTEXT_AUTOMATION_POLICY,
6
+ type ContextAutomationPolicy,
7
+ } from "../context/context-automation-policy";
8
+ import {
9
+ ContextManager,
10
+ type ContextCompactionResult,
11
+ type ContextRetirementResult,
12
+ } from "../context/context-manager";
13
+ import { ContextProtocolError } from "../context/context-protocol-validator";
14
+ import type { BuiltContextRequest } from "../context/context-revision";
15
+ import { ContextRevisionError } from "../context/context-revision-compiler";
16
+ import { createContextSurface } from "../context/context-surface";
17
+ import { CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION } from "../context/recall-retirement-contract";
18
+ import { SwapPlanner } from "../context/swap-planner";
2
19
  import { CompositeEventSink } from "../events/composite-event-sink";
3
20
  import type { EventSink } from "../events/event-sink";
4
21
  import { JsonlEventLog } from "../events/jsonl-event-log";
5
22
  import { ObservationTextLog } from "../events/observation-text-log";
6
- import type {
7
- AgentEvent,
8
- AgentEventInput,
9
- AgentEventType,
10
- ContextRevisionFinishedData,
11
- } from "../events/types";
23
+ import type { AgentEvent, AgentEventInput } from "../events/types";
24
+ import { runtimeIdFactory, type SessionId, type TurnId } from "../ids/runtime-id";
25
+ import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
26
+ import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
12
27
  import {
13
- runtimeIdFactory,
14
- type MessageId,
15
- type RuntimeIdFactory,
16
- type SessionId,
17
- type TurnId,
18
- } from "../ids/runtime-id";
28
+ validateUserMessage,
29
+ type ImageAssetRef,
30
+ type UserMessage,
31
+ } from "../image/image-types";
19
32
  import { loadMcpConfig } from "../mcp/mcp-config";
20
33
  import {
21
34
  createMcpManager,
22
35
  type McpInventorySnapshot,
23
36
  type McpManager,
24
37
  } from "../mcp/mcp-manager";
38
+ import { CommittedPrefixAuditor } from "../model/committed-prefix-auditor";
25
39
  import {
26
40
  materializeModelRequest,
27
41
  ModelRequestMediaAggregateError,
28
42
  type MaterializedModelRequest,
29
- type ModelClient,
30
43
  } from "../model/model-client";
31
- import type { ContextPressure } from "../model/model-request-preflight";
32
- import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
33
- import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
34
- import {
35
- validateUserMessage,
36
- type ImageAssetRef,
37
- type UserMessage,
38
- } from "../image/image-types";
39
- import { projectUserMessage } from "./user-prompt-projection";
40
- import { CommittedPrefixAuditor } from "../model/committed-prefix-auditor";
41
- import { SwapPlanner } from "../context/swap-planner";
42
- import {
43
- commitAgentSkillsContextUpdate,
44
- ContextManager,
45
- ContextManagerError,
46
- type ContextCompactionResult,
47
- type ContextCompactionTrigger,
48
- type ContextRetirementResult,
49
- type ContextRetirementTrigger,
50
- } from "../context/context-manager";
51
- import {
52
- assertMatchingContextBudget,
53
- type ModelContextBudget,
54
- type ModelContextProfile,
55
- } from "../model/model-context-profile";
44
+ import { assertMatchingContextBudget } from "../model/model-context-profile";
45
+ import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
56
46
  import { ObservationBuilder } from "../observation/observation-builder";
57
- import { ContextProtocolError } from "../context/context-protocol-validator";
58
- import { CompiledContextError } from "../context/compiled-context-validator";
59
- import {
60
- ContextRevisionCompiler,
61
- ContextRevisionError,
62
- } from "../context/context-revision-compiler";
63
- import {
64
- changedContextSurfaceComponents,
65
- contextSurfaceChangeManifestHash,
66
- contextSurfaceChanges,
67
- createContextSurface,
68
- sameContextSurface,
69
- type ContextSurfaceComponent,
70
- type StoredContextSurfaceV8,
71
- } from "../context/context-surface";
72
- import {
73
- canonicalSequenceHash,
74
- renderedMessageHash,
75
- } from "../context/compiled-context-hash";
76
- import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
77
- import {
78
- ToolExecutionFatalError,
79
- type ContextMaintenanceHandle,
80
- type ContextStatusRawResult,
81
- type ContextSwapCandidatesRawResult,
82
- type ContextSwapRawResult,
83
- type AskUserRequest,
84
- type AskUserResponse,
85
- type ToolExecutor,
86
- } from "../tools/types";
87
- import type { TurnUndoResult } from "../tools/turn-undo-manager";
88
- import type { Refiner } from "../tools/web-fetch/refiner";
89
- import type { ProjectInstructionManifest } from "../instructions/project-instructions";
90
- import type {
91
- AssistantTextDeltaSink,
92
- AssistantTextDeltaUpdate,
93
- } from "./assistant-text-delta";
47
+ import { SessionError } from "../session/session-errors";
94
48
  import {
95
- SessionStore,
96
49
  createSessionCompatibilityContract,
50
+ SessionStore,
97
51
  type CompletedTurnSnapshot,
98
52
  type SessionRecoveryResult,
99
53
  type StoredSkillActivation,
100
54
  } from "../session/session-store";
101
55
  import { SqliteSessionLedger } from "../session/sqlite-session-ledger";
102
- import { SessionError } from "../session/session-errors";
56
+ import {
57
+ createSkillCatalogSnapshot,
58
+ skillCatalogManifest,
59
+ } from "../skills/skill-catalog";
60
+ import {
61
+ buildActiveSystemPrompt,
62
+ rebindActiveSkills,
63
+ SkillActivationCoordinator,
64
+ } from "../skills/skill-context";
65
+ import type { SkillCatalogSnapshot } from "../skills/skill-loader";
66
+ import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
67
+ import type { TurnUndoResult } from "../tools/turn-undo-manager";
68
+ import { ToolExecutionFatalError, type AskUserRequest } from "../tools/types";
69
+ import type { AssistantTextDeltaUpdate } from "./assistant-text-delta";
70
+ import { ContextMeter } from "./context-meter";
103
71
  import { FatalAgentTurnError, runAgent, type RunAgentInput } from "./loop";
72
+ import { assertPreparedMatchesSurface } from "./runtime-context-events";
73
+ import { RuntimeContextMaintenance } from "./runtime-context-maintenance";
74
+ import { RuntimeInteractions } from "./runtime-interactions";
75
+ import { RuntimePromptScheduler } from "./runtime-prompt-scheduler";
76
+ import {
77
+ RuntimeEventAppendError,
78
+ type AcceptedTurn,
79
+ type AskUserResolution,
80
+ type AskUserSnapshot,
81
+ type BashGuardSnapshot,
82
+ type CompletedTurnHook,
83
+ type CompletedTurnHookFailure,
84
+ type CreateNewRuntimeSessionInput,
85
+ type CreateRuntimeSessionInput,
86
+ type ExecuteTurnInput,
87
+ type PromptSchedulerSnapshot,
88
+ type QueueFollowUpResult,
89
+ type RuntimeSession,
90
+ type RuntimeSessionContext,
91
+ type RuntimeSessionFactoryDependencies,
92
+ type RuntimeSessionState,
93
+ type RuntimeSkillsSnapshot,
94
+ type SessionDisposeReason,
95
+ type SkillsUpdateSummary,
96
+ } from "./runtime-session-contracts";
97
+ import { RuntimeSkills } from "./runtime-skills";
104
98
  import {
105
99
  AdmissionStaleError,
106
100
  SessionLedgerWriteError,
107
- type AgentTurnLedger,
108
101
  type AdmissionBaseToken,
102
+ type AgentTurnLedger,
109
103
  type SessionLedger,
110
104
  } from "./session-ledger";
111
- import { cancellationError, TurnCancelledError } from "./turn-cancellation";
112
- import type { ToolCompletionInput } from "../context/protocol-frame";
113
- import type { BuiltContextRequest } from "../context/context-revision";
114
- import type { CommittedToolCompletion } from "./session-ledger";
115
- import type { PublicToolingConfig } from "../cli/public-config-contract";
105
+ import { TurnCancelledError } from "./turn-cancellation";
116
106
  import type {
117
107
  IterationIdentity,
118
108
  RunAgentResult,
@@ -120,292 +110,35 @@ import type {
120
110
  ToolCallIdentity,
121
111
  TurnIdentity,
122
112
  } from "./types";
123
- import { ContextMeter, type ContextUsageSnapshot } from "./context-meter";
124
- import { contextPressureNoticeText } from "./context-pressure-notice";
125
- import { CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION } from "../context/recall-retirement-contract";
126
- import {
127
- selectContextAutomation,
128
- type ContextAutomationDecision,
129
- } from "../context/context-automation-policy";
130
- import type { SkillCatalogSnapshot } from "../skills/skill-loader";
131
- import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
132
- import {
133
- activeSkillManifestEntry,
134
- createSkillCatalogSnapshot,
135
- skillCatalogManifest,
136
- } from "../skills/skill-catalog";
137
- import {
138
- buildActiveSystemPrompt,
139
- rebindActiveSkills,
140
- renderSkillActivationReceipt,
141
- SkillActivationCoordinator,
142
- } from "../skills/skill-context";
143
-
144
- export type ExecuteTurnInput = {
145
- userMessage: UserMessage;
146
- signal: AbortSignal;
147
- };
148
-
149
- export type AcceptedTurn = {
150
- readonly turnId: TurnIdentity["turnId"];
151
- readonly userMessage: UserMessage;
152
- readonly completion: Promise<RunAgentResult>;
153
- };
154
-
155
- export type PromptSchedulerSnapshot = {
156
- readonly state: "idle" | "running";
157
- readonly activeTurnId?: TurnIdentity["turnId"];
158
- readonly pendingCount: number;
159
- };
160
-
161
- export type QueueFollowUpResult = {
162
- readonly kind: "queued";
163
- readonly pendingCount: number;
164
- readonly activeTurnId?: TurnIdentity["turnId"];
165
- };
166
-
167
- export type SessionDisposeReason =
168
- | { type: "oneshot_complete" }
169
- | { type: "tui_exit" }
170
- | { type: "session_switch" }
171
- | { type: "runner_failed"; error: string }
172
- | { type: "initialization_failed"; error: string };
173
-
174
- export type RuntimeSession = {
175
- readonly sessionId: SessionId;
176
- readonly resumed: boolean;
177
- readonly recovery: SessionRecoveryResult;
178
- skills(): RuntimeSkillsSnapshot;
179
- mcp(): McpInventorySnapshot;
180
- supportsImageInput(): boolean;
181
- reasoningEffort(): ReasoningEffortSnapshot | undefined;
182
- setReasoningEffort(effort: string): ReasoningEffortSnapshot;
183
- resetReasoningEffort(): ReasoningEffortSnapshot;
184
- importImage(
185
- sourcePath: string,
186
- signal: AbortSignal,
187
- prospectiveMessageImageCount: number,
188
- ): Promise<ImportedImageAsset>;
189
- verifyImageAssets(
190
- assets: readonly ImageAssetRef[],
191
- signal: AbortSignal,
192
- ): Promise<void>;
193
- admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn>;
194
- executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
195
- promptScheduler(): PromptSchedulerSnapshot;
196
- subscribePromptScheduler(listener: () => void): () => void;
197
- queueFollowUp(userMessage: UserMessage): QueueFollowUpResult;
198
- compactContext(): Promise<ContextCompactionResult>;
199
- retireContext(): Promise<ContextRetirementResult>;
200
- undoLatestFileMutationTurn(): Promise<TurnUndoResult>;
201
- cloneSession(targetSessionId: SessionId): Promise<void>;
202
- canSwitchSession(): boolean;
203
- bashGuard(): BashGuardSnapshot;
204
- subscribeBashGuard(listener: () => void): () => void;
205
- setYoloMode(enabled: boolean): void;
206
- resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
207
- askUser(): AskUserSnapshot;
208
- subscribeAskUser(listener: () => void): () => void;
209
- resolveAskUser(response: AskUserResolution): Promise<void>;
210
- dispose(reason: SessionDisposeReason): Promise<void>;
211
- };
212
-
213
- export type AskUserSnapshot = {
214
- readonly pending?: AskUserRequest;
215
- };
216
-
217
- export type AskUserResolution =
218
- | { readonly outcome: "selected"; readonly selectedIndex: number }
219
- | { readonly outcome: "dismissed" };
220
-
221
- export type BashGuardSource = "default" | "environment" | "cli" | "session";
113
+ import { projectUserMessage } from "./user-prompt-projection";
222
114
 
223
- export type BashGuardSnapshot = {
224
- readonly mode: "guard" | "yolo";
225
- readonly source: BashGuardSource;
226
- readonly pending?: {
227
- readonly command: string;
228
- readonly reason: string;
229
- };
230
- };
115
+ export {
116
+ RuntimeEventAppendError,
117
+ type AcceptedTurn,
118
+ type AskUserResolution,
119
+ type AskUserSnapshot,
120
+ type BashGuardSnapshot,
121
+ type BashGuardSource,
122
+ type CompletedTurnHook,
123
+ type CompletedTurnHookFailure,
124
+ type CompletedTurnHookInput,
125
+ type ContextSurfaceRefreshSummary,
126
+ type CreateRuntimeSessionInput,
127
+ type ExecuteTurnInput,
128
+ type PromptSchedulerSnapshot,
129
+ type QueueFollowUpResult,
130
+ type RuntimeSession,
131
+ type RuntimeSessionContext,
132
+ type RuntimeSessionFactoryDependencies,
133
+ type RuntimeSkillsSnapshot,
134
+ type SessionDisposeReason,
135
+ type SkillsUpdateSummary,
136
+ } from "./runtime-session-contracts";
231
137
 
232
138
  const EMPTY_MCP_INVENTORY: McpInventorySnapshot = Object.freeze({
233
139
  servers: Object.freeze([]),
234
140
  });
235
141
 
236
- export type RuntimeSkillsSnapshot = {
237
- readonly skills: readonly {
238
- readonly name: string;
239
- readonly description: string;
240
- readonly scope: "project" | "user";
241
- readonly active: boolean;
242
- }[];
243
- readonly shadowedNames: readonly string[];
244
- };
245
-
246
- export type RuntimeSessionContext = {
247
- readonly sessionId: SessionId;
248
- readonly contextMaintenance: ContextMaintenanceHandle;
249
- createIteration(turn: TurnIdentity, iterationNumber: number): IterationIdentity;
250
- createToolCall(
251
- iteration: IterationIdentity,
252
- toolCallNumber: number,
253
- ): ToolCallIdentity;
254
- finishIterationForContinuation(iteration: IterationIdentity): void;
255
- append(input: AgentEventInput): Promise<void>;
256
- updateAssistantTextDelta?(update: AssistantTextDeltaUpdate): void;
257
- onToolCompletionsCommitted?(input: {
258
- completions: readonly ToolCompletionInput[];
259
- committed: readonly CommittedToolCompletion[];
260
- }): void;
261
- prepareModelDispatch?(input: {
262
- iteration: IterationIdentity;
263
- built: BuiltContextRequest;
264
- }): void;
265
- maintainContextAfterIteration?(input: {
266
- turn: TurnIdentity;
267
- consumedThroughOrdinal: number;
268
- ledger: AgentTurnLedger;
269
- }): Promise<void>;
270
- applyQueuedSteering?(input: {
271
- turn: TurnIdentity;
272
- ledger: AgentTurnLedger;
273
- }): Promise<number>;
274
- };
275
-
276
- export type ContextSurfaceRefreshSummary = {
277
- readonly previousRevisionNumber: number;
278
- readonly revisionNumber: number;
279
- readonly changed: readonly ContextSurfaceComponent[];
280
- readonly toolCountBefore: number;
281
- readonly toolCountAfter: number;
282
- };
283
-
284
- export type SkillsUpdateSummary = {
285
- readonly previousRevisionNumber: number;
286
- readonly revisionNumber: number;
287
- readonly activated: readonly string[];
288
- readonly refreshed: readonly string[];
289
- readonly deactivated: readonly string[];
290
- readonly unavailable: readonly string[];
291
- readonly addedOverrideCount: number;
292
- };
293
-
294
- export type CompletedTurnHookInput = {
295
- readonly workspaceRoot: string;
296
- readonly sessionId: SessionId;
297
- readonly turnId: TurnId;
298
- readonly snapshot: CompletedTurnSnapshot;
299
- };
300
-
301
- export type CompletedTurnHookFailure = {
302
- readonly workspaceRoot: string;
303
- readonly sessionId: SessionId;
304
- readonly turnId: TurnId;
305
- readonly reason: "completed_turn_snapshot_failed" | "completed_turn_enqueue_failed";
306
- };
307
-
308
- export type CompletedTurnHook = {
309
- enqueue(input: CompletedTurnHookInput): void;
310
- recordFailure(input: CompletedTurnHookFailure): void;
311
- };
312
-
313
- type CommonRuntimeSessionInput = {
314
- workspaceRoot: string;
315
- homeRoot?: string;
316
- modelName: string;
317
- profileName?: string;
318
- maxIterations: number;
319
- includeReasoningContent: boolean;
320
- contextProfile: ModelContextProfile;
321
- contextBudget: ModelContextBudget;
322
- modelClient: ModelClient;
323
- systemPrompt: string;
324
- projectInstruction?: ProjectInstructionManifest;
325
- skillCatalog?: SkillCatalogSnapshot;
326
- presentationSinks?: EventSink[];
327
- assistantTextDeltaSink?: AssistantTextDeltaSink;
328
- persistence?:
329
- | false
330
- | {
331
- eventLogPath?: string;
332
- observationLogPath?: string;
333
- };
334
- webFetchRefiner?: Refiner;
335
- toolingConfig?: PublicToolingConfig;
336
- memorySearch?: ToolExecutor;
337
- memoryGet?: ToolExecutor;
338
- memoryCreate?: ToolExecutor;
339
- memoryUpdate?: ToolExecutor;
340
- memoryDelete?: ToolExecutor;
341
- completedTurnHook?: CompletedTurnHook;
342
- enableTurnUndo?: boolean;
343
- enableAskUser?: boolean;
344
- bashGuard?: {
345
- readonly mode: "guard" | "yolo";
346
- readonly source: Exclude<BashGuardSource, "session">;
347
- readonly surface: "tui" | "one-shot";
348
- };
349
- };
350
-
351
- type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
352
- selection: { mode: "new"; sessionId: SessionId };
353
- };
354
-
355
- type ResumeRuntimeSessionInput = CommonRuntimeSessionInput & {
356
- selection: { mode: "resume"; sessionId: SessionId };
357
- };
358
-
359
- export type CreateRuntimeSessionInput =
360
- | CreateNewRuntimeSessionInput
361
- | ResumeRuntimeSessionInput;
362
-
363
- export type RuntimeSessionFactoryDependencies = {
364
- idFactory: RuntimeIdFactory;
365
- createTooling: typeof createDefaultTooling;
366
- loadMcpConfig: typeof loadMcpConfig;
367
- createMcpManager: typeof createMcpManager;
368
- createObservationBuilder: () => ObservationBuilder;
369
- openStore: (
370
- input: CreateRuntimeSessionInput,
371
- idFactory: RuntimeIdFactory,
372
- ) => Promise<SessionStore>;
373
- createLedger: (store: SessionStore, idFactory: RuntimeIdFactory) => SessionLedger;
374
- createEventSink: (
375
- input: CreateRuntimeSessionInput,
376
- sessionDirectory: string,
377
- ) => EventSink;
378
- selectShadowPlanning: NonNullable<RunAgentInput["shadowPlanning"]>["select"];
379
- onShadowPlanningResult?: NonNullable<RunAgentInput["shadowPlanning"]>["onResult"];
380
- selectContextAutomation: typeof selectContextAutomation;
381
- automaticCompactionTrigger: () => ContextCompactionTrigger;
382
- automaticRetirementTrigger: () => ContextRetirementTrigger;
383
- manualCompactionTrigger: () => ContextCompactionTrigger;
384
- manualRetirementTrigger: () => ContextRetirementTrigger;
385
- };
386
-
387
- export class RuntimeEventAppendError extends Error {
388
- readonly eventType: AgentEventType;
389
-
390
- constructor(eventType: AgentEventType, options?: ErrorOptions) {
391
- super(`Failed to append runtime event ${eventType}.`, options);
392
- this.name = "RuntimeEventAppendError";
393
- this.eventType = eventType;
394
- }
395
- }
396
-
397
- type RuntimeSessionState =
398
- | "initializing"
399
- | "admitting"
400
- | "ready"
401
- | "executing"
402
- | "compacting"
403
- | "maintaining_context"
404
- | "undoing"
405
- | "faulted"
406
- | "disposing"
407
- | "disposed";
408
-
409
142
  type ActiveTurn = {
410
143
  turn: TurnIdentity;
411
144
  ledger: AgentTurnLedger;
@@ -414,13 +147,6 @@ type ActiveTurn = {
414
147
  completion: Promise<RunAgentResult>;
415
148
  };
416
149
 
417
- type QueuedPrompt = {
418
- readonly userMessage: UserMessage;
419
- };
420
-
421
- const MAX_QUEUED_PROMPTS = 8;
422
- const MAX_QUEUED_PROMPT_TEXT_BYTES = 64 * 1024;
423
-
424
150
  type ActiveAdmission = {
425
151
  controller: AbortController;
426
152
  settled: Promise<void>;
@@ -453,7 +179,7 @@ const defaultDependencies: RuntimeSessionFactoryDependencies = {
453
179
  createEventSink,
454
180
  selectShadowPlanning: ({ preflight }) =>
455
181
  preflight.pressure === "normal" ? undefined : { trigger: "runtime_pressure" },
456
- selectContextAutomation,
182
+ contextAutomationPolicy: DEFAULT_CONTEXT_AUTOMATION_POLICY,
457
183
  automaticCompactionTrigger: () => ({ kind: "runtime_pressure" }),
458
184
  automaticRetirementTrigger: () => ({ kind: "runtime_pressure" }),
459
185
  manualCompactionTrigger: () => ({ kind: "manual" }),
@@ -467,6 +193,7 @@ class DefaultRuntimeSession implements RuntimeSession {
467
193
  syntheticCompletionCount: 0,
468
194
  recallIndexRebuilt: false,
469
195
  };
196
+
470
197
  private state: RuntimeSessionState = "initializing";
471
198
  private nextTurnNumber: number;
472
199
  private readonly turns = new Map<string, TurnIdentity>();
@@ -480,13 +207,6 @@ class DefaultRuntimeSession implements RuntimeSession {
480
207
  private ledger?: SessionLedger;
481
208
  private activeAdmission?: ActiveAdmission;
482
209
  private activeTurn?: ActiveTurn;
483
- private executionChainRunning = false;
484
- private readonly queuedPrompts: QueuedPrompt[] = [];
485
- private promptSchedulerSnapshot: PromptSchedulerSnapshot = Object.freeze({
486
- state: "idle",
487
- pendingCount: 0,
488
- });
489
- private readonly promptSchedulerListeners = new Set<() => void>();
490
210
  private activeContextRevision?: Promise<
491
211
  ContextCompactionResult | ContextRetirementResult
492
212
  >;
@@ -496,38 +216,14 @@ class DefaultRuntimeSession implements RuntimeSession {
496
216
  private readonly committedPrefixAuditor = new CommittedPrefixAuditor();
497
217
  private readonly shadowPlanner: SwapPlanner;
498
218
  private contextManager?: ContextManager;
499
- private contextAutomationDecision?: ContextAutomationDecision;
500
- private pendingAutomaticContextMaintenance = false;
501
- private pendingModelDirectedSwap?: Set<MessageId>;
502
- private modelDirectedSwapLease = false;
503
- private pressureNoticeSentThisTurn = false;
504
- private readonly skillCatalog: SkillCatalogSnapshot;
505
- private skillCoordinator = new SkillActivationCoordinator();
506
- private bashGuardMode: "guard" | "yolo";
507
- private bashGuardSource: BashGuardSource;
508
- private bashGuardSnapshot: BashGuardSnapshot;
509
- private readonly bashGuardListeners = new Set<() => void>();
510
- private askUserSnapshot: AskUserSnapshot = Object.freeze({});
511
- private readonly askUserListeners = new Set<() => void>();
512
- private pendingAskUser?: {
513
- readonly request: AskUserRequest;
514
- readonly startedAt: number;
515
- readonly call: ToolCallIdentity;
516
- readonly resolve: (response: AskUserResponse) => void;
517
- readonly reject: (error: unknown) => void;
518
- readonly removeAbortListener: () => void;
519
- };
219
+ private contextAutomationPolicy?: ContextAutomationPolicy;
520
220
  private assistantTextDeltaSinkDisabled = false;
521
- private pendingBashConfirmation?: {
522
- readonly command: string;
523
- readonly reason: string;
524
- readonly startedAt: number;
525
- readonly call: ToolCallIdentity;
526
- readonly resolve: (decision: "allow" | "deny") => void;
527
- readonly reject: (error: unknown) => void;
528
- readonly removeAbortListener: () => void;
529
- };
530
221
 
222
+ private readonly skillCatalog: SkillCatalogSnapshot;
223
+ private readonly interactions: RuntimeInteractions;
224
+ private readonly scheduler: RuntimePromptScheduler;
225
+ private readonly contextMaintenance: RuntimeContextMaintenance;
226
+ private readonly runtimeSkills: RuntimeSkills;
531
227
  private readonly context: RuntimeSessionContext;
532
228
 
533
229
  private constructor(
@@ -540,12 +236,15 @@ class DefaultRuntimeSession implements RuntimeSession {
540
236
  ) {
541
237
  this.sessionId = input.selection.sessionId;
542
238
  this.resumed = input.selection.mode === "resume";
543
- this.bashGuardMode = input.bashGuard?.mode ?? "guard";
544
- this.bashGuardSource = input.bashGuard?.source ?? "default";
545
- this.bashGuardSnapshot = Object.freeze({
546
- mode: this.bashGuardMode,
547
- source: this.bashGuardSource,
548
- });
239
+ this.scheduler = new RuntimePromptScheduler(
240
+ () => this.state,
241
+ () => this.activeTurn,
242
+ (input) => this.admitSingleTurn(input),
243
+ (event) => this.append(event),
244
+ );
245
+ this.interactions = new RuntimeInteractions(input.bashGuard, (event) =>
246
+ this.append(event),
247
+ );
549
248
  this.skillCatalog =
550
249
  input.skillCatalog ??
551
250
  createSkillCatalogSnapshot({
@@ -559,12 +258,40 @@ class DefaultRuntimeSession implements RuntimeSession {
559
258
  onMeasuredAnchor: (anchor) => store.writeMeasuredContextAnchor(anchor),
560
259
  });
561
260
  this.shadowPlanner = new SwapPlanner(input.modelClient);
261
+ this.contextMaintenance = new RuntimeContextMaintenance(
262
+ this.sessionId,
263
+ store,
264
+ () => this.requireContextManager(),
265
+ () => this.requireContextAutomation(),
266
+ (call, expectedName) => this.requireActiveContextTool(call, expectedName),
267
+ (event) => this.append(event),
268
+ {
269
+ getState: () => this.state,
270
+ setState: (state) => {
271
+ this.state = state;
272
+ },
273
+ hasActiveTurn: () => this.activeTurn !== undefined,
274
+ fault: (error) => this.fault(error),
275
+ },
276
+ dependencies,
277
+ );
278
+ this.runtimeSkills = new RuntimeSkills(
279
+ this.sessionId,
280
+ store,
281
+ input,
282
+ this.skillCatalog,
283
+ dependencies.idFactory,
284
+ this.contextMeter,
285
+ () => this.requireTooling().registry.definitions(),
286
+ (event) => this.append(event),
287
+ );
562
288
  this.context = {
563
289
  sessionId: this.sessionId,
564
290
  contextMaintenance: {
565
- status: (call) => this.contextStatus(call),
566
- candidates: (call, page) => this.contextSwapCandidates(call, page),
567
- swap: (call, selection) => this.contextSwap(call, selection),
291
+ status: (call) => this.contextMaintenance.contextStatus(call),
292
+ candidates: (call, page) =>
293
+ this.contextMaintenance.contextSwapCandidates(call, page),
294
+ swap: (call, selection) => this.contextMaintenance.contextSwap(call, selection),
568
295
  },
569
296
  createIteration: (turn, iterationNumber) =>
570
297
  this.createIteration(turn, iterationNumber),
@@ -580,11 +307,11 @@ class DefaultRuntimeSession implements RuntimeSession {
580
307
  this.updateAssistantTextDelta(update),
581
308
  }),
582
309
  onToolCompletionsCommitted: (completion) =>
583
- this.onToolCompletionsCommitted(completion),
310
+ this.runtimeSkills.onToolCompletionsCommitted(completion),
584
311
  prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
585
312
  maintainContextAfterIteration: (maintenance) =>
586
- this.performActiveTurnContextMaintenance(maintenance),
587
- applyQueuedSteering: (steering) => this.applyQueuedSteering(steering),
313
+ this.contextMaintenance.performActiveTurnContextMaintenance(maintenance),
314
+ applyQueuedSteering: (steering) => this.scheduler.applyQueuedSteering(steering),
588
315
  };
589
316
  }
590
317
 
@@ -669,9 +396,9 @@ class DefaultRuntimeSession implements RuntimeSession {
669
396
  active: storedSurface.activeSkills,
670
397
  promotionNames,
671
398
  });
672
- session.skillCoordinator = new SkillActivationCoordinator({
673
- active: rebound.active,
674
- });
399
+ session.runtimeSkills.restoreCoordinator(
400
+ new SkillActivationCoordinator({ active: rebound.active }),
401
+ );
675
402
  const activated = promotionNames
676
403
  .filter((entry) => session.skillCatalog.skills.has(entry.name))
677
404
  .map((entry) => entry.name)
@@ -711,7 +438,7 @@ class DefaultRuntimeSession implements RuntimeSession {
711
438
  }
712
439
 
713
440
  session.tooling = dependencies.createTooling({
714
- workspaceRoot: input.workspaceRoot,
441
+ workspaceRoot: store.workspaceRoot,
715
442
  ...(input.homeRoot === undefined ? {} : { homeRoot: input.homeRoot }),
716
443
  runtimeSession: session.context,
717
444
  historyReader: store.historyReader(),
@@ -728,13 +455,13 @@ class DefaultRuntimeSession implements RuntimeSession {
728
455
  call: ToolCallIdentity,
729
456
  request: AskUserRequest,
730
457
  signal: AbortSignal,
731
- ) => session.requestUserAnswer(call, request, signal),
458
+ ) => session.interactions.requestUserAnswer(call, request, signal),
732
459
  }
733
460
  : {}),
734
461
  bashGuard: {
735
462
  surface: input.bashGuard?.surface ?? "one-shot",
736
463
  confirm: (call, request, signal) =>
737
- session.confirmBashCommand(call, request, signal),
464
+ session.interactions.confirmBashCommand(call, request, signal),
738
465
  },
739
466
  ...(input.memorySearch === undefined
740
467
  ? {}
@@ -753,7 +480,7 @@ class DefaultRuntimeSession implements RuntimeSession {
753
480
  ? {}
754
481
  : {
755
482
  skillCatalog: session.skillCatalog,
756
- skillCoordinator: session.skillCoordinator,
483
+ skillCoordinator: session.runtimeSkills.coordinator,
757
484
  }),
758
485
  });
759
486
 
@@ -774,7 +501,7 @@ class DefaultRuntimeSession implements RuntimeSession {
774
501
  const definitions = session.requireTooling().registry.definitions();
775
502
  const activeSystemPrompt = buildActiveSystemPrompt({
776
503
  baseSystemPrompt: input.systemPrompt,
777
- activeSkills: session.skillCoordinator.activeEntries(),
504
+ activeSkills: session.runtimeSkills.coordinator.activeEntries(),
778
505
  });
779
506
  const surfacePrepared = input.modelClient.prepare({
780
507
  messages: [{ role: "system", content: activeSystemPrompt }],
@@ -789,7 +516,7 @@ class DefaultRuntimeSession implements RuntimeSession {
789
516
  ? {}
790
517
  : { projectInstruction: input.projectInstruction }),
791
518
  skillCatalog: skillCatalogManifest(session.skillCatalog.skills.values()),
792
- activeSkills: session.skillCoordinator.activeManifest(),
519
+ activeSkills: session.runtimeSkills.coordinator.activeManifest(),
793
520
  toolDefinitions: definitions,
794
521
  prepared: surfacePrepared,
795
522
  createdAt: new Date().toISOString(),
@@ -807,10 +534,10 @@ class DefaultRuntimeSession implements RuntimeSession {
807
534
  let skillsUpdate: SkillsUpdateSummary | undefined;
808
535
  const refresh =
809
536
  resumeSkills.unresolved.length === 0
810
- ? await session.refreshContextSurface(candidateSurface)
537
+ ? await session.runtimeSkills.refreshContextSurface(candidateSurface)
811
538
  : undefined;
812
539
  if (resumeSkills.unresolved.length > 0) {
813
- skillsUpdate = await session.commitSkillSettlements({
540
+ skillsUpdate = await session.runtimeSkills.commitSkillSettlements({
814
541
  reason: "resume",
815
542
  candidateSurface,
816
543
  unresolved: resumeSkills.unresolved,
@@ -884,8 +611,12 @@ class DefaultRuntimeSession implements RuntimeSession {
884
611
  }
885
612
  }
886
613
 
887
- await session.appendSkillsCatalogLoaded();
614
+ await session.runtimeSkills.appendSkillsCatalogLoaded();
888
615
 
616
+ assertContextMaintenanceCapabilities(
617
+ session.requireTooling().registry,
618
+ store.loadContextSnapshot().surface.recallContractVersion,
619
+ );
889
620
  session.ledger = dependencies.createLedger(store, dependencies.idFactory);
890
621
  session.contextManager = new ContextManager({
891
622
  store,
@@ -927,18 +658,17 @@ class DefaultRuntimeSession implements RuntimeSession {
927
658
  data: { phase: "initial", snapshot: initialSnapshot },
928
659
  });
929
660
 
930
- session.contextAutomationDecision = dependencies.selectContextAutomation({
931
- ...(input.profileName === undefined ? {} : { profileName: input.profileName }),
932
- surface: store.loadContextSnapshot().surface,
661
+ session.contextAutomationPolicy = Object.freeze({
662
+ ...dependencies.contextAutomationPolicy,
933
663
  });
934
664
  if (
935
665
  input.selection.mode === "resume" &&
936
666
  initialSnapshot.pressure !== "normal" &&
937
- session.contextAutomationDecision.automaticSwapOnly
667
+ session.contextAutomationPolicy.automaticSwap
938
668
  ) {
939
- session.pendingAutomaticContextMaintenance = true;
669
+ session.contextMaintenance.scheduleAutomaticMaintenance();
940
670
  session.state = "executing";
941
- await session.performAutomaticContextMaintenance();
671
+ await session.contextMaintenance.performAutomaticContextMaintenance();
942
672
  }
943
673
 
944
674
  session.state = "ready";
@@ -948,139 +678,36 @@ class DefaultRuntimeSession implements RuntimeSession {
948
678
  }
949
679
  }
950
680
 
951
- private async refreshContextSurface(
952
- candidateSurface: StoredContextSurfaceV8,
953
- ): Promise<ContextSurfaceRefreshSummary | undefined> {
954
- const snapshot = this.store.loadContextSnapshot();
955
- if (sameContextSurface(snapshot.surface, candidateSurface)) {
956
- return undefined;
957
- }
681
+ bashGuard(): BashGuardSnapshot {
682
+ return this.interactions.bashGuard();
683
+ }
958
684
 
959
- const changes = contextSurfaceChanges(snapshot.surface, candidateSurface);
960
- const changed = changedContextSurfaceComponents(changes);
961
- if (changed.length === 0) {
962
- throw new Error("Changed context surface has an empty change manifest.");
963
- }
964
- const startedAt = performance.now();
965
- await this.append({
966
- type: "context.revision.started",
967
- sessionId: this.sessionId,
968
- data: {
969
- strategy: "surface_refresh",
970
- reason: "resume",
971
- baseRevisionNumber: snapshot.revision.revisionNumber,
972
- changed,
973
- },
974
- });
685
+ subscribeBashGuard(listener: () => void): () => void {
686
+ return this.interactions.subscribeBashGuard(listener);
687
+ }
975
688
 
976
- let stage: "prepare" | "commit" | "activate" = "prepare";
977
- let committed = false;
978
- try {
979
- const compiler = new ContextRevisionCompiler();
980
- const active = compiler.compileActive(snapshot);
981
- const candidateCompiled = compiler.compileProspective({
982
- active,
983
- canonical: snapshot.canonical,
984
- activeOverrides: snapshot.activeOverrides,
985
- addedOverrides: [],
986
- activeSurface: snapshot.surface,
987
- surface: candidateSurface,
988
- });
989
- const prepared = this.input.modelClient.prepare({
990
- messages: candidateCompiled.entries.map((entry) => entry.message),
991
- tools: [...candidateSurface.toolDefinitions],
992
- });
993
- assertPreparedMatchesSurface(prepared, candidateSurface);
994
-
995
- stage = "commit";
996
- const revision = this.store.commitSurfaceRefresh({
997
- revisionId: this.dependencies.idFactory.createContextRevisionId(),
998
- expectedBaseRevisionId: snapshot.revision.revisionId,
999
- expectedBaseRevisionNumber: snapshot.revision.revisionNumber,
1000
- expectedCanonicalThroughOrdinal: snapshot.canonical.messages.length,
1001
- expectedBaseActiveOverrideManifestSha256:
1002
- snapshot.revision.activeOverrideManifestSha256,
1003
- surface: candidateSurface,
1004
- changes,
1005
- changeManifestSha256: contextSurfaceChangeManifestHash(changes),
1006
- canonicalSequenceSha256: canonicalSequenceHash(snapshot.canonical),
1007
- renderedMessageSha256: renderedMessageHash(candidateCompiled.entries),
1008
- });
1009
- committed = true;
689
+ setYoloMode(enabled: boolean): void {
690
+ return this.interactions.setYoloMode(enabled);
691
+ }
1010
692
 
1011
- stage = "activate";
1012
- this.contextMeter.startRevision({
1013
- reason: "context_rebuilt",
1014
- requestConfigHash: prepared.requestConfigHash,
1015
- toolSchemaHash: prepared.toolSchemaHash,
1016
- });
1017
- const summary = Object.freeze({
1018
- previousRevisionNumber: snapshot.revision.revisionNumber,
1019
- revisionNumber: revision.revisionNumber,
1020
- changed,
1021
- toolCountBefore: snapshot.surface.toolDefinitions.length,
1022
- toolCountAfter: candidateSurface.toolDefinitions.length,
1023
- });
1024
- await this.append({
1025
- type: "context.revision.finished",
1026
- sessionId: this.sessionId,
1027
- data: {
1028
- strategy: "surface_refresh",
1029
- reason: "resume",
1030
- baseRevisionNumber: summary.previousRevisionNumber,
1031
- revisionNumber: summary.revisionNumber,
1032
- changed: summary.changed,
1033
- toolCountBefore: summary.toolCountBefore,
1034
- toolCountAfter: summary.toolCountAfter,
1035
- measuredAnchorCleared: true,
1036
- durationMs: elapsedMs(startedAt),
1037
- },
1038
- });
1039
- return summary;
1040
- } catch (error) {
1041
- await this.append({
1042
- type: "context.revision.failed",
1043
- sessionId: this.sessionId,
1044
- data: {
1045
- strategy: "surface_refresh",
1046
- reason: "resume",
1047
- stage,
1048
- errorCode: boundedContextErrorCode(
1049
- error instanceof SessionError
1050
- ? error.code
1051
- : error instanceof Error
1052
- ? error.name
1053
- : "CONTEXT_SURFACE_REFRESH_FAILED",
1054
- ),
1055
- error: `Context surface refresh failed at ${stage}.`,
1056
- committed,
1057
- },
1058
- }).catch(() => undefined);
1059
- throw error;
1060
- }
693
+ resolveBashConfirmation(decision: "allow" | "deny"): Promise<void> {
694
+ return this.interactions.resolveBashConfirmation(decision);
695
+ }
696
+
697
+ askUser(): AskUserSnapshot {
698
+ return this.interactions.askUser();
699
+ }
700
+
701
+ subscribeAskUser(listener: () => void): () => void {
702
+ return this.interactions.subscribeAskUser(listener);
703
+ }
704
+
705
+ resolveAskUser(response: AskUserResolution): Promise<void> {
706
+ return this.interactions.resolveAskUser(response);
1061
707
  }
1062
708
 
1063
709
  skills(): RuntimeSkillsSnapshot {
1064
- const activeNames = new Set(
1065
- this.skillCoordinator.activeEntries().map((entry) => entry.skill.name),
1066
- );
1067
- return Object.freeze({
1068
- skills: Object.freeze(
1069
- [...this.skillCatalog.skills.values()]
1070
- .sort((left, right) => compareText(left.name, right.name))
1071
- .map((skill) =>
1072
- Object.freeze({
1073
- name: skill.name,
1074
- description: skill.description,
1075
- scope: skill.scope,
1076
- active: activeNames.has(skill.name),
1077
- }),
1078
- ),
1079
- ),
1080
- shadowedNames: Object.freeze(
1081
- this.skillCatalog.shadowed.map((entry) => entry.name),
1082
- ),
1083
- });
710
+ return this.runtimeSkills.skills();
1084
711
  }
1085
712
 
1086
713
  mcp(): McpInventorySnapshot {
@@ -1121,261 +748,20 @@ class DefaultRuntimeSession implements RuntimeSession {
1121
748
  return reasoningEffort.reset();
1122
749
  }
1123
750
 
1124
- bashGuard(): BashGuardSnapshot {
1125
- return this.bashGuardSnapshot;
1126
- }
1127
-
1128
- private refreshBashGuardSnapshot(): void {
1129
- this.bashGuardSnapshot = Object.freeze({
1130
- mode: this.bashGuardMode,
1131
- source: this.bashGuardSource,
1132
- ...(this.pendingBashConfirmation === undefined
1133
- ? {}
1134
- : {
1135
- pending: Object.freeze({
1136
- command: this.pendingBashConfirmation.command,
1137
- reason: this.pendingBashConfirmation.reason,
1138
- }),
1139
- }),
1140
- });
1141
- }
1142
-
1143
- subscribeBashGuard(listener: () => void): () => void {
1144
- this.bashGuardListeners.add(listener);
1145
- return () => this.bashGuardListeners.delete(listener);
1146
- }
1147
-
1148
- setYoloMode(enabled: boolean): void {
1149
- this.bashGuardMode = enabled ? "yolo" : "guard";
1150
- this.bashGuardSource = "session";
1151
- this.refreshBashGuardSnapshot();
1152
- this.notifyBashGuardListeners();
1153
- }
1154
-
1155
- async resolveBashConfirmation(decision: "allow" | "deny"): Promise<void> {
1156
- const pending = this.pendingBashConfirmation;
1157
- if (pending === undefined) {
1158
- throw new Error("No Bash confirmation is pending.");
1159
- }
1160
- this.pendingBashConfirmation = undefined;
1161
- this.refreshBashGuardSnapshot();
1162
- pending.removeAbortListener();
1163
- await this.append({
1164
- type: "tool.confirmation.resolved",
1165
- ...pending.call,
1166
- data: {
1167
- command: pending.command,
1168
- reason: pending.reason,
1169
- decision,
1170
- durationMs: Date.now() - pending.startedAt,
1171
- },
1172
- });
1173
- pending.resolve(decision);
1174
- this.notifyBashGuardListeners();
1175
- }
1176
-
1177
- private async confirmBashCommand(
1178
- call: ToolCallIdentity,
1179
- request: { command: string; reason: string },
751
+ async importImage(
752
+ sourcePath: string,
1180
753
  signal: AbortSignal,
1181
- ): Promise<"allow" | "deny"> {
1182
- const startedAt = Date.now();
1183
- await this.append({
1184
- type: "tool.confirmation.requested",
1185
- ...call,
1186
- data: request,
1187
- });
1188
-
1189
- const surface = this.input.bashGuard?.surface ?? "one-shot";
1190
- if (this.bashGuardMode === "yolo" || surface === "one-shot") {
1191
- const decision = this.bashGuardMode === "yolo" ? "allow" : "deny";
1192
- await this.append({
1193
- type: "tool.confirmation.resolved",
1194
- ...call,
1195
- data: {
1196
- ...request,
1197
- decision,
1198
- durationMs: Date.now() - startedAt,
1199
- },
1200
- });
1201
- return decision;
754
+ prospectiveMessageImageCount: number,
755
+ ): Promise<ImportedImageAsset> {
756
+ if (this.state !== "ready") {
757
+ throw new Error(`Cannot import an image while RuntimeSession is ${this.state}.`);
1202
758
  }
1203
-
1204
- if (this.pendingBashConfirmation !== undefined) {
1205
- throw new Error("Another Bash confirmation is already pending.");
1206
- }
1207
-
1208
- return new Promise<"allow" | "deny">((resolve, reject) => {
1209
- const onAbort = () => {
1210
- const pending = this.pendingBashConfirmation;
1211
- if (pending?.call.toolCallId !== call.toolCallId) {
1212
- return;
1213
- }
1214
- this.pendingBashConfirmation = undefined;
1215
- this.refreshBashGuardSnapshot();
1216
- void this.append({
1217
- type: "tool.confirmation.resolved",
1218
- ...call,
1219
- data: {
1220
- ...request,
1221
- decision: "cancelled",
1222
- durationMs: Date.now() - startedAt,
1223
- },
1224
- }).finally(() => {
1225
- reject(cancellationError(signal));
1226
- this.notifyBashGuardListeners();
1227
- });
1228
- };
1229
- signal.addEventListener("abort", onAbort, { once: true });
1230
- this.pendingBashConfirmation = {
1231
- ...request,
1232
- startedAt,
1233
- call,
1234
- resolve,
1235
- reject,
1236
- removeAbortListener: () => signal.removeEventListener("abort", onAbort),
1237
- };
1238
- this.refreshBashGuardSnapshot();
1239
- this.notifyBashGuardListeners();
1240
- if (signal.aborted) {
1241
- onAbort();
1242
- }
1243
- });
1244
- }
1245
-
1246
- private notifyBashGuardListeners(): void {
1247
- for (const listener of this.bashGuardListeners) {
1248
- listener();
1249
- }
1250
- }
1251
-
1252
- askUser(): AskUserSnapshot {
1253
- return this.askUserSnapshot;
1254
- }
1255
-
1256
- subscribeAskUser(listener: () => void): () => void {
1257
- this.askUserListeners.add(listener);
1258
- return () => this.askUserListeners.delete(listener);
1259
- }
1260
-
1261
- async resolveAskUser(response: AskUserResolution): Promise<void> {
1262
- const pending = this.pendingAskUser;
1263
- if (pending === undefined) {
1264
- throw new Error("No AskUser question is pending.");
1265
- }
1266
- let result: AskUserResponse;
1267
- if (response.outcome === "selected") {
1268
- if (!Number.isSafeInteger(response.selectedIndex)) {
1269
- throw new Error("AskUser selectedIndex must be an integer.");
1270
- }
1271
- const option = pending.request.options[response.selectedIndex];
1272
- if (option === undefined) {
1273
- throw new Error("AskUser selectedIndex is out of range.");
1274
- }
1275
- result = { outcome: "selected", answer: option.description };
1276
- } else {
1277
- result = { outcome: "dismissed" };
1278
- }
1279
- this.pendingAskUser = undefined;
1280
- this.askUserSnapshot = Object.freeze({});
1281
- pending.removeAbortListener();
1282
- await this.append({
1283
- type: "tool.user_question.resolved",
1284
- ...pending.call,
1285
- data: {
1286
- ...result,
1287
- durationMs: Date.now() - pending.startedAt,
1288
- },
1289
- });
1290
- pending.resolve(result);
1291
- this.notifyAskUserListeners();
1292
- }
1293
-
1294
- private async requestUserAnswer(
1295
- call: ToolCallIdentity,
1296
- request: AskUserRequest,
1297
- signal: AbortSignal,
1298
- ): Promise<AskUserResponse> {
1299
- if (this.pendingAskUser !== undefined) {
1300
- throw new Error("Another AskUser question is already pending.");
1301
- }
1302
- if (this.pendingBashConfirmation !== undefined) {
1303
- throw new Error("Cannot ask the user while a Bash confirmation is pending.");
1304
- }
1305
- if (signal.aborted) {
1306
- throw cancellationError(signal);
1307
- }
1308
- const startedAt = Date.now();
1309
- await this.append({
1310
- type: "tool.user_question.requested",
1311
- ...call,
1312
- data: request,
1313
- });
1314
- return new Promise<AskUserResponse>((resolve, reject) => {
1315
- const onAbort = () => {
1316
- const pending = this.pendingAskUser;
1317
- if (pending?.call.toolCallId !== call.toolCallId) {
1318
- return;
1319
- }
1320
- this.pendingAskUser = undefined;
1321
- this.askUserSnapshot = Object.freeze({});
1322
- void this.append({
1323
- type: "tool.user_question.resolved",
1324
- ...call,
1325
- data: {
1326
- outcome: "cancelled",
1327
- durationMs: Date.now() - startedAt,
1328
- },
1329
- }).finally(() => {
1330
- reject(cancellationError(signal));
1331
- this.notifyAskUserListeners();
1332
- });
1333
- };
1334
- signal.addEventListener("abort", onAbort, { once: true });
1335
- const immutableRequest = Object.freeze({
1336
- question: request.question,
1337
- options: Object.freeze(
1338
- request.options.map((option) =>
1339
- Object.freeze({ description: option.description }),
1340
- ),
1341
- ),
1342
- });
1343
- this.pendingAskUser = {
1344
- request: immutableRequest,
1345
- startedAt,
1346
- call,
1347
- resolve,
1348
- reject,
1349
- removeAbortListener: () => signal.removeEventListener("abort", onAbort),
1350
- };
1351
- this.askUserSnapshot = Object.freeze({ pending: immutableRequest });
1352
- this.notifyAskUserListeners();
1353
- if (signal.aborted) {
1354
- onAbort();
1355
- }
1356
- });
1357
- }
1358
-
1359
- private notifyAskUserListeners(): void {
1360
- for (const listener of this.askUserListeners) {
1361
- listener();
1362
- }
1363
- }
1364
-
1365
- async importImage(
1366
- sourcePath: string,
1367
- signal: AbortSignal,
1368
- prospectiveMessageImageCount: number,
1369
- ): Promise<ImportedImageAsset> {
1370
- if (this.state !== "ready") {
1371
- throw new Error(`Cannot import an image while RuntimeSession is ${this.state}.`);
1372
- }
1373
- if (
1374
- !Number.isSafeInteger(prospectiveMessageImageCount) ||
1375
- prospectiveMessageImageCount < 1 ||
1376
- prospectiveMessageImageCount > IMAGE_INPUT_POLICY.maxImagesPerMessage
1377
- ) {
1378
- throw new Error("Prospective Prompt image count is invalid.");
759
+ if (
760
+ !Number.isSafeInteger(prospectiveMessageImageCount) ||
761
+ prospectiveMessageImageCount < 1 ||
762
+ prospectiveMessageImageCount > IMAGE_INPUT_POLICY.maxImagesPerMessage
763
+ ) {
764
+ throw new Error("Prospective Prompt image count is invalid.");
1379
765
  }
1380
766
  const assertImageAllowed = () => {
1381
767
  if (this.state !== "ready") {
@@ -1421,108 +807,11 @@ class DefaultRuntimeSession implements RuntimeSession {
1421
807
  }
1422
808
  }
1423
809
 
1424
- private requireContextAutomation(): ContextAutomationDecision {
1425
- if (this.contextAutomationDecision === undefined) {
810
+ private requireContextAutomation(): ContextAutomationPolicy {
811
+ if (this.contextAutomationPolicy === undefined) {
1426
812
  throw new Error("RuntimeSession context automation is not initialized.");
1427
813
  }
1428
- return this.contextAutomationDecision;
1429
- }
1430
-
1431
- private async contextStatus(call: ToolCall): Promise<ContextStatusRawResult> {
1432
- const active = this.requireActiveContextTool(call, "ContextStatus");
1433
- try {
1434
- const usage = this.requireContextManager().measureActive(
1435
- active.turn.turnId,
1436
- active.ledger,
1437
- );
1438
- return Object.freeze({
1439
- ok: true,
1440
- operation: "status",
1441
- usedInputTokens: usage.usedInputTokens,
1442
- inputBudgetTokens: usage.inputBudgetTokens,
1443
- pressure: toolContextPressure(usage.pressure),
1444
- triggerTokens: usage.triggerTokens,
1445
- source: usage.source,
1446
- });
1447
- } catch (error) {
1448
- return {
1449
- ok: false,
1450
- operation: "status",
1451
- error: this.contextToolFailure("status", error),
1452
- };
1453
- }
1454
- }
1455
-
1456
- private async contextSwapCandidates(
1457
- call: ToolCall,
1458
- page: { readonly limit: number; readonly offset: number },
1459
- ): Promise<ContextSwapCandidatesRawResult> {
1460
- const active = this.requireActiveContextTool(call, "ContextSwapCandidates");
1461
- try {
1462
- const result = this.requireContextManager().listActiveSwapCandidates({
1463
- turnId: active.turn.turnId,
1464
- consumedThroughOrdinal: active.consumedThroughOrdinal,
1465
- activeLedger: active.ledger,
1466
- limit: page.limit,
1467
- offset: page.offset,
1468
- });
1469
- if (result.total > 0 && result.usage.pressure !== "normal") {
1470
- this.modelDirectedSwapLease = true;
1471
- }
1472
- return Object.freeze({
1473
- ok: true,
1474
- operation: "candidates",
1475
- total: result.total,
1476
- candidates: result.candidates,
1477
- });
1478
- } catch (error) {
1479
- return {
1480
- ok: false,
1481
- operation: "candidates",
1482
- error: this.contextToolFailure("candidate listing", error),
1483
- };
1484
- }
1485
- }
1486
-
1487
- private async contextSwap(
1488
- call: ToolCall,
1489
- selection: { readonly candidateIds: readonly MessageId[] },
1490
- ): Promise<ContextSwapRawResult> {
1491
- const active = this.requireActiveContextTool(call, "ContextSwap");
1492
- try {
1493
- const result = this.requireContextManager().validateActiveSwapSelection({
1494
- turnId: active.turn.turnId,
1495
- consumedThroughOrdinal: active.consumedThroughOrdinal,
1496
- activeLedger: active.ledger,
1497
- messageIds: selection.candidateIds,
1498
- });
1499
- if (result.scheduled.length === 0) {
1500
- return Object.freeze({
1501
- ok: false,
1502
- operation: "swap",
1503
- scheduled: Object.freeze([]),
1504
- rejected: result.rejected,
1505
- });
1506
- }
1507
- const pending = (this.pendingModelDirectedSwap ??= new Set<MessageId>());
1508
- for (const candidate of result.scheduled) pending.add(candidate.candidateId);
1509
- this.modelDirectedSwapLease = false;
1510
- return Object.freeze({
1511
- ok: true,
1512
- operation: "swap",
1513
- scheduled: result.scheduled,
1514
- rejected: result.rejected,
1515
- note: "Swap executes when this iteration's tool frames close.",
1516
- });
1517
- } catch (error) {
1518
- return {
1519
- ok: false,
1520
- operation: "swap",
1521
- scheduled: [],
1522
- rejected: [],
1523
- error: this.contextToolFailure("swap scheduling", error),
1524
- };
1525
- }
814
+ return this.contextAutomationPolicy;
1526
815
  }
1527
816
 
1528
817
  private requireActiveContextTool(
@@ -1545,71 +834,6 @@ class DefaultRuntimeSession implements RuntimeSession {
1545
834
  return active as ActiveTurn & { consumedThroughOrdinal: number };
1546
835
  }
1547
836
 
1548
- private contextToolFailure(operation: string, error: unknown): string {
1549
- if (error instanceof ContextManagerError && !error.fatal) {
1550
- return `Context ${operation} failed (${boundedContextErrorCode(error.code)}).`;
1551
- }
1552
- throw new ToolExecutionFatalError(
1553
- `Context ${operation} required canonical session state that could not be read safely.`,
1554
- { cause: error },
1555
- );
1556
- }
1557
-
1558
- private appendSkillsCatalogLoaded(): Promise<void> {
1559
- const activeNames = this.skillCoordinator
1560
- .activeEntries()
1561
- .map((entry) => entry.skill.name);
1562
- if (
1563
- this.skillCatalog.skills.size === 0 &&
1564
- activeNames.length === 0 &&
1565
- this.skillCatalog.shadowed.length === 0
1566
- ) {
1567
- return Promise.resolve();
1568
- }
1569
- const skills = [...this.skillCatalog.skills.values()];
1570
- return this.append({
1571
- type: "skills.catalog.loaded",
1572
- sessionId: this.sessionId,
1573
- data: {
1574
- availableCount: skills.length,
1575
- projectCount: skills.filter((skill) => skill.scope === "project").length,
1576
- userCount: skills.filter((skill) => skill.scope === "user").length,
1577
- activeNames: Object.freeze(activeNames),
1578
- shadowedNames: Object.freeze(
1579
- this.skillCatalog.shadowed.map((entry) => entry.name),
1580
- ),
1581
- },
1582
- });
1583
- }
1584
-
1585
- private onToolCompletionsCommitted(input: {
1586
- completions: readonly ToolCompletionInput[];
1587
- committed: readonly CommittedToolCompletion[];
1588
- }): void {
1589
- if (input.completions.length !== input.committed.length) {
1590
- throw new Error("Committed tool completion identity count does not match.");
1591
- }
1592
- for (let index = 0; index < input.completions.length; index += 1) {
1593
- const completion = input.completions[index];
1594
- const committed = input.committed[index];
1595
- if (
1596
- completion === undefined ||
1597
- committed === undefined ||
1598
- completion.call.toolCallId !== committed.toolCallId
1599
- ) {
1600
- throw new Error("Committed tool completion identity is invalid.");
1601
- }
1602
- if (
1603
- completion.kind === "returned" &&
1604
- completion.raw.kind === "skill" &&
1605
- completion.raw.ok &&
1606
- completion.raw.status === "loaded"
1607
- ) {
1608
- this.skillCoordinator.markPending(completion.raw.name);
1609
- }
1610
- }
1611
- }
1612
-
1613
837
  private prepareModelDispatch(input: {
1614
838
  iteration: IterationIdentity;
1615
839
  built: BuiltContextRequest;
@@ -1623,317 +847,27 @@ class DefaultRuntimeSession implements RuntimeSession {
1623
847
  throw new Error("Model dispatch does not belong to the active runtime turn.");
1624
848
  }
1625
849
  active.consumedThroughOrdinal = input.built.canonical.messages.length;
1626
- const pending = this.store.loadSkillActivations(["pending"]);
1627
- if (pending.length === 0) {
1628
- return;
1629
- }
1630
- const visibleCanonicalMessageIds = new Set(
1631
- input.built.compiled.entries
1632
- .filter(
1633
- (entry) =>
1634
- entry.representation === "canonical" && entry.message.role === "tool",
1635
- )
1636
- .map((entry) => entry.messageId),
1637
- );
1638
- const included = pending.filter((activation) =>
1639
- visibleCanonicalMessageIds.has(activation.activationMessageId),
1640
- );
1641
- if (included.length === 0) {
1642
- return;
1643
- }
1644
- const dispatched = this.store.markSkillActivationsDispatched({
1645
- iterationId: input.iteration.iterationId,
1646
- activationMessageIds: included.map(
1647
- (activation) => activation.activationMessageId,
1648
- ),
1649
- });
1650
- this.skillCoordinator.markDispatched(
1651
- dispatched.map((activation) => activation.name),
1652
- );
1653
- }
1654
-
1655
- private async commitSkillSettlements(input: {
1656
- reason: "activation" | "resume";
1657
- unresolved: readonly StoredSkillActivation[];
1658
- candidateSurface?: StoredContextSurfaceV8;
1659
- activated?: readonly string[];
1660
- refreshed?: readonly string[];
1661
- deactivated?: readonly string[];
1662
- }): Promise<SkillsUpdateSummary> {
1663
- if (input.unresolved.length === 0) {
1664
- throw new Error("Agent Skills update requires unresolved activations.");
1665
- }
1666
- const snapshot = this.store.loadContextSnapshot();
1667
- const canonicalMessages = new Map(
1668
- snapshot.canonical.messages.map((message) => [message.messageId, message]),
1669
- );
1670
- const activeByName = new Map(
1671
- this.skillCoordinator
1672
- .activeEntries()
1673
- .map((entry) => [entry.skill.name, entry] as const),
1674
- );
1675
- const activated = new Set(input.activated ?? []);
1676
- const unavailable = new Set<string>();
1677
- const settlements: Array<{
1678
- activationMessageId: StoredSkillActivation["activationMessageId"];
1679
- name: string;
1680
- state: "promoted" | "rejected";
1681
- rejectionReason?: string;
1682
- }> = [];
1683
- const receipts = [];
1684
- for (const activation of [...input.unresolved].sort((left, right) =>
1685
- compareText(left.name, right.name),
1686
- )) {
1687
- const skill = this.skillCatalog.skills.get(activation.name);
1688
- const canPromote = activation.state === "dispatched" && skill !== undefined;
1689
- if (canPromote) {
1690
- const existing = activeByName.get(activation.name);
1691
- if (
1692
- existing !== undefined &&
1693
- existing.activationMessageId !== activation.activationMessageId
1694
- ) {
1695
- throw new Error(
1696
- `Agent Skill ${activation.name} already has another active activation.`,
1697
- );
1698
- }
1699
- activeByName.set(activation.name, {
1700
- skill,
1701
- activationMessageId: activation.activationMessageId,
1702
- });
1703
- activated.add(activation.name);
1704
- }
1705
- const state = canPromote ? "promoted" : "rejected";
1706
- const rejectionReason =
1707
- state === "promoted"
1708
- ? undefined
1709
- : activation.state === "pending"
1710
- ? "not_dispatched"
1711
- : "unavailable";
1712
- if (rejectionReason === "unavailable") {
1713
- unavailable.add(activation.name);
1714
- }
1715
- settlements.push({
1716
- activationMessageId: activation.activationMessageId,
1717
- name: activation.name,
1718
- state,
1719
- ...(rejectionReason === undefined ? {} : { rejectionReason }),
1720
- });
1721
- const message = canonicalMessages.get(activation.activationMessageId);
1722
- if (message?.role !== "tool") {
1723
- throw new Error(
1724
- `Agent Skill activation message ${activation.activationMessageId} is missing.`,
1725
- );
1726
- }
1727
- receipts.push(
1728
- renderSkillActivationReceipt({
1729
- message: {
1730
- messageId: message.messageId,
1731
- frameId: message.frameId,
1732
- ordinal: message.ordinal,
1733
- content: message.displayText,
1734
- contentSha256: message.contentSha256,
1735
- },
1736
- name: activation.name,
1737
- outcome:
1738
- state === "promoted"
1739
- ? "promoted"
1740
- : rejectionReason === "unavailable"
1741
- ? "unavailable"
1742
- : "rejected",
1743
- }),
1744
- );
1745
- }
1746
- const nextActive = Object.freeze(
1747
- [...activeByName.values()].sort((left, right) =>
1748
- compareText(left.skill.name, right.skill.name),
1749
- ),
1750
- );
1751
- const createdAt = new Date().toISOString();
1752
- const definitions = this.requireTooling().registry.definitions();
1753
- const renderedSystemPrompt = buildActiveSystemPrompt({
1754
- baseSystemPrompt: this.input.systemPrompt,
1755
- activeSkills: nextActive,
1756
- });
1757
- const surfacePrepared = this.input.modelClient.prepare({
1758
- messages: [{ role: "system", content: renderedSystemPrompt }],
1759
- tools: definitions,
1760
- });
1761
- const generatedSurface =
1762
- input.candidateSurface ??
1763
- createContextSurface({
1764
- surfaceId: this.dependencies.idFactory.createContextSurfaceId(),
1765
- sessionId: this.sessionId,
1766
- systemPrompt: renderedSystemPrompt,
1767
- recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
1768
- ...(this.input.projectInstruction === undefined
1769
- ? {}
1770
- : { projectInstruction: this.input.projectInstruction }),
1771
- skillCatalog: skillCatalogManifest(this.skillCatalog.skills.values()),
1772
- activeSkills: nextActive.map((entry) =>
1773
- activeSkillManifestEntry(entry.skill, entry.activationMessageId),
1774
- ),
1775
- toolDefinitions: definitions,
1776
- prepared: surfacePrepared,
1777
- createdAt,
1778
- });
1779
- assertPreparedMatchesSurface(surfacePrepared, generatedSurface);
1780
- const surface = sameContextSurface(snapshot.surface, generatedSurface)
1781
- ? snapshot.surface
1782
- : generatedSurface;
1783
- const startedAt = performance.now();
1784
- await this.append({
1785
- type: "context.revision.started",
1786
- sessionId: this.sessionId,
1787
- data: {
1788
- strategy: "skills_update",
1789
- reason: input.reason,
1790
- baseRevisionNumber: snapshot.revision.revisionNumber,
1791
- names: Object.freeze(
1792
- input.unresolved.map((entry) => entry.name).sort(compareText),
1793
- ),
1794
- },
1795
- });
1796
- let stage: "prepare" | "commit" | "activate" = "prepare";
1797
- let committed = false;
1798
- try {
1799
- const revision = commitAgentSkillsContextUpdate({
1800
- store: this.store,
1801
- contextMeter: this.contextMeter,
1802
- idFactory: this.dependencies.idFactory,
1803
- snapshot,
1804
- surface,
1805
- addedOverrides: receipts,
1806
- settlements,
1807
- });
1808
- committed = true;
1809
- stage = "activate";
1810
- this.skillCoordinator.replaceActive(nextActive);
1811
- this.skillCoordinator.settle(
1812
- input.unresolved.map((activation) => activation.name),
1813
- );
1814
- const summary = Object.freeze({
1815
- previousRevisionNumber: snapshot.revision.revisionNumber,
1816
- revisionNumber: revision.revisionNumber,
1817
- activated: Object.freeze([...activated].sort()),
1818
- refreshed: Object.freeze([...(input.refreshed ?? [])].sort()),
1819
- deactivated: Object.freeze([...(input.deactivated ?? [])].sort()),
1820
- unavailable: Object.freeze([...unavailable].sort()),
1821
- addedOverrideCount: receipts.length,
1822
- });
1823
- await this.append({
1824
- type: "context.revision.finished",
1825
- sessionId: this.sessionId,
1826
- data: {
1827
- strategy: "skills_update",
1828
- reason: input.reason,
1829
- baseRevisionNumber: summary.previousRevisionNumber,
1830
- revisionNumber: summary.revisionNumber,
1831
- activated: summary.activated,
1832
- refreshed: summary.refreshed,
1833
- deactivated: summary.deactivated,
1834
- unavailable: summary.unavailable,
1835
- addedOverrideCount: summary.addedOverrideCount,
1836
- measuredAnchorCleared: true,
1837
- durationMs: elapsedMs(startedAt),
1838
- },
1839
- });
1840
- return summary;
1841
- } catch (error) {
1842
- if (error instanceof ContextManagerError) {
1843
- committed = error.committed;
1844
- stage =
1845
- error.stage === "commit"
1846
- ? "commit"
1847
- : error.stage === "activate"
1848
- ? "activate"
1849
- : "prepare";
1850
- }
1851
- await this.append({
1852
- type: "context.revision.failed",
1853
- sessionId: this.sessionId,
1854
- data: {
1855
- strategy: "skills_update",
1856
- reason: input.reason,
1857
- stage,
1858
- errorCode: boundedContextErrorCode(
1859
- error instanceof ContextManagerError
1860
- ? error.code
1861
- : error instanceof SessionError
1862
- ? error.code
1863
- : error instanceof Error
1864
- ? error.name
1865
- : "SKILLS_UPDATE_VALIDATION_FAILED",
1866
- ),
1867
- error: `Agent Skills update failed at ${stage}.`,
1868
- committed,
1869
- },
1870
- }).catch(() => undefined);
1871
- throw error;
1872
- }
850
+ this.runtimeSkills.markModelDispatch(input);
1873
851
  }
1874
852
 
1875
853
  promptScheduler(): PromptSchedulerSnapshot {
1876
- return this.promptSchedulerSnapshot;
854
+ return this.scheduler.promptScheduler();
1877
855
  }
1878
856
 
1879
857
  subscribePromptScheduler(listener: () => void): () => void {
1880
- this.promptSchedulerListeners.add(listener);
1881
- return () => this.promptSchedulerListeners.delete(listener);
858
+ return this.scheduler.subscribePromptScheduler(listener);
1882
859
  }
1883
860
 
1884
861
  queueFollowUp(userMessage: UserMessage): QueueFollowUpResult {
1885
- if (!this.executionChainRunning) {
1886
- throw new Error("Cannot queue a follow-up while no execution chain is running.");
1887
- }
1888
- validateUserMessage(userMessage);
1889
- if (userMessage.attachments !== undefined) {
1890
- throw new Error("Active-turn follow-ups do not support image attachments.");
1891
- }
1892
- if (this.queuedPrompts.length >= MAX_QUEUED_PROMPTS) {
1893
- throw new Error(`At most ${MAX_QUEUED_PROMPTS} follow-ups may be queued.`);
1894
- }
1895
- const queuedBytes = this.queuedPrompts.reduce(
1896
- (total, entry) => total + Buffer.byteLength(entry.userMessage.content, "utf8"),
1897
- 0,
1898
- );
1899
- const nextBytes = Buffer.byteLength(userMessage.content, "utf8");
1900
- if (queuedBytes + nextBytes > MAX_QUEUED_PROMPT_TEXT_BYTES) {
1901
- throw new Error("Queued follow-ups exceed the 64 KiB text limit.");
1902
- }
1903
- this.queuedPrompts.push({
1904
- userMessage: Object.freeze({ ...userMessage }),
1905
- });
1906
- this.notifyPromptScheduler();
1907
- return Object.freeze({
1908
- kind: "queued",
1909
- pendingCount: this.queuedPrompts.length,
1910
- ...(this.activeTurn === undefined
1911
- ? {}
1912
- : { activeTurnId: this.activeTurn.turn.turnId }),
1913
- });
862
+ return this.scheduler.queueFollowUp(userMessage);
1914
863
  }
1915
864
 
1916
- async executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
1917
- return (await this.admitTurn(input)).completion;
865
+ admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
866
+ return this.scheduler.admitTurn(input);
1918
867
  }
1919
868
 
1920
- async admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
1921
- if (this.executionChainRunning) {
1922
- throw new Error(
1923
- `Cannot execute a turn while RuntimeSession is ${this.state}; a prompt chain is already executing.`,
1924
- );
1925
- }
1926
- this.executionChainRunning = true;
1927
- this.notifyPromptScheduler();
1928
- try {
1929
- const accepted = await this.admitSingleTurn(input);
1930
- const completion = this.continueExecutionChain(accepted.completion, input.signal);
1931
- return Object.freeze({ ...accepted, completion });
1932
- } catch (error) {
1933
- this.executionChainRunning = false;
1934
- this.notifyPromptScheduler();
1935
- throw error;
1936
- }
869
+ async executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
870
+ return (await this.admitTurn(input)).completion;
1937
871
  }
1938
872
 
1939
873
  private async admitSingleTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
@@ -1997,7 +931,7 @@ class DefaultRuntimeSession implements RuntimeSession {
1997
931
  controller,
1998
932
  completion,
1999
933
  };
2000
- this.notifyPromptScheduler();
934
+ this.scheduler.notifyPromptScheduler();
2001
935
  return Object.freeze({
2002
936
  turnId: turn.turnId,
2003
937
  userMessage: input.userMessage,
@@ -2019,79 +953,6 @@ class DefaultRuntimeSession implements RuntimeSession {
2019
953
  }
2020
954
  }
2021
955
 
2022
- private async continueExecutionChain(
2023
- initialCompletion: Promise<RunAgentResult>,
2024
- signal: AbortSignal,
2025
- ): Promise<RunAgentResult> {
2026
- let completion = initialCompletion;
2027
- let finalResult: RunAgentResult;
2028
- try {
2029
- for (;;) {
2030
- finalResult = await completion;
2031
- if (finalResult.status !== "completed" || this.queuedPrompts.length === 0) {
2032
- return finalResult;
2033
- }
2034
- const next = this.queuedPrompts[0];
2035
- if (next === undefined) {
2036
- return finalResult;
2037
- }
2038
- const accepted = await this.admitSingleTurn({
2039
- userMessage: next.userMessage,
2040
- signal,
2041
- });
2042
- this.queuedPrompts.shift();
2043
- this.notifyPromptScheduler();
2044
- completion = accepted.completion;
2045
- }
2046
- } finally {
2047
- this.queuedPrompts.splice(0);
2048
- this.executionChainRunning = false;
2049
- this.notifyPromptScheduler();
2050
- }
2051
- }
2052
-
2053
- private notifyPromptScheduler(): void {
2054
- this.promptSchedulerSnapshot = Object.freeze({
2055
- state: this.executionChainRunning ? "running" : "idle",
2056
- ...(this.activeTurn === undefined
2057
- ? {}
2058
- : { activeTurnId: this.activeTurn.turn.turnId }),
2059
- pendingCount: this.queuedPrompts.length,
2060
- });
2061
- for (const listener of this.promptSchedulerListeners) listener();
2062
- }
2063
-
2064
- private async applyQueuedSteering(input: {
2065
- turn: TurnIdentity;
2066
- ledger: AgentTurnLedger;
2067
- }): Promise<number> {
2068
- if (this.activeTurn?.turn.turnId !== input.turn.turnId) {
2069
- throw new Error("Cannot apply steering outside the active turn.");
2070
- }
2071
- if (this.queuedPrompts.length === 0) return 0;
2072
- const drained = this.queuedPrompts.splice(0);
2073
- const records = input.ledger.appendSteeringUserMessages(
2074
- drained.map((entry) => entry.userMessage),
2075
- );
2076
- this.notifyPromptScheduler();
2077
- for (let index = 0; index < records.length; index += 1) {
2078
- const record = records[index];
2079
- const queued = drained[index];
2080
- if (record === undefined || queued === undefined) {
2081
- throw new Error("Steering ledger result did not match the drained queue.");
2082
- }
2083
- await this.append({
2084
- type: "turn.steering.applied",
2085
- ...input.turn,
2086
- data: {
2087
- userPrompt: projectUserMessage(queued.userMessage),
2088
- ordinal: record.ordinal,
2089
- },
2090
- });
2091
- }
2092
- return records.length;
2093
- }
2094
-
2095
956
  private settleAdmission(admission: ActiveAdmission): void {
2096
957
  if (this.activeAdmission !== admission) {
2097
958
  throw new Error("Runtime admission ownership was lost.");
@@ -2124,108 +985,14 @@ class DefaultRuntimeSession implements RuntimeSession {
2124
985
  });
2125
986
  }
2126
987
 
2127
- compactContext(): Promise<ContextCompactionResult> {
2128
- if (this.state !== "ready") {
2129
- throw new Error(`Cannot compact context while RuntimeSession is ${this.state}.`);
2130
- }
2131
- if (this.activeTurn !== undefined) {
2132
- throw new Error("Cannot compact context while a turn is active.");
2133
- }
2134
- const completion = this.performCompactContext();
2135
- this.activeContextRevision = completion;
2136
- void completion.then(
2137
- () => {
2138
- if (this.activeContextRevision === completion) {
2139
- this.activeContextRevision = undefined;
2140
- }
2141
- },
2142
- () => {
2143
- if (this.activeContextRevision === completion) {
2144
- this.activeContextRevision = undefined;
2145
- }
2146
- },
2147
- );
2148
- return completion;
2149
- }
2150
-
2151
- private async performCompactContext(): Promise<ContextCompactionResult> {
2152
- if (this.state !== "ready") {
2153
- throw new Error(`Cannot compact context while RuntimeSession is ${this.state}.`);
2154
- }
2155
- if (this.activeTurn !== undefined) {
2156
- throw new Error("Cannot compact context while a turn is active.");
2157
- }
2158
- this.store.assertContextRevisionIdle();
2159
- this.state = "compacting";
2160
- let started = false;
2161
- try {
2162
- await this.append({
2163
- type: "context.revision.started",
2164
- sessionId: this.sessionId,
2165
- data: {
2166
- strategy: "swap",
2167
- reason: "manual",
2168
- policyVersion: "swap-only-v1",
2169
- rendererFormat: "swap-observation-v1",
2170
- },
2171
- });
2172
- started = true;
2173
- const result = await this.requireContextManager().compact(
2174
- this.dependencies.manualCompactionTrigger(),
2175
- );
2176
- await this.append({
2177
- type: "context.revision.finished",
2178
- sessionId: this.sessionId,
2179
- data: contextRevisionFinishedData(result),
2180
- });
2181
- if (this.state === "compacting") {
2182
- this.state = "ready";
2183
- }
2184
- return result;
2185
- } catch (error) {
2186
- if (started && !(error instanceof RuntimeEventAppendError)) {
2187
- const failure =
2188
- error instanceof ContextManagerError
2189
- ? error
2190
- : new ContextManagerError(
2191
- "activate",
2192
- error instanceof Error ? error.name : "CONTEXT_COMPACTION_FAILED",
2193
- true,
2194
- false,
2195
- "Context compaction failed.",
2196
- { cause: error },
2197
- );
2198
- await this.append({
2199
- type: "context.revision.failed",
2200
- sessionId: this.sessionId,
2201
- data: {
2202
- strategy: "swap",
2203
- reason: "manual",
2204
- stage: failure.stage,
2205
- errorCode: boundedContextErrorCode(failure.code),
2206
- error: `Context compaction failed at ${failure.stage}.`,
2207
- },
2208
- }).catch(() => undefined);
2209
- }
2210
- if (!(error instanceof ContextManagerError) || error.fatal) {
2211
- this.fault(error);
2212
- } else if (this.state === "compacting") {
2213
- this.state = "ready";
2214
- }
2215
- throw error;
2216
- }
2217
- }
2218
-
2219
- retireContext(): Promise<ContextRetirementResult> {
2220
- if (this.state !== "ready") {
2221
- throw new Error(
2222
- `Cannot retire context prefix while RuntimeSession is ${this.state}.`,
2223
- );
988
+ compactContext(): Promise<ContextCompactionResult> {
989
+ if (this.state !== "ready") {
990
+ throw new Error(`Cannot compact context while RuntimeSession is ${this.state}.`);
2224
991
  }
2225
992
  if (this.activeTurn !== undefined) {
2226
- throw new Error("Cannot retire context prefix while a turn is active.");
993
+ throw new Error("Cannot compact context while a turn is active.");
2227
994
  }
2228
- const completion = this.performRetireContext();
995
+ const completion = this.contextMaintenance.performCompactContext();
2229
996
  this.activeContextRevision = completion;
2230
997
  void completion.then(
2231
998
  () => {
@@ -2242,7 +1009,7 @@ class DefaultRuntimeSession implements RuntimeSession {
2242
1009
  return completion;
2243
1010
  }
2244
1011
 
2245
- private async performRetireContext(): Promise<ContextRetirementResult> {
1012
+ retireContext(): Promise<ContextRetirementResult> {
2246
1013
  if (this.state !== "ready") {
2247
1014
  throw new Error(
2248
1015
  `Cannot retire context prefix while RuntimeSession is ${this.state}.`,
@@ -2251,67 +1018,21 @@ class DefaultRuntimeSession implements RuntimeSession {
2251
1018
  if (this.activeTurn !== undefined) {
2252
1019
  throw new Error("Cannot retire context prefix while a turn is active.");
2253
1020
  }
2254
- this.store.assertContextRevisionIdle();
2255
- const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
2256
- this.state = "compacting";
2257
- let started = false;
2258
- try {
2259
- await this.append({
2260
- type: "context.revision.started",
2261
- sessionId: this.sessionId,
2262
- data: {
2263
- strategy: "retire_prefix",
2264
- reason: "manual",
2265
- policyVersion: "recall-first-retirement-v1",
2266
- baseRevisionNumber,
2267
- },
2268
- });
2269
- started = true;
2270
- const result = await this.requireContextManager().retirePrefix(
2271
- this.dependencies.manualRetirementTrigger(),
2272
- );
2273
- await this.append({
2274
- type: "context.revision.finished",
2275
- sessionId: this.sessionId,
2276
- data: contextRetirementFinishedData(result),
2277
- });
2278
- if (this.state === "compacting") {
2279
- this.state = "ready";
2280
- }
2281
- return result;
2282
- } catch (error) {
2283
- if (started && !(error instanceof RuntimeEventAppendError)) {
2284
- const failure =
2285
- error instanceof ContextManagerError
2286
- ? error
2287
- : new ContextManagerError(
2288
- "activate",
2289
- error instanceof Error ? error.name : "CONTEXT_RETIREMENT_FAILED",
2290
- true,
2291
- false,
2292
- "Context prefix retirement failed.",
2293
- { cause: error },
2294
- );
2295
- await this.append({
2296
- type: "context.revision.failed",
2297
- sessionId: this.sessionId,
2298
- data: {
2299
- strategy: "retire_prefix",
2300
- reason: "manual",
2301
- stage: failure.stage,
2302
- errorCode: boundedContextErrorCode(failure.code),
2303
- error: `Context prefix retirement failed at ${failure.stage}.`,
2304
- committed: failure.committed,
2305
- },
2306
- }).catch(() => undefined);
2307
- }
2308
- if (!(error instanceof ContextManagerError) || error.fatal) {
2309
- this.fault(error);
2310
- } else if (this.state === "compacting") {
2311
- this.state = "ready";
2312
- }
2313
- throw error;
2314
- }
1021
+ const completion = this.contextMaintenance.performRetireContext();
1022
+ this.activeContextRevision = completion;
1023
+ void completion.then(
1024
+ () => {
1025
+ if (this.activeContextRevision === completion) {
1026
+ this.activeContextRevision = undefined;
1027
+ }
1028
+ },
1029
+ () => {
1030
+ if (this.activeContextRevision === completion) {
1031
+ this.activeContextRevision = undefined;
1032
+ }
1033
+ },
1034
+ );
1035
+ return completion;
2315
1036
  }
2316
1037
 
2317
1038
  dispose(reason: SessionDisposeReason): Promise<void> {
@@ -2322,8 +1043,8 @@ class DefaultRuntimeSession implements RuntimeSession {
2322
1043
  canSwitchSession(): boolean {
2323
1044
  return (
2324
1045
  this.state === "ready" &&
2325
- !this.executionChainRunning &&
2326
- this.queuedPrompts.length === 0 &&
1046
+ !this.scheduler.isRunning &&
1047
+ this.scheduler.pendingCount === 0 &&
2327
1048
  this.activeTurn === undefined &&
2328
1049
  (this.tooling?.taskManager
2329
1050
  .listBackgroundTasks()
@@ -2445,9 +1166,9 @@ class DefaultRuntimeSession implements RuntimeSession {
2445
1166
  const selection = this.dependencies.selectShadowPlanning(planningInput);
2446
1167
  if (
2447
1168
  selection?.trigger === "runtime_pressure" &&
2448
- this.requireContextAutomation().automaticSwapOnly
1169
+ this.requireContextAutomation().automaticSwap
2449
1170
  ) {
2450
- this.pendingAutomaticContextMaintenance = true;
1171
+ this.contextMaintenance.scheduleAutomaticMaintenance();
2451
1172
  }
2452
1173
  return selection;
2453
1174
  },
@@ -2477,7 +1198,7 @@ class DefaultRuntimeSession implements RuntimeSession {
2477
1198
  );
2478
1199
  pendingLedgerTurn.finish(error.result);
2479
1200
  settled = true;
2480
- await this.settleClosedTurnSkills();
1201
+ await this.runtimeSkills.settleClosedTurnSkills();
2481
1202
  throw error;
2482
1203
  }
2483
1204
 
@@ -2497,10 +1218,10 @@ class DefaultRuntimeSession implements RuntimeSession {
2497
1218
  if (result.status === "completed") {
2498
1219
  this.notifyCompletedTurn(turn);
2499
1220
  }
2500
- await this.settleClosedTurnSkills();
1221
+ await this.runtimeSkills.settleClosedTurnSkills();
2501
1222
  if (result.status === "completed") {
2502
- await this.evaluateClosedTurnContextPressure();
2503
- await this.performAutomaticContextMaintenance();
1223
+ await this.contextMaintenance.evaluateClosedTurnContextPressure();
1224
+ await this.contextMaintenance.performAutomaticContextMaintenance();
2504
1225
  }
2505
1226
  return result;
2506
1227
  } catch (error) {
@@ -2512,317 +1233,14 @@ class DefaultRuntimeSession implements RuntimeSession {
2512
1233
  } finally {
2513
1234
  removeExternalAbortListener();
2514
1235
  this.activeTurn = undefined;
2515
- this.notifyPromptScheduler();
2516
- this.pendingAutomaticContextMaintenance = false;
2517
- this.pendingModelDirectedSwap = undefined;
2518
- this.modelDirectedSwapLease = false;
2519
- this.pressureNoticeSentThisTurn = false;
1236
+ this.scheduler.notifyPromptScheduler();
1237
+ this.contextMaintenance.finishTurn();
2520
1238
  if (this.state === "executing") {
2521
1239
  this.state = "ready";
2522
1240
  }
2523
1241
  }
2524
1242
  }
2525
1243
 
2526
- private async evaluateClosedTurnContextPressure(): Promise<void> {
2527
- const automation = this.requireContextAutomation();
2528
- if (!automation.automaticSwapOnly) return;
2529
-
2530
- const snapshot = this.requireContextManager().measureCurrent();
2531
- await this.append({
2532
- type: "context.usage.updated",
2533
- sessionId: this.sessionId,
2534
- data: { phase: "turn_close", snapshot },
2535
- });
2536
- if (snapshot.pressure !== "normal") {
2537
- this.pendingAutomaticContextMaintenance = true;
2538
- }
2539
- }
2540
-
2541
- private async performAutomaticContextMaintenance(): Promise<void> {
2542
- if (!this.pendingAutomaticContextMaintenance) return;
2543
- this.pendingAutomaticContextMaintenance = false;
2544
- const automation = this.requireContextAutomation();
2545
- if (!automation.automaticSwapOnly) return;
2546
- if (this.state !== "executing") {
2547
- throw new Error(
2548
- `Cannot run automatic context maintenance while RuntimeSession is ${this.state}.`,
2549
- );
2550
- }
2551
- this.store.assertContextRevisionIdle();
2552
- const qualificationId = requireAutomationQualificationId(automation);
2553
- this.state = "maintaining_context";
2554
- try {
2555
- const swap = await this.performAutomaticCompaction(qualificationId);
2556
- if (swap === undefined) return;
2557
- if (automation.automaticPrefixRetirement && automaticSwapNeedsRetirement(swap)) {
2558
- await this.performAutomaticRetirement(qualificationId);
2559
- }
2560
- } finally {
2561
- if (this.state === "maintaining_context") {
2562
- this.state = "executing";
2563
- }
2564
- }
2565
- }
2566
-
2567
- private async performActiveTurnContextMaintenance(input: {
2568
- turn: TurnIdentity;
2569
- consumedThroughOrdinal: number;
2570
- ledger: AgentTurnLedger;
2571
- }): Promise<void> {
2572
- if (this.state !== "executing") {
2573
- throw new Error(
2574
- `Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
2575
- );
2576
- }
2577
- const pendingModelDirectedSwap = this.pendingModelDirectedSwap;
2578
- this.pendingModelDirectedSwap = undefined;
2579
-
2580
- const automation = this.requireContextAutomation();
2581
- const manager = this.requireContextManager();
2582
- this.pendingAutomaticContextMaintenance = false;
2583
-
2584
- let suppressAutomaticSwap = this.modelDirectedSwapLease;
2585
- this.modelDirectedSwapLease = false;
2586
-
2587
- if (pendingModelDirectedSwap !== undefined) {
2588
- suppressAutomaticSwap = false;
2589
- this.state = "maintaining_context";
2590
- try {
2591
- await this.performModelDirectedCompaction({
2592
- turn: input.turn,
2593
- consumedThroughOrdinal: input.consumedThroughOrdinal,
2594
- ledger: input.ledger,
2595
- messageIds: Object.freeze([...pendingModelDirectedSwap]),
2596
- });
2597
- } finally {
2598
- if (this.state === "maintaining_context") {
2599
- this.state = "executing";
2600
- }
2601
- }
2602
- }
2603
-
2604
- let measured: ContextUsageSnapshot | undefined;
2605
- if (
2606
- pendingModelDirectedSwap === undefined &&
2607
- (suppressAutomaticSwap ||
2608
- !this.pressureNoticeSentThisTurn ||
2609
- automation.automaticSwapOnly)
2610
- ) {
2611
- measured = manager.measureCurrent(input.turn.turnId, input.ledger);
2612
- if (!this.pressureNoticeSentThisTurn && measured.pressure !== "normal") {
2613
- await this.injectContextPressureNotice({
2614
- turn: input.turn,
2615
- ledger: input.ledger,
2616
- usage: measured,
2617
- automaticSwapEnabled: automation.automaticSwapOnly,
2618
- });
2619
- this.pressureNoticeSentThisTurn = true;
2620
- suppressAutomaticSwap = true;
2621
- }
2622
- if (measured.pressure === "blocked") {
2623
- // Emergency override: a lease or notice must never hold automatic
2624
- // compaction past the budget line; the next preflight would fail the
2625
- // turn before the model could act.
2626
- suppressAutomaticSwap = false;
2627
- }
2628
- }
2629
-
2630
- if (suppressAutomaticSwap || !automation.automaticSwapOnly) {
2631
- return;
2632
- }
2633
-
2634
- this.state = "maintaining_context";
2635
- try {
2636
- const usage = measured ?? manager.measureCurrent(input.turn.turnId, input.ledger);
2637
- if (usage.pressure === "normal") return;
2638
-
2639
- const qualificationId = requireAutomationQualificationId(automation);
2640
- const compactionTrigger = {
2641
- kind: "runtime_pressure",
2642
- activeTurn: {
2643
- turnId: input.turn.turnId,
2644
- consumedThroughOrdinal: input.consumedThroughOrdinal,
2645
- },
2646
- } as const;
2647
- await this.append({
2648
- type: "context.revision.started",
2649
- sessionId: this.sessionId,
2650
- data: {
2651
- strategy: "swap",
2652
- reason: "runtime_pressure",
2653
- policyVersion: "swap-only-v1",
2654
- rendererFormat: "swap-observation-v1",
2655
- qualificationId,
2656
- },
2657
- });
2658
- let swap: ContextCompactionResult;
2659
- try {
2660
- swap = await manager.compact(compactionTrigger, input.ledger);
2661
- await this.append({
2662
- type: "context.revision.finished",
2663
- sessionId: this.sessionId,
2664
- data: contextRevisionFinishedData(swap, "runtime_pressure", qualificationId),
2665
- });
2666
- } catch (error) {
2667
- const failure = automaticContextFailure(error, "compaction");
2668
- await this.append({
2669
- type: "context.revision.failed",
2670
- sessionId: this.sessionId,
2671
- data: {
2672
- strategy: "swap",
2673
- reason: "runtime_pressure",
2674
- stage: failure.stage,
2675
- errorCode: boundedContextErrorCode(failure.code),
2676
- error: `Automatic context compaction failed at ${failure.stage}.`,
2677
- qualificationId,
2678
- },
2679
- }).catch(() => undefined);
2680
- if (failure.fatal) throw error;
2681
- return;
2682
- }
2683
-
2684
- if (
2685
- !automation.automaticPrefixRetirement ||
2686
- !automaticSwapNeedsRetirement(swap)
2687
- ) {
2688
- return;
2689
- }
2690
-
2691
- await this.append({
2692
- type: "context.revision.started",
2693
- sessionId: this.sessionId,
2694
- data: {
2695
- strategy: "retire_prefix",
2696
- reason: "runtime_pressure",
2697
- policyVersion: "recall-first-retirement-v1",
2698
- baseRevisionNumber: this.store.loadContextSnapshot().revision.revisionNumber,
2699
- qualificationId,
2700
- },
2701
- });
2702
- try {
2703
- const retirement = await manager.retirePrefix(
2704
- {
2705
- kind: "runtime_pressure",
2706
- activeTurnId: input.turn.turnId,
2707
- },
2708
- input.ledger,
2709
- );
2710
- await this.append({
2711
- type: "context.revision.finished",
2712
- sessionId: this.sessionId,
2713
- data: contextRetirementFinishedData(
2714
- retirement,
2715
- "runtime_pressure",
2716
- qualificationId,
2717
- ),
2718
- });
2719
- } catch (error) {
2720
- const failure = automaticContextFailure(error, "retirement");
2721
- await this.append({
2722
- type: "context.revision.failed",
2723
- sessionId: this.sessionId,
2724
- data: {
2725
- strategy: "retire_prefix",
2726
- reason: "runtime_pressure",
2727
- stage: failure.stage,
2728
- errorCode: boundedContextErrorCode(failure.code),
2729
- error: `Automatic context retirement failed at ${failure.stage}.`,
2730
- committed: failure.committed,
2731
- qualificationId,
2732
- },
2733
- }).catch(() => undefined);
2734
- if (failure.fatal) throw error;
2735
- }
2736
- } finally {
2737
- if (this.state === "maintaining_context") {
2738
- this.state = "executing";
2739
- }
2740
- }
2741
- }
2742
-
2743
- private async injectContextPressureNotice(input: {
2744
- turn: TurnIdentity;
2745
- ledger: AgentTurnLedger;
2746
- usage: ContextUsageSnapshot;
2747
- automaticSwapEnabled: boolean;
2748
- }): Promise<void> {
2749
- const userMessage: UserMessage = Object.freeze({
2750
- role: "user",
2751
- content: contextPressureNoticeText({
2752
- usage: input.usage,
2753
- toolPressure: toolContextPressure(input.usage.pressure) as "high" | "critical",
2754
- automaticSwapEnabled: input.automaticSwapEnabled,
2755
- }),
2756
- });
2757
- const records = input.ledger.appendSteeringUserMessages([userMessage]);
2758
- const record = records[0];
2759
- if (records.length !== 1 || record === undefined) {
2760
- throw new Error("Pressure notice steering did not append exactly one message.");
2761
- }
2762
- await this.append({
2763
- type: "context.pressure_notice.sent",
2764
- ...input.turn,
2765
- data: {
2766
- usedInputTokens: input.usage.usedInputTokens,
2767
- inputBudgetTokens: input.usage.inputBudgetTokens,
2768
- triggerTokens: input.usage.triggerTokens,
2769
- pressure: input.usage.pressure === "blocked" ? "blocked" : "triggered",
2770
- automaticSwapEnabled: input.automaticSwapEnabled,
2771
- ordinal: record.ordinal,
2772
- },
2773
- });
2774
- }
2775
-
2776
- private async performModelDirectedCompaction(input: {
2777
- turn: TurnIdentity;
2778
- consumedThroughOrdinal: number;
2779
- ledger: AgentTurnLedger;
2780
- messageIds: readonly MessageId[];
2781
- }): Promise<void> {
2782
- await this.append({
2783
- type: "context.revision.started",
2784
- sessionId: this.sessionId,
2785
- data: {
2786
- strategy: "swap",
2787
- reason: "model_directed",
2788
- policyVersion: "swap-only-v1",
2789
- rendererFormat: "swap-observation-v1",
2790
- },
2791
- });
2792
- try {
2793
- const result = await this.requireContextManager().compact(
2794
- {
2795
- kind: "model_directed",
2796
- messageIds: input.messageIds,
2797
- activeTurn: {
2798
- turnId: input.turn.turnId,
2799
- consumedThroughOrdinal: input.consumedThroughOrdinal,
2800
- },
2801
- },
2802
- input.ledger,
2803
- );
2804
- await this.append({
2805
- type: "context.revision.finished",
2806
- sessionId: this.sessionId,
2807
- data: contextRevisionFinishedData(result, "model_directed"),
2808
- });
2809
- } catch (error) {
2810
- const failure = automaticContextFailure(error, "compaction");
2811
- await this.append({
2812
- type: "context.revision.failed",
2813
- sessionId: this.sessionId,
2814
- data: {
2815
- strategy: "swap",
2816
- reason: "model_directed",
2817
- stage: failure.stage,
2818
- errorCode: boundedContextErrorCode(failure.code),
2819
- error: `Model-directed context compaction failed at ${failure.stage}.`,
2820
- },
2821
- }).catch(() => undefined);
2822
- if (failure.fatal) throw error;
2823
- }
2824
- }
2825
-
2826
1244
  private notifyCompletedTurn(turn: TurnIdentity): void {
2827
1245
  const hook = this.input.completedTurnHook;
2828
1246
  if (hook === undefined) {
@@ -2872,133 +1290,6 @@ class DefaultRuntimeSession implements RuntimeSession {
2872
1290
  }
2873
1291
  }
2874
1292
 
2875
- private async performAutomaticCompaction(
2876
- qualificationId: string,
2877
- ): Promise<ContextCompactionResult | undefined> {
2878
- let started = false;
2879
- try {
2880
- await this.append({
2881
- type: "context.revision.started",
2882
- sessionId: this.sessionId,
2883
- data: {
2884
- strategy: "swap",
2885
- reason: "runtime_pressure",
2886
- policyVersion: "swap-only-v1",
2887
- rendererFormat: "swap-observation-v1",
2888
- qualificationId,
2889
- },
2890
- });
2891
- started = true;
2892
- const result = await this.requireContextManager().compact(
2893
- this.dependencies.automaticCompactionTrigger(),
2894
- );
2895
- await this.append({
2896
- type: "context.revision.finished",
2897
- sessionId: this.sessionId,
2898
- data: contextRevisionFinishedData(result, "runtime_pressure", qualificationId),
2899
- });
2900
- return result;
2901
- } catch (error) {
2902
- if (started && !(error instanceof RuntimeEventAppendError)) {
2903
- const failure = automaticContextFailure(error, "compaction");
2904
- await this.append({
2905
- type: "context.revision.failed",
2906
- sessionId: this.sessionId,
2907
- data: {
2908
- strategy: "swap",
2909
- reason: "runtime_pressure",
2910
- stage: failure.stage,
2911
- errorCode: boundedContextErrorCode(failure.code),
2912
- error: `Automatic context compaction failed at ${failure.stage}.`,
2913
- qualificationId,
2914
- },
2915
- }).catch(() => undefined);
2916
- }
2917
- if (error instanceof ContextManagerError && !error.fatal) {
2918
- return undefined;
2919
- }
2920
- throw error;
2921
- }
2922
- }
2923
-
2924
- private async performAutomaticRetirement(
2925
- qualificationId: string,
2926
- ): Promise<ContextRetirementResult | undefined> {
2927
- const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
2928
- let started = false;
2929
- try {
2930
- await this.append({
2931
- type: "context.revision.started",
2932
- sessionId: this.sessionId,
2933
- data: {
2934
- strategy: "retire_prefix",
2935
- reason: "runtime_pressure",
2936
- policyVersion: "recall-first-retirement-v1",
2937
- baseRevisionNumber,
2938
- qualificationId,
2939
- },
2940
- });
2941
- started = true;
2942
- const result = await this.requireContextManager().retirePrefix(
2943
- this.dependencies.automaticRetirementTrigger(),
2944
- );
2945
- await this.append({
2946
- type: "context.revision.finished",
2947
- sessionId: this.sessionId,
2948
- data: contextRetirementFinishedData(
2949
- result,
2950
- "runtime_pressure",
2951
- qualificationId,
2952
- ),
2953
- });
2954
- return result;
2955
- } catch (error) {
2956
- if (started && !(error instanceof RuntimeEventAppendError)) {
2957
- const failure = automaticContextFailure(error, "retirement");
2958
- await this.append({
2959
- type: "context.revision.failed",
2960
- sessionId: this.sessionId,
2961
- data: {
2962
- strategy: "retire_prefix",
2963
- reason: "runtime_pressure",
2964
- stage: failure.stage,
2965
- errorCode: boundedContextErrorCode(failure.code),
2966
- error: `Automatic context retirement failed at ${failure.stage}.`,
2967
- committed: failure.committed,
2968
- qualificationId,
2969
- },
2970
- }).catch(() => undefined);
2971
- }
2972
- if (error instanceof ContextManagerError && !error.fatal) {
2973
- return undefined;
2974
- }
2975
- throw error;
2976
- }
2977
- }
2978
-
2979
- private async settleClosedTurnSkills(): Promise<void> {
2980
- const unresolved = this.store.loadSkillActivations(["pending", "dispatched"]);
2981
- if (unresolved.length === 0) {
2982
- return;
2983
- }
2984
- const summary = await this.commitSkillSettlements({
2985
- reason: "activation",
2986
- unresolved,
2987
- });
2988
- await this.append({
2989
- type: "skills.updated",
2990
- sessionId: this.sessionId,
2991
- data: {
2992
- reason: "activation",
2993
- activated: summary.activated,
2994
- refreshed: summary.refreshed,
2995
- deactivated: summary.deactivated,
2996
- unavailable: summary.unavailable,
2997
- revisionNumber: summary.revisionNumber,
2998
- },
2999
- });
3000
- }
3001
-
3002
1293
  private async appendTerminalEvent(
3003
1294
  turn: TurnIdentity,
3004
1295
  result: RunAgentResult,
@@ -3040,9 +1331,7 @@ class DefaultRuntimeSession implements RuntimeSession {
3040
1331
  }
3041
1332
 
3042
1333
  this.state = "disposing";
3043
- this.queuedPrompts.splice(0);
3044
- this.executionChainRunning = false;
3045
- this.notifyPromptScheduler();
1334
+ this.scheduler.clear();
3046
1335
  const errors: unknown[] = this.faultCause === undefined ? [] : [this.faultCause];
3047
1336
  const activeAdmission = this.activeAdmission;
3048
1337
  if (activeAdmission !== undefined) {
@@ -3452,6 +1741,7 @@ function validateCreateInput(input: CreateRuntimeSessionInput): void {
3452
1741
  if (input.modelName.trim() === "") {
3453
1742
  throw new Error("RuntimeSession modelName must not be empty.");
3454
1743
  }
1744
+
3455
1745
  requirePositiveNumber(input.maxIterations, "maxIterations");
3456
1746
  if (input.systemPrompt.trim() === "") {
3457
1747
  throw new Error("RuntimeSession systemPrompt must not be empty.");
@@ -3468,24 +1758,8 @@ function validateCreateInput(input: CreateRuntimeSessionInput): void {
3468
1758
  "RuntimeSession modelClient must implement prepare() and request().",
3469
1759
  );
3470
1760
  }
3471
- assertMatchingContextBudget(input.contextProfile, input.contextBudget);
3472
- }
3473
-
3474
- function assertPreparedMatchesSurface(
3475
- prepared: ReturnType<ModelClient["prepare"]>,
3476
- surface: StoredContextSurfaceV8,
3477
- ): void {
3478
- if (
3479
- prepared.requestConfigHash !== surface.requestConfigSha256 ||
3480
- prepared.toolSchemaHash !== surface.toolSchemaSha256 ||
3481
- prepared.requestMaxOutputTokens !== surface.requestMaxOutputTokens
3482
- ) {
3483
- throw new Error("Prepared model request does not match its context surface.");
3484
- }
3485
- }
3486
1761
 
3487
- function elapsedMs(startedAt: number): number {
3488
- return Math.round((performance.now() - startedAt) * 100) / 100;
1762
+ assertMatchingContextBudget(input.contextProfile, input.contextBudget);
3489
1763
  }
3490
1764
 
3491
1765
  function isNewSessionInput(
@@ -3560,166 +1834,12 @@ function errorMessage(error: unknown): string {
3560
1834
  return error instanceof Error ? error.message : String(error);
3561
1835
  }
3562
1836
 
3563
- function contextRevisionFinishedData(
3564
- result: ContextCompactionResult,
3565
- reason: "manual" | "runtime_pressure" | "model_directed" = "manual",
3566
- qualificationId?: string,
3567
- ): ContextRevisionFinishedData {
3568
- if (result.status === "unchanged") {
3569
- return {
3570
- strategy: "swap",
3571
- reason,
3572
- policyVersion: "swap-only-v1",
3573
- outcome: result.outcome,
3574
- baseRevisionNumber: result.revisionNumber,
3575
- addedOverrideCount: 0,
3576
- activeOverrideCount: result.activeOverrideCount,
3577
- originalObservationBytes: 0,
3578
- projectedObservationBytes: 0,
3579
- rawTokensBefore: result.rawTokensBefore,
3580
- guardedTokensBefore: result.guardedTokensBefore,
3581
- targetTokens: result.targetTokens,
3582
- durationMs: result.durationMs,
3583
- ...(qualificationId === undefined ? {} : { qualificationId }),
3584
- };
3585
- }
3586
- return {
3587
- strategy: "swap",
3588
- reason,
3589
- policyVersion: "swap-only-v1",
3590
- outcome: result.outcome,
3591
- baseRevisionNumber: result.previousRevisionNumber,
3592
- revisionNumber: result.revisionNumber,
3593
- addedOverrideCount: result.addedOverrideCount,
3594
- activeOverrideCount: result.activeOverrideCount,
3595
- originalObservationBytes: result.originalObservationBytes,
3596
- projectedObservationBytes: result.projectedObservationBytes,
3597
- rawTokensBefore: result.rawTokensBefore,
3598
- rawTokensAfter: result.rawTokensAfter,
3599
- guardedTokensBefore: result.guardedTokensBefore,
3600
- guardedTokensAfter: result.guardedTokensAfter,
3601
- targetTokens: result.targetTokens,
3602
- planHash: result.planHash,
3603
- durationMs: result.durationMs,
3604
- ...(qualificationId === undefined ? {} : { qualificationId }),
3605
- };
3606
- }
3607
-
3608
- function contextRetirementFinishedData(
3609
- result: ContextRetirementResult,
3610
- reason: "manual" | "runtime_pressure" = "manual",
3611
- qualificationId?: string,
3612
- ): ContextRevisionFinishedData {
3613
- if (result.status === "unchanged") {
3614
- return {
3615
- strategy: "retire_prefix",
3616
- reason,
3617
- policyVersion: "recall-first-retirement-v1",
3618
- outcome: result.outcome,
3619
- baseRevisionNumber: result.revisionNumber,
3620
- previousKeepFromOrdinal: result.keepFromOrdinal,
3621
- keepFromOrdinal: result.keepFromOrdinal,
3622
- retiredTurnCount: 0,
3623
- retiredFrameCount: 0,
3624
- retiredMessageCount: 0,
3625
- activeOverrideCount: result.activeOverrideCount,
3626
- guardedTokensBefore: result.guardedTokensBefore,
3627
- targetTokens: result.targetTokens,
3628
- planningDurationMs: result.planningDurationMs,
3629
- durationMs: result.durationMs,
3630
- ...(qualificationId === undefined ? {} : { qualificationId }),
3631
- };
3632
- }
3633
- return {
3634
- strategy: "retire_prefix",
3635
- reason,
3636
- policyVersion: "recall-first-retirement-v1",
3637
- outcome: result.outcome,
3638
- baseRevisionNumber: result.previousRevisionNumber,
3639
- revisionNumber: result.revisionNumber,
3640
- previousKeepFromOrdinal: result.previousKeepFromOrdinal,
3641
- keepFromOrdinal: result.keepFromOrdinal,
3642
- retiredTurnCount: result.retiredTurnCount,
3643
- retiredFrameCount: result.retiredFrameCount,
3644
- retiredMessageCount: result.retiredMessageCount,
3645
- activeOverrideCount: result.activeOverrideCount,
3646
- rawTokensBefore: result.rawTokensBefore,
3647
- rawTokensAfter: result.rawTokensAfter,
3648
- guardedTokensBefore: result.guardedTokensBefore,
3649
- guardedTokensAfter: result.guardedTokensAfter,
3650
- targetTokens: result.targetTokens,
3651
- planHash: result.planHash,
3652
- planningDurationMs: result.planningDurationMs,
3653
- validationDurationMs: result.validationDurationMs,
3654
- transactionDurationMs: result.transactionDurationMs,
3655
- activationDurationMs: result.activationDurationMs,
3656
- durationMs: result.durationMs,
3657
- ...(qualificationId === undefined ? {} : { qualificationId }),
3658
- };
3659
- }
3660
-
3661
- function requireAutomationQualificationId(decision: ContextAutomationDecision): string {
3662
- if (
3663
- !decision.automaticSwapOnly ||
3664
- (decision.reason !== "qualified" && decision.reason !== "swap_only_qualified") ||
3665
- decision.qualificationId === undefined
3666
- ) {
3667
- throw new Error("Automatic context maintenance has no qualification identity.");
3668
- }
3669
- return decision.qualificationId;
3670
- }
3671
-
3672
- function automaticSwapNeedsRetirement(result: ContextCompactionResult): boolean {
3673
- return (
3674
- result.outcome === "no_eligible_candidates" ||
3675
- result.outcome === "insufficient_candidates"
3676
- );
3677
- }
3678
-
3679
- function automaticContextFailure(
3680
- error: unknown,
3681
- strategy: "compaction" | "retirement",
3682
- ): ContextManagerError {
3683
- return error instanceof ContextManagerError
3684
- ? error
3685
- : new ContextManagerError(
3686
- "activate",
3687
- error instanceof Error
3688
- ? error.name
3689
- : `AUTOMATIC_CONTEXT_${strategy.toUpperCase()}_FAILED`,
3690
- true,
3691
- false,
3692
- `Automatic context ${strategy} failed.`,
3693
- { cause: error },
3694
- );
3695
- }
3696
-
3697
- function boundedContextErrorCode(code: string): string {
3698
- return /^[A-Za-z0-9_]+$/.test(code) && code.length <= 80
3699
- ? code
3700
- : "CONTEXT_COMPACTION_FAILED";
3701
- }
3702
-
3703
- function toolContextPressure(
3704
- pressure: ContextPressure,
3705
- ): "normal" | "high" | "critical" {
3706
- return pressure === "triggered"
3707
- ? "high"
3708
- : pressure === "blocked"
3709
- ? "critical"
3710
- : "normal";
3711
- }
3712
-
3713
1837
  function requirePositiveNumber(value: number, name: string): void {
3714
1838
  if (!Number.isInteger(value) || value < 1) {
3715
1839
  throw new Error(`${name} must be a positive integer; received ${value}.`);
3716
1840
  }
3717
1841
  }
3718
1842
 
3719
- function compareText(left: string, right: string): number {
3720
- return left < right ? -1 : left > right ? 1 : 0;
3721
- }
3722
-
3723
1843
  function isCanonicalRuntimeFault(error: unknown): boolean {
3724
1844
  return (
3725
1845
  error instanceof ContextProtocolError ||