tinker-agent 1.0.65 → 1.2.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 (208) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +90 -9
  5. package/bin/tinker.js +39 -0
  6. package/node_modules/ansi-regex/index.d.ts +33 -0
  7. package/node_modules/ansi-regex/index.js +14 -0
  8. package/node_modules/ansi-regex/license +9 -0
  9. package/node_modules/ansi-regex/package.json +61 -0
  10. package/node_modules/ansi-regex/readme.md +66 -0
  11. package/node_modules/ansi-styles/index.d.ts +236 -0
  12. package/node_modules/ansi-styles/index.js +223 -0
  13. package/node_modules/ansi-styles/license +9 -0
  14. package/node_modules/ansi-styles/package.json +54 -0
  15. package/node_modules/ansi-styles/readme.md +173 -0
  16. package/node_modules/chalk/license +9 -0
  17. package/node_modules/chalk/package.json +83 -0
  18. package/node_modules/chalk/readme.md +297 -0
  19. package/node_modules/chalk/source/index.d.ts +325 -0
  20. package/node_modules/chalk/source/index.js +225 -0
  21. package/node_modules/chalk/source/utilities.js +33 -0
  22. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  23. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  24. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  25. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  26. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  27. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  28. package/node_modules/character-entities/index.d.ts +6 -0
  29. package/node_modules/character-entities/index.js +2132 -0
  30. package/node_modules/character-entities/license +22 -0
  31. package/node_modules/character-entities/package.json +78 -0
  32. package/node_modules/character-entities/readme.md +152 -0
  33. package/node_modules/decode-named-character-reference/index.d.ts +13 -0
  34. package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
  35. package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
  36. package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
  37. package/node_modules/decode-named-character-reference/index.dom.js +32 -0
  38. package/node_modules/decode-named-character-reference/index.js +19 -0
  39. package/node_modules/decode-named-character-reference/license +22 -0
  40. package/node_modules/decode-named-character-reference/package.json +90 -0
  41. package/node_modules/decode-named-character-reference/readme.md +136 -0
  42. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  43. package/node_modules/get-east-asian-width/index.js +30 -0
  44. package/node_modules/get-east-asian-width/license +9 -0
  45. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  46. package/node_modules/get-east-asian-width/lookup.js +138 -0
  47. package/node_modules/get-east-asian-width/package.json +71 -0
  48. package/node_modules/get-east-asian-width/readme.md +65 -0
  49. package/node_modules/get-east-asian-width/utilities.js +24 -0
  50. package/node_modules/has-flag/index.d.ts +39 -0
  51. package/node_modules/has-flag/index.js +8 -0
  52. package/node_modules/has-flag/license +9 -0
  53. package/node_modules/has-flag/package.json +49 -0
  54. package/node_modules/has-flag/readme.md +74 -0
  55. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  56. package/node_modules/is-fullwidth-code-point/index.js +12 -0
  57. package/node_modules/is-fullwidth-code-point/license +9 -0
  58. package/node_modules/is-fullwidth-code-point/package.json +53 -0
  59. package/node_modules/is-fullwidth-code-point/readme.md +31 -0
  60. package/node_modules/markdansi/LICENSE +21 -0
  61. package/node_modules/markdansi/README.md +167 -0
  62. package/node_modules/markdansi/dist/ast.d.ts +99 -0
  63. package/node_modules/markdansi/dist/ast.js +1 -0
  64. package/node_modules/markdansi/dist/cli.d.ts +17 -0
  65. package/node_modules/markdansi/dist/cli.js +208 -0
  66. package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
  67. package/node_modules/markdansi/dist/hyperlink.js +26 -0
  68. package/node_modules/markdansi/dist/index.d.ts +10 -0
  69. package/node_modules/markdansi/dist/index.js +16 -0
  70. package/node_modules/markdansi/dist/parser.d.ts +2 -0
  71. package/node_modules/markdansi/dist/parser.js +205 -0
  72. package/node_modules/markdansi/dist/render.d.ts +9 -0
  73. package/node_modules/markdansi/dist/render.js +680 -0
  74. package/node_modules/markdansi/dist/stream.d.ts +39 -0
  75. package/node_modules/markdansi/dist/stream.js +192 -0
  76. package/node_modules/markdansi/dist/theme.d.ts +18 -0
  77. package/node_modules/markdansi/dist/theme.js +105 -0
  78. package/node_modules/markdansi/dist/types.d.ts +58 -0
  79. package/node_modules/markdansi/dist/types.js +1 -0
  80. package/node_modules/markdansi/dist/wrap.d.ts +10 -0
  81. package/node_modules/markdansi/dist/wrap.js +73 -0
  82. package/node_modules/markdansi/docs/spec.md +110 -0
  83. package/node_modules/markdansi/package.json +78 -0
  84. package/node_modules/markdansi/tsconfig.json +18 -0
  85. package/node_modules/marked/LICENSE +44 -0
  86. package/node_modules/marked/README.md +115 -0
  87. package/node_modules/marked/bin/main.js +284 -0
  88. package/node_modules/marked/bin/marked.js +16 -0
  89. package/node_modules/marked/lib/marked.d.ts +760 -0
  90. package/node_modules/marked/lib/marked.esm.js +77 -0
  91. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  92. package/node_modules/marked/lib/marked.umd.js +79 -0
  93. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  94. package/node_modules/marked/man/marked.1 +113 -0
  95. package/node_modules/marked/man/marked.1.md +93 -0
  96. package/node_modules/marked/package.json +103 -0
  97. package/node_modules/slice-ansi/index.d.ts +19 -0
  98. package/node_modules/slice-ansi/index.js +317 -0
  99. package/node_modules/slice-ansi/license +10 -0
  100. package/node_modules/slice-ansi/package.json +59 -0
  101. package/node_modules/slice-ansi/readme.md +55 -0
  102. package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
  103. package/node_modules/string-width/index.d.ts +39 -0
  104. package/node_modules/string-width/index.js +203 -0
  105. package/node_modules/string-width/license +9 -0
  106. package/node_modules/string-width/package.json +65 -0
  107. package/node_modules/string-width/readme.md +66 -0
  108. package/node_modules/strip-ansi/index.d.ts +15 -0
  109. package/node_modules/strip-ansi/index.js +19 -0
  110. package/node_modules/strip-ansi/license +9 -0
  111. package/node_modules/strip-ansi/package.json +59 -0
  112. package/node_modules/strip-ansi/readme.md +37 -0
  113. package/node_modules/supports-color/browser.d.ts +1 -0
  114. package/node_modules/supports-color/browser.js +35 -0
  115. package/node_modules/supports-color/index.d.ts +55 -0
  116. package/node_modules/supports-color/index.js +202 -0
  117. package/node_modules/supports-color/license +9 -0
  118. package/node_modules/supports-color/package.json +64 -0
  119. package/node_modules/supports-color/readme.md +75 -0
  120. package/node_modules/supports-hyperlinks/browser.js +10 -0
  121. package/node_modules/supports-hyperlinks/index.d.ts +25 -0
  122. package/node_modules/supports-hyperlinks/index.js +163 -0
  123. package/node_modules/supports-hyperlinks/license +10 -0
  124. package/node_modules/supports-hyperlinks/package.json +47 -0
  125. package/node_modules/supports-hyperlinks/readme.md +45 -0
  126. package/package.json +41 -10
  127. package/patches/markdansi@0.3.2.patch +22 -2
  128. package/src/agent/context-builder.ts +24 -10
  129. package/src/agent/context-meter.ts +89 -5
  130. package/src/agent/loop.ts +107 -10
  131. package/src/agent/runtime-session.ts +1437 -82
  132. package/src/agent/session-ledger.ts +169 -29
  133. package/src/agent/types.ts +12 -1
  134. package/src/agent/user-prompt-projection.ts +62 -0
  135. package/src/cli/config.ts +57 -14
  136. package/src/cli/model-profiles.ts +124 -0
  137. package/src/cli/run-runner.ts +4 -1
  138. package/src/cli/tui-runner.tsx +68 -30
  139. package/src/context/compiled-context-hash.ts +1 -1
  140. package/src/context/compiled-context-validator.ts +92 -15
  141. package/src/context/context-automation-policy.ts +121 -0
  142. package/src/context/context-manager.ts +390 -11
  143. package/src/context/context-policy.ts +8 -0
  144. package/src/context/context-protocol-validator.ts +11 -1
  145. package/src/context/context-revision-compiler.ts +190 -27
  146. package/src/context/context-revision.ts +105 -19
  147. package/src/context/context-surface.ts +329 -0
  148. package/src/context/prefix-retirement-planner.ts +570 -0
  149. package/src/context/protocol-frame.ts +30 -3
  150. package/src/context/recall-retirement-contract.ts +27 -0
  151. package/src/context/swap-planner.ts +34 -10
  152. package/src/events/observation-text-log.ts +3 -3
  153. package/src/events/stdout-event-printer.ts +39 -3
  154. package/src/events/types.ts +172 -35
  155. package/src/ids/runtime-id.ts +6 -0
  156. package/src/ids/uuid-v7.ts +7 -0
  157. package/src/image/image-asset-store.ts +319 -0
  158. package/src/image/image-input-policy.ts +14 -0
  159. package/src/image/image-probe.ts +233 -0
  160. package/src/image/image-types.ts +262 -0
  161. package/src/instructions/project-instructions.ts +4 -1
  162. package/src/mcp/mcp-manager.ts +83 -19
  163. package/src/model/fake-model-client.ts +9 -1
  164. package/src/model/input-token-estimator.ts +25 -0
  165. package/src/model/model-client.ts +60 -0
  166. package/src/model/model-request-preflight.ts +1 -0
  167. package/src/model/moonshot-input-token-estimator.ts +107 -0
  168. package/src/model/openai-chat-mapping.ts +77 -1
  169. package/src/model/openai-chat-model-client.ts +389 -59
  170. package/src/model/openai-chat-stream.ts +272 -0
  171. package/src/model/prompt-prefix-hash.ts +2 -4
  172. package/src/model/token-estimator.ts +4 -0
  173. package/src/observation/observation-builder.ts +29 -0
  174. package/src/session/resume-projection.ts +94 -34
  175. package/src/session/session-catalog.ts +4 -4
  176. package/src/session/session-errors.ts +1 -1
  177. package/src/session/session-history-reader.ts +151 -4
  178. package/src/session/session-last-response-reader.ts +189 -0
  179. package/src/session/session-schema.ts +349 -35
  180. package/src/session/session-store.ts +3266 -255
  181. package/src/session/sqlite-session-ledger.ts +9 -4
  182. package/src/skills/skill-catalog.ts +182 -0
  183. package/src/skills/skill-context.ts +364 -0
  184. package/src/skills/skill-loader.ts +779 -0
  185. package/src/skills/skill-tool.ts +297 -0
  186. package/src/tools/edit.ts +51 -64
  187. package/src/tools/ensure-parent-directory.ts +6 -0
  188. package/src/tools/read.ts +2 -2
  189. package/src/tools/recall.ts +43 -40
  190. package/src/tools/registry.ts +24 -3
  191. package/src/tools/ripgrep.ts +3 -2
  192. package/src/tools/types.ts +43 -4
  193. package/src/tools/write.ts +20 -31
  194. package/src/tui/app.tsx +333 -96
  195. package/src/tui/clipboard.ts +5 -0
  196. package/src/tui/components/mcp-panel.tsx +28 -0
  197. package/src/tui/components/prompt-input.tsx +790 -257
  198. package/src/tui/components/skills-panel.tsx +31 -0
  199. package/src/tui/components/timeline.tsx +30 -1
  200. package/src/tui/event-store.ts +85 -2
  201. package/src/tui/file-mention.ts +234 -0
  202. package/src/tui/line-editor.ts +1 -0
  203. package/src/tui/project-slash-commands.ts +229 -0
  204. package/src/tui/prompt-draft.ts +510 -0
  205. package/src/tui/prompt-history.ts +294 -22
  206. package/src/tui/slash-commands.ts +46 -2
  207. package/src/tui/tui-session-controller.ts +110 -48
  208. package/src/tui/workspace-file-search.ts +94 -0
@@ -15,15 +15,35 @@ import {
15
15
  type SessionId,
16
16
  } from "../ids/runtime-id";
17
17
  import { loadMcpConfig } from "../mcp/mcp-config";
18
- import { createMcpManager, type McpManager } from "../mcp/mcp-manager";
19
- import type { ModelClient } from "../model/model-client";
18
+ import {
19
+ createMcpManager,
20
+ type McpInventorySnapshot,
21
+ type McpManager,
22
+ } from "../mcp/mcp-manager";
23
+ import {
24
+ materializeModelRequest,
25
+ ModelRequestMediaAggregateError,
26
+ type MaterializedModelRequest,
27
+ type ModelClient,
28
+ } from "../model/model-client";
29
+ import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
30
+ import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
31
+ import {
32
+ validateUserMessage,
33
+ type ImageAssetRef,
34
+ type UserMessage,
35
+ } from "../image/image-types";
36
+ import { projectUserMessage } from "./user-prompt-projection";
20
37
  import { CommittedPrefixAuditor } from "../model/committed-prefix-auditor";
21
38
  import { SwapPlanner } from "../context/swap-planner";
22
39
  import {
40
+ commitAgentSkillsContextUpdate,
23
41
  ContextManager,
24
42
  ContextManagerError,
25
43
  type ContextCompactionResult,
26
44
  type ContextCompactionTrigger,
45
+ type ContextRetirementResult,
46
+ type ContextRetirementTrigger,
27
47
  } from "../context/context-manager";
28
48
  import {
29
49
  assertMatchingContextBudget,
@@ -33,20 +53,45 @@ import {
33
53
  import { ObservationBuilder } from "../observation/observation-builder";
34
54
  import { ContextProtocolError } from "../context/context-protocol-validator";
35
55
  import { CompiledContextError } from "../context/compiled-context-validator";
36
- import { ContextRevisionError } from "../context/context-revision-compiler";
56
+ import {
57
+ ContextRevisionCompiler,
58
+ ContextRevisionError,
59
+ } from "../context/context-revision-compiler";
60
+ import {
61
+ changedContextSurfaceComponents,
62
+ contextSurfaceChangeManifestHash,
63
+ contextSurfaceChanges,
64
+ createContextSurface,
65
+ sameContextSurface,
66
+ type ContextSurfaceComponent,
67
+ type StoredContextSurfaceV8,
68
+ } from "../context/context-surface";
69
+ import {
70
+ canonicalSequenceHash,
71
+ renderedMessageHash,
72
+ } from "../context/compiled-context-hash";
37
73
  import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
38
74
  import type { Refiner } from "../tools/web-fetch/refiner";
39
75
  import type { ProjectInstructionManifest } from "../instructions/project-instructions";
40
76
  import {
41
77
  SessionStore,
42
- createRuntimeContract,
78
+ createSessionCompatibilityContract,
43
79
  type SessionRecoveryResult,
80
+ type StoredSkillActivation,
44
81
  } from "../session/session-store";
45
82
  import { SqliteSessionLedger } from "../session/sqlite-session-ledger";
46
83
  import { SessionError } from "../session/session-errors";
47
84
  import { FatalAgentTurnError, runAgent, type RunAgentInput } from "./loop";
48
- import { SessionLedgerWriteError, type SessionLedger } from "./session-ledger";
85
+ import {
86
+ AdmissionStaleError,
87
+ SessionLedgerWriteError,
88
+ type AdmissionBaseToken,
89
+ type SessionLedger,
90
+ } from "./session-ledger";
49
91
  import { TurnCancelledError } from "./turn-cancellation";
92
+ import type { ToolCompletionInput } from "../context/protocol-frame";
93
+ import type { BuiltContextRequest } from "../context/context-revision";
94
+ import type { CommittedToolCompletion } from "./session-ledger";
50
95
  import type {
51
96
  IterationIdentity,
52
97
  RunAgentResult,
@@ -54,12 +99,35 @@ import type {
54
99
  TurnIdentity,
55
100
  } from "./types";
56
101
  import { ContextMeter } from "./context-meter";
102
+ import { CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION } from "../context/recall-retirement-contract";
103
+ import {
104
+ selectContextAutomation,
105
+ type ContextAutomationDecision,
106
+ } from "../context/context-automation-policy";
107
+ import type { SkillCatalogSnapshot } from "../skills/skill-loader";
108
+ import {
109
+ activeSkillManifestEntry,
110
+ createSkillCatalogSnapshot,
111
+ skillCatalogManifest,
112
+ } from "../skills/skill-catalog";
113
+ import {
114
+ buildActiveSystemPrompt,
115
+ rebindActiveSkills,
116
+ renderSkillActivationReceipt,
117
+ SkillActivationCoordinator,
118
+ } from "../skills/skill-context";
57
119
 
58
120
  export type ExecuteTurnInput = {
59
- userPrompt: string;
121
+ userMessage: UserMessage;
60
122
  signal: AbortSignal;
61
123
  };
62
124
 
125
+ export type AcceptedTurn = {
126
+ readonly turnId: TurnIdentity["turnId"];
127
+ readonly userMessage: UserMessage;
128
+ readonly completion: Promise<RunAgentResult>;
129
+ };
130
+
63
131
  export type SessionDisposeReason =
64
132
  | { type: "oneshot_complete" }
65
133
  | { type: "tui_exit" }
@@ -71,12 +139,41 @@ export type RuntimeSession = {
71
139
  readonly sessionId: SessionId;
72
140
  readonly resumed: boolean;
73
141
  readonly recovery: SessionRecoveryResult;
142
+ skills(): RuntimeSkillsSnapshot;
143
+ mcp(): McpInventorySnapshot;
144
+ supportsImageInput(): boolean;
145
+ importImage(
146
+ sourcePath: string,
147
+ signal: AbortSignal,
148
+ prospectiveMessageImageCount: number,
149
+ ): Promise<ImportedImageAsset>;
150
+ verifyImageAssets(
151
+ assets: readonly ImageAssetRef[],
152
+ signal: AbortSignal,
153
+ ): Promise<void>;
154
+ admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn>;
74
155
  executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
75
156
  compactContext(): Promise<ContextCompactionResult>;
157
+ retireContext(): Promise<ContextRetirementResult>;
158
+ cloneSession(targetSessionId: SessionId): Promise<void>;
76
159
  canSwitchSession(): boolean;
77
160
  dispose(reason: SessionDisposeReason): Promise<void>;
78
161
  };
79
162
 
163
+ const EMPTY_MCP_INVENTORY: McpInventorySnapshot = Object.freeze({
164
+ servers: Object.freeze([]),
165
+ });
166
+
167
+ export type RuntimeSkillsSnapshot = {
168
+ readonly skills: readonly {
169
+ readonly name: string;
170
+ readonly description: string;
171
+ readonly scope: "project" | "user";
172
+ readonly active: boolean;
173
+ }[];
174
+ readonly shadowedNames: readonly string[];
175
+ };
176
+
80
177
  export type RuntimeSessionContext = {
81
178
  readonly sessionId: SessionId;
82
179
  createIteration(turn: TurnIdentity, iterationNumber: number): IterationIdentity;
@@ -86,6 +183,32 @@ export type RuntimeSessionContext = {
86
183
  ): ToolCallIdentity;
87
184
  finishIterationForContinuation(iteration: IterationIdentity): void;
88
185
  append(input: AgentEventInput): Promise<void>;
186
+ onToolCompletionsCommitted?(input: {
187
+ completions: readonly ToolCompletionInput[];
188
+ committed: readonly CommittedToolCompletion[];
189
+ }): void;
190
+ prepareModelDispatch?(input: {
191
+ iteration: IterationIdentity;
192
+ built: BuiltContextRequest;
193
+ }): void;
194
+ };
195
+
196
+ export type ContextSurfaceRefreshSummary = {
197
+ readonly previousRevisionNumber: number;
198
+ readonly revisionNumber: number;
199
+ readonly changed: readonly ContextSurfaceComponent[];
200
+ readonly toolCountBefore: number;
201
+ readonly toolCountAfter: number;
202
+ };
203
+
204
+ export type SkillsUpdateSummary = {
205
+ readonly previousRevisionNumber: number;
206
+ readonly revisionNumber: number;
207
+ readonly activated: readonly string[];
208
+ readonly refreshed: readonly string[];
209
+ readonly deactivated: readonly string[];
210
+ readonly unavailable: readonly string[];
211
+ readonly addedOverrideCount: number;
89
212
  };
90
213
 
91
214
  type CommonRuntimeSessionInput = {
@@ -97,6 +220,9 @@ type CommonRuntimeSessionInput = {
97
220
  contextProfile: ModelContextProfile;
98
221
  contextBudget: ModelContextBudget;
99
222
  modelClient: ModelClient;
223
+ systemPrompt: string;
224
+ projectInstruction?: ProjectInstructionManifest;
225
+ skillCatalog?: SkillCatalogSnapshot;
100
226
  presentationSinks?: EventSink[];
101
227
  persistence?:
102
228
  | false
@@ -109,8 +235,6 @@ type CommonRuntimeSessionInput = {
109
235
 
110
236
  type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
111
237
  selection: { mode: "new"; sessionId: SessionId };
112
- systemPrompt: string;
113
- projectInstruction?: ProjectInstructionManifest;
114
238
  };
115
239
 
116
240
  type ResumeRuntimeSessionInput = CommonRuntimeSessionInput & {
@@ -135,7 +259,11 @@ export type RuntimeSessionFactoryDependencies = {
135
259
  createEventSink: (input: CreateRuntimeSessionInput) => EventSink;
136
260
  selectShadowPlanning: NonNullable<RunAgentInput["shadowPlanning"]>["select"];
137
261
  onShadowPlanningResult?: NonNullable<RunAgentInput["shadowPlanning"]>["onResult"];
262
+ selectContextAutomation: typeof selectContextAutomation;
263
+ automaticCompactionTrigger: () => ContextCompactionTrigger;
264
+ automaticRetirementTrigger: () => ContextRetirementTrigger;
138
265
  manualCompactionTrigger: () => ContextCompactionTrigger;
266
+ manualRetirementTrigger: () => ContextRetirementTrigger;
139
267
  };
140
268
 
141
269
  export class RuntimeEventAppendError extends Error {
@@ -150,9 +278,11 @@ export class RuntimeEventAppendError extends Error {
150
278
 
151
279
  type RuntimeSessionState =
152
280
  | "initializing"
281
+ | "admitting"
153
282
  | "ready"
154
283
  | "executing"
155
284
  | "compacting"
285
+ | "maintaining_context"
156
286
  | "faulted"
157
287
  | "disposing"
158
288
  | "disposed";
@@ -162,6 +292,12 @@ type ActiveTurn = {
162
292
  completion: Promise<RunAgentResult>;
163
293
  };
164
294
 
295
+ type ActiveAdmission = {
296
+ controller: AbortController;
297
+ settled: Promise<void>;
298
+ settle: () => void;
299
+ };
300
+
165
301
  const defaultDependencies: RuntimeSessionFactoryDependencies = {
166
302
  idFactory: runtimeIdFactory,
167
303
  createTooling: createDefaultTooling,
@@ -186,7 +322,11 @@ const defaultDependencies: RuntimeSessionFactoryDependencies = {
186
322
  createEventSink,
187
323
  selectShadowPlanning: ({ preflight }) =>
188
324
  preflight.pressure === "normal" ? undefined : { trigger: "runtime_pressure" },
325
+ selectContextAutomation,
326
+ automaticCompactionTrigger: () => ({ kind: "runtime_pressure" }),
327
+ automaticRetirementTrigger: () => ({ kind: "runtime_pressure" }),
189
328
  manualCompactionTrigger: () => ({ kind: "manual" }),
329
+ manualRetirementTrigger: () => ({ kind: "manual" }),
190
330
  };
191
331
 
192
332
  class DefaultRuntimeSession implements RuntimeSession {
@@ -207,14 +347,21 @@ class DefaultRuntimeSession implements RuntimeSession {
207
347
  private tooling?: DefaultTooling;
208
348
  private mcpManager?: McpManager;
209
349
  private ledger?: SessionLedger;
350
+ private activeAdmission?: ActiveAdmission;
210
351
  private activeTurn?: ActiveTurn;
211
- private activeCompaction?: Promise<ContextCompactionResult>;
352
+ private activeContextRevision?: Promise<
353
+ ContextCompactionResult | ContextRetirementResult
354
+ >;
212
355
  private disposePromise?: Promise<void>;
213
356
  private faultCause?: unknown;
214
357
  private readonly contextMeter: ContextMeter;
215
358
  private readonly committedPrefixAuditor = new CommittedPrefixAuditor();
216
359
  private readonly shadowPlanner: SwapPlanner;
217
360
  private contextManager?: ContextManager;
361
+ private contextAutomationDecision?: ContextAutomationDecision;
362
+ private pendingAutomaticContextMaintenance = false;
363
+ private readonly skillCatalog: SkillCatalogSnapshot;
364
+ private skillCoordinator = new SkillActivationCoordinator();
218
365
 
219
366
  private readonly context: RuntimeSessionContext;
220
367
 
@@ -224,9 +371,18 @@ class DefaultRuntimeSession implements RuntimeSession {
224
371
  private readonly eventSink: EventSink,
225
372
  private readonly observationBuilder: ObservationBuilder,
226
373
  private readonly store: SessionStore,
374
+ private readonly assetStore: ImageAssetStore,
227
375
  ) {
228
376
  this.sessionId = input.selection.sessionId;
229
377
  this.resumed = input.selection.mode === "resume";
378
+ this.skillCatalog =
379
+ input.skillCatalog ??
380
+ createSkillCatalogSnapshot({
381
+ workspaceRoot: input.workspaceRoot,
382
+ homeRoot: input.workspaceRoot,
383
+ skills: [],
384
+ shadowed: [],
385
+ });
230
386
  this.nextTurnNumber = store.nextTurnNumber();
231
387
  this.contextMeter = new ContextMeter(input.contextBudget, {
232
388
  onMeasuredAnchor: (anchor) => store.writeMeasuredContextAnchor(anchor),
@@ -241,6 +397,9 @@ class DefaultRuntimeSession implements RuntimeSession {
241
397
  finishIterationForContinuation: (iteration) =>
242
398
  this.finishIterationForContinuation(iteration),
243
399
  append: (event) => this.append(event),
400
+ onToolCompletionsCommitted: (completion) =>
401
+ this.onToolCompletionsCommitted(completion),
402
+ prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
244
403
  };
245
404
  }
246
405
 
@@ -250,18 +409,28 @@ class DefaultRuntimeSession implements RuntimeSession {
250
409
  ): Promise<RuntimeSession> {
251
410
  validateCreateInput(input);
252
411
  const store = await dependencies.openStore(input, dependencies.idFactory);
412
+ let assetStore: ImageAssetStore;
413
+ try {
414
+ assetStore = await ImageAssetStore.open({ workspaceRoot: store.workspaceRoot });
415
+ } catch (error) {
416
+ if (isNewSessionInput(input)) {
417
+ await store
418
+ .deleteFromDisk()
419
+ .catch(() => store.abandon().catch(() => undefined));
420
+ } else {
421
+ await store.abandon().catch(() => undefined);
422
+ }
423
+ throw error;
424
+ }
253
425
  let session: DefaultRuntimeSession;
254
- let systemPrompt: string;
255
426
  try {
256
- systemPrompt = isNewSessionInput(input)
257
- ? input.systemPrompt
258
- : store.readStoredSystemPrompt();
259
427
  session = new DefaultRuntimeSession(
260
428
  input,
261
429
  dependencies,
262
430
  dependencies.createEventSink(input),
263
431
  dependencies.createObservationBuilder(),
264
432
  store,
433
+ assetStore,
265
434
  );
266
435
  } catch (error) {
267
436
  if (isNewSessionInput(input)) {
@@ -276,6 +445,63 @@ class DefaultRuntimeSession implements RuntimeSession {
276
445
  let started = false;
277
446
 
278
447
  try {
448
+ const compatibility = createSessionCompatibilityContract({
449
+ modelName: input.modelName,
450
+ profileName: input.profileName,
451
+ includeReasoningContent: input.includeReasoningContent,
452
+ contextProfile: input.contextProfile,
453
+ messageProtocol: input.modelClient.messageProtocol,
454
+ inputModalities: input.modelClient.inputModalities ?? ["text"],
455
+ ...(input.modelClient.inputTokenEstimator === undefined
456
+ ? {}
457
+ : {
458
+ tokenEstimator: input.modelClient.inputTokenEstimator.compatibility,
459
+ }),
460
+ });
461
+ if (input.selection.mode === "resume") {
462
+ store.assertSessionCompatibility(compatibility);
463
+ }
464
+
465
+ let resumeSkills:
466
+ | {
467
+ unresolved: readonly StoredSkillActivation[];
468
+ activated: readonly string[];
469
+ refreshed: readonly string[];
470
+ deactivated: readonly string[];
471
+ }
472
+ | undefined;
473
+ if (input.selection.mode === "resume") {
474
+ session.recovery = store.recoverInterruptedState(dependencies.idFactory);
475
+ const storedSurface = store.loadContextSnapshot().surface;
476
+ const unresolved = store.loadSkillActivations(["pending", "dispatched"]);
477
+ const promotionNames = unresolved
478
+ .filter((activation) => activation.state === "dispatched")
479
+ .map((activation) => ({
480
+ name: activation.name,
481
+ activationMessageId: activation.activationMessageId,
482
+ }));
483
+ const rebound = rebindActiveSkills({
484
+ catalog: session.skillCatalog,
485
+ active: storedSurface.activeSkills,
486
+ promotionNames,
487
+ });
488
+ session.skillCoordinator = new SkillActivationCoordinator({
489
+ active: rebound.active,
490
+ });
491
+ const activated = promotionNames
492
+ .filter((entry) => session.skillCatalog.skills.has(entry.name))
493
+ .map((entry) => entry.name)
494
+ .sort();
495
+ resumeSkills = {
496
+ unresolved,
497
+ activated: Object.freeze(activated),
498
+ refreshed: Object.freeze(
499
+ rebound.refreshed.filter((name) => !activated.includes(name)).sort(),
500
+ ),
501
+ deactivated: rebound.deactivated,
502
+ };
503
+ }
504
+
279
505
  if (isNewSessionInput(input)) {
280
506
  await session.append({
281
507
  type: "session.started",
@@ -305,6 +531,12 @@ class DefaultRuntimeSession implements RuntimeSession {
305
531
  runtimeSession: session.context,
306
532
  historyReader: store.historyReader(),
307
533
  webFetchRefiner: input.webFetchRefiner,
534
+ ...(session.skillCatalog.skills.size === 0
535
+ ? {}
536
+ : {
537
+ skillCatalog: session.skillCatalog,
538
+ skillCoordinator: session.skillCoordinator,
539
+ }),
308
540
  });
309
541
 
310
542
  const mcpConfig = await dependencies.loadMcpConfig(input.workspaceRoot);
@@ -319,25 +551,53 @@ class DefaultRuntimeSession implements RuntimeSession {
319
551
  }
320
552
 
321
553
  const definitions = session.requireTooling().registry.definitions();
322
- const contractPrepared = input.modelClient.prepare({
323
- messages: [{ role: "system", content: systemPrompt }],
554
+ const activeSystemPrompt = buildActiveSystemPrompt({
555
+ baseSystemPrompt: input.systemPrompt,
556
+ activeSkills: session.skillCoordinator.activeEntries(),
557
+ });
558
+ const surfacePrepared = input.modelClient.prepare({
559
+ messages: [{ role: "system", content: activeSystemPrompt }],
324
560
  tools: definitions,
325
561
  });
326
- const runtimeContract = createRuntimeContract({
327
- modelName: input.modelName,
328
- profileName: input.profileName,
329
- includeReasoningContent: input.includeReasoningContent,
330
- contextProfile: input.contextProfile,
331
- contextBudget: input.contextBudget,
332
- systemPrompt,
333
- toolSchemaSha256: contractPrepared.toolSchemaHash,
334
- requestConfigSha256: contractPrepared.requestConfigHash,
562
+ const candidateSurface = createContextSurface({
563
+ surfaceId: dependencies.idFactory.createContextSurfaceId(),
564
+ sessionId: session.sessionId,
565
+ systemPrompt: activeSystemPrompt,
566
+ recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
567
+ ...(input.projectInstruction === undefined
568
+ ? {}
569
+ : { projectInstruction: input.projectInstruction }),
570
+ skillCatalog: skillCatalogManifest(session.skillCatalog.skills.values()),
571
+ activeSkills: session.skillCoordinator.activeManifest(),
572
+ toolDefinitions: definitions,
573
+ prepared: surfacePrepared,
574
+ createdAt: new Date().toISOString(),
335
575
  });
336
576
  if (input.selection.mode === "new") {
337
- store.finalizeRuntimeContract(runtimeContract);
577
+ store.finalizeInitialization({
578
+ contract: compatibility,
579
+ surface: candidateSurface,
580
+ revisionId: dependencies.idFactory.createContextRevisionId(),
581
+ });
338
582
  } else {
339
- store.assertRuntimeContract(runtimeContract);
340
- session.recovery = store.recoverInterruptedState(dependencies.idFactory);
583
+ if (resumeSkills === undefined) {
584
+ throw new Error("Resume Agent Skills staging state is missing.");
585
+ }
586
+ let skillsUpdate: SkillsUpdateSummary | undefined;
587
+ const refresh =
588
+ resumeSkills.unresolved.length === 0
589
+ ? await session.refreshContextSurface(candidateSurface)
590
+ : undefined;
591
+ if (resumeSkills.unresolved.length > 0) {
592
+ skillsUpdate = await session.commitSkillSettlements({
593
+ reason: "resume",
594
+ candidateSurface,
595
+ unresolved: resumeSkills.unresolved,
596
+ activated: resumeSkills.activated,
597
+ refreshed: resumeSkills.refreshed,
598
+ deactivated: resumeSkills.deactivated,
599
+ });
600
+ }
341
601
  const openCount = store.markResumed();
342
602
  if (
343
603
  session.recovery.recoveredTurnId !== undefined &&
@@ -353,22 +613,58 @@ class DefaultRuntimeSession implements RuntimeSession {
353
613
  },
354
614
  });
355
615
  }
356
- const projectInstruction = store.readProjectInstructionManifest();
357
616
  await session.append({
358
617
  type: "session.resumed",
359
618
  sessionId: session.sessionId,
360
619
  data: {
361
620
  openCount,
362
621
  ...session.recovery,
363
- ...(projectInstruction === undefined
622
+ contextProfile: input.contextProfile,
623
+ contextBudget: input.contextBudget,
624
+ ...(input.projectInstruction === undefined
364
625
  ? {}
365
626
  : {
366
- projectInstructionFile: projectInstruction.path,
627
+ projectInstructionFile: input.projectInstruction.path,
367
628
  }),
629
+ ...(refresh === undefined ? {} : { contextRefresh: refresh }),
368
630
  },
369
631
  });
632
+ if (skillsUpdate !== undefined) {
633
+ await session.append({
634
+ type: "skills.updated",
635
+ sessionId: session.sessionId,
636
+ data: {
637
+ reason: "resume",
638
+ activated: skillsUpdate.activated,
639
+ refreshed: skillsUpdate.refreshed,
640
+ deactivated: skillsUpdate.deactivated,
641
+ unavailable: skillsUpdate.unavailable,
642
+ revisionNumber: skillsUpdate.revisionNumber,
643
+ },
644
+ });
645
+ } else if (
646
+ resumeSkills.refreshed.length > 0 ||
647
+ resumeSkills.deactivated.length > 0
648
+ ) {
649
+ await session.append({
650
+ type: "skills.updated",
651
+ sessionId: session.sessionId,
652
+ data: {
653
+ reason: "resume",
654
+ activated: [],
655
+ refreshed: resumeSkills.refreshed,
656
+ deactivated: resumeSkills.deactivated,
657
+ unavailable: [],
658
+ ...(refresh === undefined
659
+ ? {}
660
+ : { revisionNumber: refresh.revisionNumber }),
661
+ },
662
+ });
663
+ }
370
664
  }
371
665
 
666
+ await session.appendSkillsCatalogLoaded();
667
+
372
668
  session.ledger = dependencies.createLedger(store, dependencies.idFactory);
373
669
  session.contextManager = new ContextManager({
374
670
  store,
@@ -389,6 +685,7 @@ class DefaultRuntimeSession implements RuntimeSession {
389
685
  .requireLedger()
390
686
  .buildCommittedModelRequest(definitions);
391
687
  const initialPrepared = input.modelClient.prepare(initialBuilt.request);
688
+ assertPreparedMatchesSurface(initialPrepared, initialBuilt.surface);
392
689
  session.committedPrefixAuditor.audit(
393
690
  initialBuilt.compiled.revisionId,
394
691
  initialPrepared,
@@ -409,6 +706,20 @@ class DefaultRuntimeSession implements RuntimeSession {
409
706
  data: { phase: "initial", snapshot: initialSnapshot },
410
707
  });
411
708
 
709
+ session.contextAutomationDecision = dependencies.selectContextAutomation({
710
+ ...(input.profileName === undefined ? {} : { profileName: input.profileName }),
711
+ surface: store.loadContextSnapshot().surface,
712
+ });
713
+ if (
714
+ input.selection.mode === "resume" &&
715
+ initialSnapshot.pressure !== "normal" &&
716
+ session.contextAutomationDecision.automaticSwapOnly
717
+ ) {
718
+ session.pendingAutomaticContextMaintenance = true;
719
+ session.state = "executing";
720
+ await session.performAutomaticContextMaintenance();
721
+ }
722
+
412
723
  session.state = "ready";
413
724
  return session;
414
725
  } catch (error) {
@@ -416,58 +727,638 @@ class DefaultRuntimeSession implements RuntimeSession {
416
727
  }
417
728
  }
418
729
 
419
- executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
730
+ private async refreshContextSurface(
731
+ candidateSurface: StoredContextSurfaceV8,
732
+ ): Promise<ContextSurfaceRefreshSummary | undefined> {
733
+ const snapshot = this.store.loadContextSnapshot();
734
+ if (sameContextSurface(snapshot.surface, candidateSurface)) {
735
+ return undefined;
736
+ }
737
+
738
+ const changes = contextSurfaceChanges(snapshot.surface, candidateSurface);
739
+ const changed = changedContextSurfaceComponents(changes);
740
+ if (changed.length === 0) {
741
+ throw new Error("Changed context surface has an empty change manifest.");
742
+ }
743
+ const startedAt = performance.now();
744
+ await this.append({
745
+ type: "context.revision.started",
746
+ sessionId: this.sessionId,
747
+ data: {
748
+ strategy: "surface_refresh",
749
+ reason: "resume",
750
+ baseRevisionNumber: snapshot.revision.revisionNumber,
751
+ changed,
752
+ },
753
+ });
754
+
755
+ let stage: "prepare" | "commit" | "activate" = "prepare";
756
+ let committed = false;
757
+ try {
758
+ const compiler = new ContextRevisionCompiler();
759
+ const active = compiler.compileActive(snapshot);
760
+ const candidateCompiled = compiler.compileProspective({
761
+ active,
762
+ canonical: snapshot.canonical,
763
+ activeOverrides: snapshot.activeOverrides,
764
+ addedOverrides: [],
765
+ activeSurface: snapshot.surface,
766
+ surface: candidateSurface,
767
+ });
768
+ const prepared = this.input.modelClient.prepare({
769
+ messages: candidateCompiled.entries.map((entry) => entry.message),
770
+ tools: [...candidateSurface.toolDefinitions],
771
+ });
772
+ assertPreparedMatchesSurface(prepared, candidateSurface);
773
+
774
+ stage = "commit";
775
+ const revision = this.store.commitSurfaceRefresh({
776
+ revisionId: this.dependencies.idFactory.createContextRevisionId(),
777
+ expectedBaseRevisionId: snapshot.revision.revisionId,
778
+ expectedBaseRevisionNumber: snapshot.revision.revisionNumber,
779
+ expectedCanonicalThroughOrdinal: snapshot.canonical.messages.length,
780
+ expectedBaseActiveOverrideManifestSha256:
781
+ snapshot.revision.activeOverrideManifestSha256,
782
+ surface: candidateSurface,
783
+ changes,
784
+ changeManifestSha256: contextSurfaceChangeManifestHash(changes),
785
+ canonicalSequenceSha256: canonicalSequenceHash(snapshot.canonical),
786
+ renderedMessageSha256: renderedMessageHash(candidateCompiled.entries),
787
+ });
788
+ committed = true;
789
+
790
+ stage = "activate";
791
+ this.contextMeter.startRevision({
792
+ reason: "context_rebuilt",
793
+ requestConfigHash: prepared.requestConfigHash,
794
+ toolSchemaHash: prepared.toolSchemaHash,
795
+ });
796
+ const summary = Object.freeze({
797
+ previousRevisionNumber: snapshot.revision.revisionNumber,
798
+ revisionNumber: revision.revisionNumber,
799
+ changed,
800
+ toolCountBefore: snapshot.surface.toolDefinitions.length,
801
+ toolCountAfter: candidateSurface.toolDefinitions.length,
802
+ });
803
+ await this.append({
804
+ type: "context.revision.finished",
805
+ sessionId: this.sessionId,
806
+ data: {
807
+ strategy: "surface_refresh",
808
+ reason: "resume",
809
+ baseRevisionNumber: summary.previousRevisionNumber,
810
+ revisionNumber: summary.revisionNumber,
811
+ changed: summary.changed,
812
+ toolCountBefore: summary.toolCountBefore,
813
+ toolCountAfter: summary.toolCountAfter,
814
+ measuredAnchorCleared: true,
815
+ durationMs: elapsedMs(startedAt),
816
+ },
817
+ });
818
+ return summary;
819
+ } catch (error) {
820
+ await this.append({
821
+ type: "context.revision.failed",
822
+ sessionId: this.sessionId,
823
+ data: {
824
+ strategy: "surface_refresh",
825
+ reason: "resume",
826
+ stage,
827
+ errorCode: boundedContextErrorCode(
828
+ error instanceof SessionError
829
+ ? error.code
830
+ : error instanceof Error
831
+ ? error.name
832
+ : "CONTEXT_SURFACE_REFRESH_FAILED",
833
+ ),
834
+ error: `Context surface refresh failed at ${stage}.`,
835
+ committed,
836
+ },
837
+ }).catch(() => undefined);
838
+ throw error;
839
+ }
840
+ }
841
+
842
+ skills(): RuntimeSkillsSnapshot {
843
+ const activeNames = new Set(
844
+ this.skillCoordinator.activeEntries().map((entry) => entry.skill.name),
845
+ );
846
+ return Object.freeze({
847
+ skills: Object.freeze(
848
+ [...this.skillCatalog.skills.values()]
849
+ .sort((left, right) => compareText(left.name, right.name))
850
+ .map((skill) =>
851
+ Object.freeze({
852
+ name: skill.name,
853
+ description: skill.description,
854
+ scope: skill.scope,
855
+ active: activeNames.has(skill.name),
856
+ }),
857
+ ),
858
+ ),
859
+ shadowedNames: Object.freeze(
860
+ this.skillCatalog.shadowed.map((entry) => entry.name),
861
+ ),
862
+ });
863
+ }
864
+
865
+ mcp(): McpInventorySnapshot {
866
+ return this.mcpManager?.inventory ?? EMPTY_MCP_INVENTORY;
867
+ }
868
+
869
+ supportsImageInput(): boolean {
870
+ return this.input.modelClient.inputModalities?.includes("image") === true;
871
+ }
872
+
873
+ importImage(
874
+ sourcePath: string,
875
+ signal: AbortSignal,
876
+ prospectiveMessageImageCount: number,
877
+ ): Promise<ImportedImageAsset> {
420
878
  if (this.state !== "ready") {
421
- throw new Error(`Cannot execute a turn while RuntimeSession is ${this.state}.`);
879
+ throw new Error(`Cannot import an image while RuntimeSession is ${this.state}.`);
422
880
  }
423
- if (input.userPrompt.trim() === "") {
424
- throw new Error("Cannot execute a turn with an empty prompt.");
881
+ if (!this.supportsImageInput()) {
882
+ throw new Error("Current model profile does not support image input.");
425
883
  }
426
- if (this.activeTurn !== undefined) {
427
- throw new Error("Cannot execute concurrent turns in one RuntimeSession.");
884
+ if (
885
+ !Number.isSafeInteger(prospectiveMessageImageCount) ||
886
+ prospectiveMessageImageCount < 1 ||
887
+ prospectiveMessageImageCount > IMAGE_INPUT_POLICY.maxImagesPerMessage
888
+ ) {
889
+ throw new Error("Prospective Prompt image count is invalid.");
890
+ }
891
+ const activeImageCount = this.input.modelClient.prepare(
892
+ this.requireLedger().buildCommittedModelRequest(
893
+ this.requireTooling().registry.definitions(),
894
+ ).request,
895
+ ).mediaOccurrenceCount;
896
+ const aggregateImageCount = activeImageCount + prospectiveMessageImageCount;
897
+ if (aggregateImageCount > IMAGE_INPUT_POLICY.maxImagesPerRequest) {
898
+ throw new ModelRequestMediaAggregateError(
899
+ `Model request would have ${aggregateImageCount} images; maximum is ${IMAGE_INPUT_POLICY.maxImagesPerRequest}.`,
900
+ );
901
+ }
902
+ return this.assetStore.importWorkspaceFile(sourcePath, { signal });
903
+ }
904
+
905
+ async verifyImageAssets(
906
+ assets: readonly ImageAssetRef[],
907
+ signal: AbortSignal,
908
+ ): Promise<void> {
909
+ if (!this.supportsImageInput()) {
910
+ throw new Error("Current model profile does not support image input.");
911
+ }
912
+ for (const asset of assets) {
913
+ signal.throwIfAborted();
914
+ await this.assetStore.readVerified(asset, { signal });
915
+ }
916
+ }
917
+
918
+ private requireContextAutomation(): ContextAutomationDecision {
919
+ if (this.contextAutomationDecision === undefined) {
920
+ throw new Error("RuntimeSession context automation is not initialized.");
921
+ }
922
+ return this.contextAutomationDecision;
923
+ }
924
+
925
+ private appendSkillsCatalogLoaded(): Promise<void> {
926
+ const activeNames = this.skillCoordinator
927
+ .activeEntries()
928
+ .map((entry) => entry.skill.name);
929
+ if (
930
+ this.skillCatalog.skills.size === 0 &&
931
+ activeNames.length === 0 &&
932
+ this.skillCatalog.shadowed.length === 0
933
+ ) {
934
+ return Promise.resolve();
935
+ }
936
+ const skills = [...this.skillCatalog.skills.values()];
937
+ return this.append({
938
+ type: "skills.catalog.loaded",
939
+ sessionId: this.sessionId,
940
+ data: {
941
+ availableCount: skills.length,
942
+ projectCount: skills.filter((skill) => skill.scope === "project").length,
943
+ userCount: skills.filter((skill) => skill.scope === "user").length,
944
+ activeNames: Object.freeze(activeNames),
945
+ shadowedNames: Object.freeze(
946
+ this.skillCatalog.shadowed.map((entry) => entry.name),
947
+ ),
948
+ },
949
+ });
950
+ }
951
+
952
+ private onToolCompletionsCommitted(input: {
953
+ completions: readonly ToolCompletionInput[];
954
+ committed: readonly CommittedToolCompletion[];
955
+ }): void {
956
+ if (input.completions.length !== input.committed.length) {
957
+ throw new Error("Committed tool completion identity count does not match.");
428
958
  }
959
+ for (let index = 0; index < input.completions.length; index += 1) {
960
+ const completion = input.completions[index];
961
+ const committed = input.committed[index];
962
+ if (
963
+ completion === undefined ||
964
+ committed === undefined ||
965
+ completion.call.toolCallId !== committed.toolCallId
966
+ ) {
967
+ throw new Error("Committed tool completion identity is invalid.");
968
+ }
969
+ if (
970
+ completion.kind === "returned" &&
971
+ completion.raw.kind === "skill" &&
972
+ completion.raw.ok &&
973
+ completion.raw.status === "loaded"
974
+ ) {
975
+ this.skillCoordinator.markPending(completion.raw.name);
976
+ }
977
+ }
978
+ }
429
979
 
430
- let admissionPrepared;
980
+ private prepareModelDispatch(input: {
981
+ iteration: IterationIdentity;
982
+ built: BuiltContextRequest;
983
+ }): void {
984
+ const pending = this.store.loadSkillActivations(["pending"]);
985
+ if (pending.length === 0) {
986
+ return;
987
+ }
988
+ const visibleCanonicalMessageIds = new Set(
989
+ input.built.compiled.entries
990
+ .filter(
991
+ (entry) =>
992
+ entry.representation === "canonical" && entry.message.role === "tool",
993
+ )
994
+ .map((entry) => entry.messageId),
995
+ );
996
+ const included = pending.filter((activation) =>
997
+ visibleCanonicalMessageIds.has(activation.activationMessageId),
998
+ );
999
+ if (included.length === 0) {
1000
+ return;
1001
+ }
1002
+ const dispatched = this.store.markSkillActivationsDispatched({
1003
+ iterationId: input.iteration.iterationId,
1004
+ activationMessageIds: included.map(
1005
+ (activation) => activation.activationMessageId,
1006
+ ),
1007
+ });
1008
+ this.skillCoordinator.markDispatched(
1009
+ dispatched.map((activation) => activation.name),
1010
+ );
1011
+ }
1012
+
1013
+ private async commitSkillSettlements(input: {
1014
+ reason: "activation" | "resume";
1015
+ unresolved: readonly StoredSkillActivation[];
1016
+ candidateSurface?: StoredContextSurfaceV8;
1017
+ activated?: readonly string[];
1018
+ refreshed?: readonly string[];
1019
+ deactivated?: readonly string[];
1020
+ }): Promise<SkillsUpdateSummary> {
1021
+ if (input.unresolved.length === 0) {
1022
+ throw new Error("Agent Skills update requires unresolved activations.");
1023
+ }
1024
+ const snapshot = this.store.loadContextSnapshot();
1025
+ const canonicalMessages = new Map(
1026
+ snapshot.canonical.messages.map((message) => [message.messageId, message]),
1027
+ );
1028
+ const activeByName = new Map(
1029
+ this.skillCoordinator
1030
+ .activeEntries()
1031
+ .map((entry) => [entry.skill.name, entry] as const),
1032
+ );
1033
+ const activated = new Set(input.activated ?? []);
1034
+ const unavailable = new Set<string>();
1035
+ const settlements: Array<{
1036
+ activationMessageId: StoredSkillActivation["activationMessageId"];
1037
+ name: string;
1038
+ state: "promoted" | "rejected";
1039
+ rejectionReason?: string;
1040
+ }> = [];
1041
+ const receipts = [];
1042
+ for (const activation of [...input.unresolved].sort((left, right) =>
1043
+ compareText(left.name, right.name),
1044
+ )) {
1045
+ const skill = this.skillCatalog.skills.get(activation.name);
1046
+ const canPromote = activation.state === "dispatched" && skill !== undefined;
1047
+ if (canPromote) {
1048
+ const existing = activeByName.get(activation.name);
1049
+ if (
1050
+ existing !== undefined &&
1051
+ existing.activationMessageId !== activation.activationMessageId
1052
+ ) {
1053
+ throw new Error(
1054
+ `Agent Skill ${activation.name} already has another active activation.`,
1055
+ );
1056
+ }
1057
+ activeByName.set(activation.name, {
1058
+ skill,
1059
+ activationMessageId: activation.activationMessageId,
1060
+ });
1061
+ activated.add(activation.name);
1062
+ }
1063
+ const state = canPromote ? "promoted" : "rejected";
1064
+ const rejectionReason =
1065
+ state === "promoted"
1066
+ ? undefined
1067
+ : activation.state === "pending"
1068
+ ? "not_dispatched"
1069
+ : "unavailable";
1070
+ if (rejectionReason === "unavailable") {
1071
+ unavailable.add(activation.name);
1072
+ }
1073
+ settlements.push({
1074
+ activationMessageId: activation.activationMessageId,
1075
+ name: activation.name,
1076
+ state,
1077
+ ...(rejectionReason === undefined ? {} : { rejectionReason }),
1078
+ });
1079
+ const message = canonicalMessages.get(activation.activationMessageId);
1080
+ if (message?.role !== "tool") {
1081
+ throw new Error(
1082
+ `Agent Skill activation message ${activation.activationMessageId} is missing.`,
1083
+ );
1084
+ }
1085
+ receipts.push(
1086
+ renderSkillActivationReceipt({
1087
+ message: {
1088
+ messageId: message.messageId,
1089
+ frameId: message.frameId,
1090
+ ordinal: message.ordinal,
1091
+ content: message.content,
1092
+ contentSha256: message.contentSha256,
1093
+ },
1094
+ name: activation.name,
1095
+ outcome:
1096
+ state === "promoted"
1097
+ ? "promoted"
1098
+ : rejectionReason === "unavailable"
1099
+ ? "unavailable"
1100
+ : "rejected",
1101
+ }),
1102
+ );
1103
+ }
1104
+ const nextActive = Object.freeze(
1105
+ [...activeByName.values()].sort((left, right) =>
1106
+ compareText(left.skill.name, right.skill.name),
1107
+ ),
1108
+ );
1109
+ const createdAt = new Date().toISOString();
1110
+ const definitions = this.requireTooling().registry.definitions();
1111
+ const renderedSystemPrompt = buildActiveSystemPrompt({
1112
+ baseSystemPrompt: this.input.systemPrompt,
1113
+ activeSkills: nextActive,
1114
+ });
1115
+ const surfacePrepared = this.input.modelClient.prepare({
1116
+ messages: [{ role: "system", content: renderedSystemPrompt }],
1117
+ tools: definitions,
1118
+ });
1119
+ const generatedSurface =
1120
+ input.candidateSurface ??
1121
+ createContextSurface({
1122
+ surfaceId: this.dependencies.idFactory.createContextSurfaceId(),
1123
+ sessionId: this.sessionId,
1124
+ systemPrompt: renderedSystemPrompt,
1125
+ recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
1126
+ ...(this.input.projectInstruction === undefined
1127
+ ? {}
1128
+ : { projectInstruction: this.input.projectInstruction }),
1129
+ skillCatalog: skillCatalogManifest(this.skillCatalog.skills.values()),
1130
+ activeSkills: nextActive.map((entry) =>
1131
+ activeSkillManifestEntry(entry.skill, entry.activationMessageId),
1132
+ ),
1133
+ toolDefinitions: definitions,
1134
+ prepared: surfacePrepared,
1135
+ createdAt,
1136
+ });
1137
+ assertPreparedMatchesSurface(surfacePrepared, generatedSurface);
1138
+ const surface = sameContextSurface(snapshot.surface, generatedSurface)
1139
+ ? snapshot.surface
1140
+ : generatedSurface;
1141
+ const startedAt = performance.now();
1142
+ await this.append({
1143
+ type: "context.revision.started",
1144
+ sessionId: this.sessionId,
1145
+ data: {
1146
+ strategy: "skills_update",
1147
+ reason: input.reason,
1148
+ baseRevisionNumber: snapshot.revision.revisionNumber,
1149
+ names: Object.freeze(
1150
+ input.unresolved.map((entry) => entry.name).sort(compareText),
1151
+ ),
1152
+ },
1153
+ });
1154
+ let stage: "prepare" | "commit" | "activate" = "prepare";
1155
+ let committed = false;
431
1156
  try {
432
- admissionPrepared = this.input.modelClient.prepare(
433
- this.requireLedger().buildCandidateModelRequest(
434
- input.userPrompt,
435
- this.requireTooling().registry.definitions(),
436
- ).request,
1157
+ const revision = commitAgentSkillsContextUpdate({
1158
+ store: this.store,
1159
+ contextMeter: this.contextMeter,
1160
+ idFactory: this.dependencies.idFactory,
1161
+ snapshot,
1162
+ surface,
1163
+ addedOverrides: receipts,
1164
+ settlements,
1165
+ });
1166
+ committed = true;
1167
+ stage = "activate";
1168
+ this.skillCoordinator.replaceActive(nextActive);
1169
+ this.skillCoordinator.settle(
1170
+ input.unresolved.map((activation) => activation.name),
437
1171
  );
1172
+ const summary = Object.freeze({
1173
+ previousRevisionNumber: snapshot.revision.revisionNumber,
1174
+ revisionNumber: revision.revisionNumber,
1175
+ activated: Object.freeze([...activated].sort()),
1176
+ refreshed: Object.freeze([...(input.refreshed ?? [])].sort()),
1177
+ deactivated: Object.freeze([...(input.deactivated ?? [])].sort()),
1178
+ unavailable: Object.freeze([...unavailable].sort()),
1179
+ addedOverrideCount: receipts.length,
1180
+ });
1181
+ await this.append({
1182
+ type: "context.revision.finished",
1183
+ sessionId: this.sessionId,
1184
+ data: {
1185
+ strategy: "skills_update",
1186
+ reason: input.reason,
1187
+ baseRevisionNumber: summary.previousRevisionNumber,
1188
+ revisionNumber: summary.revisionNumber,
1189
+ activated: summary.activated,
1190
+ refreshed: summary.refreshed,
1191
+ deactivated: summary.deactivated,
1192
+ unavailable: summary.unavailable,
1193
+ addedOverrideCount: summary.addedOverrideCount,
1194
+ measuredAnchorCleared: true,
1195
+ durationMs: elapsedMs(startedAt),
1196
+ },
1197
+ });
1198
+ return summary;
438
1199
  } catch (error) {
439
- if (isCanonicalRuntimeFault(error)) {
440
- this.fault(error);
1200
+ if (error instanceof ContextManagerError) {
1201
+ committed = error.committed;
1202
+ stage =
1203
+ error.stage === "commit"
1204
+ ? "commit"
1205
+ : error.stage === "activate"
1206
+ ? "activate"
1207
+ : "prepare";
441
1208
  }
1209
+ await this.append({
1210
+ type: "context.revision.failed",
1211
+ sessionId: this.sessionId,
1212
+ data: {
1213
+ strategy: "skills_update",
1214
+ reason: input.reason,
1215
+ stage,
1216
+ errorCode: boundedContextErrorCode(
1217
+ error instanceof ContextManagerError
1218
+ ? error.code
1219
+ : error instanceof SessionError
1220
+ ? error.code
1221
+ : error instanceof Error
1222
+ ? error.name
1223
+ : "SKILLS_UPDATE_VALIDATION_FAILED",
1224
+ ),
1225
+ error: `Agent Skills update failed at ${stage}.`,
1226
+ committed,
1227
+ },
1228
+ }).catch(() => undefined);
442
1229
  throw error;
443
1230
  }
444
- const admissionSnapshot = this.contextMeter.measure(admissionPrepared);
445
- this.contextMeter.assertWithinBudget(admissionSnapshot);
1231
+ }
1232
+
1233
+ async executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
1234
+ return (await this.admitTurn(input)).completion;
1235
+ }
1236
+
1237
+ async admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
1238
+ if (this.state !== "ready") {
1239
+ throw new Error(`Cannot execute a turn while RuntimeSession is ${this.state}.`);
1240
+ }
1241
+ validateUserMessage(input.userMessage);
1242
+ if (this.activeTurn !== undefined) {
1243
+ throw new Error("Cannot execute concurrent turns in one RuntimeSession.");
1244
+ }
446
1245
 
1246
+ this.state = "admitting";
447
1247
  const controller = new AbortController();
448
1248
  const removeExternalAbortListener = forwardExternalAbort(input.signal, controller);
449
- const turn = this.stageTurn(input.userPrompt);
450
- let pendingLedgerTurn;
1249
+ const admission = createActiveAdmission(controller);
1250
+ this.activeAdmission = admission;
1251
+ let pendingLedgerTurn: ReturnType<SessionLedger["beginTurn"]>;
1252
+ let admissionPrepared: MaterializedModelRequest;
1253
+ let admissionSnapshot;
451
1254
  try {
1255
+ controller.signal.throwIfAborted();
1256
+ const built = this.requireLedger().buildCandidateModelRequest(
1257
+ input.userMessage,
1258
+ this.requireTooling().registry.definitions(),
1259
+ );
1260
+ const admissionBase = this.createAdmissionBaseToken(built);
1261
+ const prepared = this.input.modelClient.prepare(built.request);
1262
+ const localSnapshot = this.contextMeter.measure(prepared);
1263
+ this.contextMeter.assertWithinBudget(localSnapshot);
1264
+ admissionPrepared = await materializeModelRequest(
1265
+ this.input.modelClient,
1266
+ prepared,
1267
+ { assetStore: this.assetStore, signal: controller.signal },
1268
+ );
1269
+ admissionSnapshot = this.contextMeter.measure(admissionPrepared);
1270
+ if (
1271
+ prepared.mediaOccurrenceCount > 0 &&
1272
+ admissionSnapshot.source !== "measured_plus_estimated_delta"
1273
+ ) {
1274
+ const estimator = this.input.modelClient.inputTokenEstimator;
1275
+ if (estimator === undefined) {
1276
+ throw new Error("Image model request has no input token estimator.");
1277
+ }
1278
+ const estimate = await this.contextMeter.estimateProviderInput(
1279
+ admissionPrepared,
1280
+ estimator,
1281
+ { signal: controller.signal },
1282
+ );
1283
+ admissionSnapshot = this.contextMeter.applyProviderEstimate(
1284
+ admissionPrepared,
1285
+ estimate,
1286
+ );
1287
+ }
1288
+ this.contextMeter.assertWithinBudget(admissionSnapshot);
1289
+ controller.signal.throwIfAborted();
1290
+ const turn = this.stageTurn(input.userMessage);
452
1291
  pendingLedgerTurn = this.requireLedger().beginTurn({
453
1292
  turn,
454
- userPrompt: input.userPrompt,
1293
+ userMessage: input.userMessage,
1294
+ admissionBase,
1295
+ });
1296
+
1297
+ this.settleAdmission(admission);
1298
+ this.state = "executing";
1299
+ const completion = this.startAcceptedTurn({
1300
+ userMessage: input.userMessage,
1301
+ turn,
1302
+ pendingLedgerTurn,
1303
+ controller,
1304
+ removeExternalAbortListener,
1305
+ initialRequest: {
1306
+ prepared: admissionPrepared,
1307
+ usage: admissionSnapshot,
1308
+ },
1309
+ });
1310
+ this.activeTurn = { controller, completion };
1311
+ return Object.freeze({
1312
+ turnId: turn.turnId,
1313
+ userMessage: input.userMessage,
1314
+ completion,
455
1315
  });
456
- this.registerTurn(turn);
457
1316
  } catch (error) {
458
- this.fault(error);
1317
+ this.settleAdmission(admission);
1318
+ removeExternalAbortListener();
1319
+ if (error instanceof AdmissionStaleError) {
1320
+ this.nextTurnNumber = this.store.nextTurnNumber();
1321
+ }
1322
+ if (this.state === "admitting") {
1323
+ this.state = "ready";
1324
+ }
1325
+ if (isCanonicalRuntimeFault(error)) {
1326
+ this.fault(error);
1327
+ }
459
1328
  throw error;
460
1329
  }
461
- this.state = "executing";
462
- const completion = this.performExecuteTurn(
463
- input.userPrompt,
464
- turn,
465
- pendingLedgerTurn,
466
- controller.signal,
467
- removeExternalAbortListener,
468
- );
469
- this.activeTurn = { controller, completion };
470
- return completion;
1330
+ }
1331
+
1332
+ private settleAdmission(admission: ActiveAdmission): void {
1333
+ if (this.activeAdmission !== admission) {
1334
+ throw new Error("Runtime admission ownership was lost.");
1335
+ }
1336
+ this.activeAdmission = undefined;
1337
+ admission.settle();
1338
+ }
1339
+
1340
+ private createAdmissionBaseToken(built: BuiltContextRequest): AdmissionBaseToken {
1341
+ const meta = this.store.readMeta();
1342
+ const head = built.canonical.messages.at(-1);
1343
+ if (
1344
+ head === undefined ||
1345
+ meta.sessionCompatibilitySha256 === null ||
1346
+ built.revision.revisionId !== meta.activeRevisionId ||
1347
+ built.surface.surfaceSha256 !== built.revision.surfaceSha256 ||
1348
+ meta.nextTurnNumber !== this.nextTurnNumber
1349
+ ) {
1350
+ throw new Error("Cannot capture a consistent turn admission base.");
1351
+ }
1352
+ return Object.freeze({
1353
+ canonicalMessageCount: built.canonical.messages.length,
1354
+ canonicalHeadMessageId: head.messageId,
1355
+ canonicalHeadContentSha256: head.contentSha256,
1356
+ activeRevisionId: built.revision.revisionId,
1357
+ activeRevisionNumber: built.revision.revisionNumber,
1358
+ surfaceSha256: built.surface.surfaceSha256,
1359
+ sessionCompatibilitySha256: meta.sessionCompatibilitySha256,
1360
+ nextTurnNumber: meta.nextTurnNumber,
1361
+ });
471
1362
  }
472
1363
 
473
1364
  compactContext(): Promise<ContextCompactionResult> {
@@ -478,16 +1369,16 @@ class DefaultRuntimeSession implements RuntimeSession {
478
1369
  throw new Error("Cannot compact context while a turn is active.");
479
1370
  }
480
1371
  const completion = this.performCompactContext();
481
- this.activeCompaction = completion;
1372
+ this.activeContextRevision = completion;
482
1373
  void completion.then(
483
1374
  () => {
484
- if (this.activeCompaction === completion) {
485
- this.activeCompaction = undefined;
1375
+ if (this.activeContextRevision === completion) {
1376
+ this.activeContextRevision = undefined;
486
1377
  }
487
1378
  },
488
1379
  () => {
489
- if (this.activeCompaction === completion) {
490
- this.activeCompaction = undefined;
1380
+ if (this.activeContextRevision === completion) {
1381
+ this.activeContextRevision = undefined;
491
1382
  }
492
1383
  },
493
1384
  );
@@ -562,6 +1453,104 @@ class DefaultRuntimeSession implements RuntimeSession {
562
1453
  }
563
1454
  }
564
1455
 
1456
+ retireContext(): Promise<ContextRetirementResult> {
1457
+ if (this.state !== "ready") {
1458
+ throw new Error(
1459
+ `Cannot retire context prefix while RuntimeSession is ${this.state}.`,
1460
+ );
1461
+ }
1462
+ if (this.activeTurn !== undefined) {
1463
+ throw new Error("Cannot retire context prefix while a turn is active.");
1464
+ }
1465
+ const completion = this.performRetireContext();
1466
+ this.activeContextRevision = completion;
1467
+ void completion.then(
1468
+ () => {
1469
+ if (this.activeContextRevision === completion) {
1470
+ this.activeContextRevision = undefined;
1471
+ }
1472
+ },
1473
+ () => {
1474
+ if (this.activeContextRevision === completion) {
1475
+ this.activeContextRevision = undefined;
1476
+ }
1477
+ },
1478
+ );
1479
+ return completion;
1480
+ }
1481
+
1482
+ private async performRetireContext(): Promise<ContextRetirementResult> {
1483
+ if (this.state !== "ready") {
1484
+ throw new Error(
1485
+ `Cannot retire context prefix while RuntimeSession is ${this.state}.`,
1486
+ );
1487
+ }
1488
+ if (this.activeTurn !== undefined) {
1489
+ throw new Error("Cannot retire context prefix while a turn is active.");
1490
+ }
1491
+ this.store.assertContextRevisionIdle();
1492
+ const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
1493
+ this.state = "compacting";
1494
+ let started = false;
1495
+ try {
1496
+ await this.append({
1497
+ type: "context.revision.started",
1498
+ sessionId: this.sessionId,
1499
+ data: {
1500
+ strategy: "retire_prefix",
1501
+ reason: "manual",
1502
+ policyVersion: "recall-first-retirement-v1",
1503
+ baseRevisionNumber,
1504
+ },
1505
+ });
1506
+ started = true;
1507
+ const result = await this.requireContextManager().retirePrefix(
1508
+ this.dependencies.manualRetirementTrigger(),
1509
+ );
1510
+ await this.append({
1511
+ type: "context.revision.finished",
1512
+ sessionId: this.sessionId,
1513
+ data: contextRetirementFinishedData(result),
1514
+ });
1515
+ if (this.state === "compacting") {
1516
+ this.state = "ready";
1517
+ }
1518
+ return result;
1519
+ } catch (error) {
1520
+ if (started && !(error instanceof RuntimeEventAppendError)) {
1521
+ const failure =
1522
+ error instanceof ContextManagerError
1523
+ ? error
1524
+ : new ContextManagerError(
1525
+ "activate",
1526
+ error instanceof Error ? error.name : "CONTEXT_RETIREMENT_FAILED",
1527
+ true,
1528
+ false,
1529
+ "Context prefix retirement failed.",
1530
+ { cause: error },
1531
+ );
1532
+ await this.append({
1533
+ type: "context.revision.failed",
1534
+ sessionId: this.sessionId,
1535
+ data: {
1536
+ strategy: "retire_prefix",
1537
+ reason: "manual",
1538
+ stage: failure.stage,
1539
+ errorCode: boundedContextErrorCode(failure.code),
1540
+ error: `Context prefix retirement failed at ${failure.stage}.`,
1541
+ committed: failure.committed,
1542
+ },
1543
+ }).catch(() => undefined);
1544
+ }
1545
+ if (!(error instanceof ContextManagerError) || error.fatal) {
1546
+ this.fault(error);
1547
+ } else if (this.state === "compacting") {
1548
+ this.state = "ready";
1549
+ }
1550
+ throw error;
1551
+ }
1552
+ }
1553
+
565
1554
  dispose(reason: SessionDisposeReason): Promise<void> {
566
1555
  this.disposePromise ??= this.performDispose(reason);
567
1556
  return this.disposePromise;
@@ -578,12 +1567,65 @@ class DefaultRuntimeSession implements RuntimeSession {
578
1567
  );
579
1568
  }
580
1569
 
1570
+ async cloneSession(targetSessionId: SessionId): Promise<void> {
1571
+ if (!this.canSwitchSession()) {
1572
+ throw new Error(
1573
+ "Cannot clone the session while a turn, context operation, or background task is active.",
1574
+ );
1575
+ }
1576
+ await this.waitForStableEventTail();
1577
+ if (!this.canSwitchSession()) {
1578
+ throw new Error(
1579
+ "Cannot clone the session while a turn, context operation, or background task is active.",
1580
+ );
1581
+ }
1582
+ await this.store.cloneTo({ targetSessionId });
1583
+ }
1584
+
1585
+ private async waitForStableEventTail(): Promise<void> {
1586
+ for (;;) {
1587
+ const tail = this.eventTail;
1588
+ await tail;
1589
+ if (this.eventTail === tail) {
1590
+ return;
1591
+ }
1592
+ }
1593
+ }
1594
+
1595
+ private async startAcceptedTurn(input: {
1596
+ userMessage: UserMessage;
1597
+ turn: TurnIdentity;
1598
+ pendingLedgerTurn: ReturnType<SessionLedger["beginTurn"]>;
1599
+ controller: AbortController;
1600
+ removeExternalAbortListener: () => void;
1601
+ initialRequest: NonNullable<RunAgentInput["initialRequest"]>;
1602
+ }): Promise<RunAgentResult> {
1603
+ try {
1604
+ this.registerTurn(input.turn);
1605
+ } catch (error) {
1606
+ input.removeExternalAbortListener();
1607
+ input.pendingLedgerTurn.fault(error);
1608
+ this.activeTurn = undefined;
1609
+ this.fault(error);
1610
+ throw error;
1611
+ }
1612
+ return this.performExecuteTurn(
1613
+ input.userMessage,
1614
+ input.turn,
1615
+ input.pendingLedgerTurn,
1616
+ input.controller.signal,
1617
+ input.removeExternalAbortListener,
1618
+ input.initialRequest,
1619
+ );
1620
+ }
1621
+
581
1622
  private async performExecuteTurn(
582
- userPrompt: string,
1623
+ userMessage: UserMessage,
583
1624
  turn: TurnIdentity,
584
1625
  pendingLedgerTurn: ReturnType<SessionLedger["beginTurn"]>,
585
1626
  signal: AbortSignal,
586
1627
  removeExternalAbortListener: () => void,
1628
+ initialRequest: NonNullable<RunAgentInput["initialRequest"]>,
587
1629
  ): Promise<RunAgentResult> {
588
1630
  let settled = false;
589
1631
 
@@ -591,7 +1633,7 @@ class DefaultRuntimeSession implements RuntimeSession {
591
1633
  await this.append({
592
1634
  type: "turn.started",
593
1635
  ...turn,
594
- data: { userPrompt },
1636
+ data: { userPrompt: projectUserMessage(userMessage) },
595
1637
  });
596
1638
 
597
1639
  let result: RunAgentResult;
@@ -604,7 +1646,16 @@ class DefaultRuntimeSession implements RuntimeSession {
604
1646
  committedPrefixAuditor: this.committedPrefixAuditor,
605
1647
  shadowPlanning: {
606
1648
  planner: this.shadowPlanner,
607
- select: this.dependencies.selectShadowPlanning,
1649
+ select: (planningInput) => {
1650
+ const selection = this.dependencies.selectShadowPlanning(planningInput);
1651
+ if (
1652
+ selection?.trigger === "runtime_pressure" &&
1653
+ this.requireContextAutomation().automaticSwapOnly
1654
+ ) {
1655
+ this.pendingAutomaticContextMaintenance = true;
1656
+ }
1657
+ return selection;
1658
+ },
608
1659
  ...(this.dependencies.onShadowPlanningResult === undefined
609
1660
  ? {}
610
1661
  : { onResult: this.dependencies.onShadowPlanningResult }),
@@ -615,6 +1666,8 @@ class DefaultRuntimeSession implements RuntimeSession {
615
1666
  runtimeSession: this.context,
616
1667
  turn,
617
1668
  signal,
1669
+ assetStore: this.assetStore,
1670
+ initialRequest,
618
1671
  });
619
1672
  } catch (error) {
620
1673
  if (error instanceof RuntimeEventAppendError) {
@@ -629,6 +1682,7 @@ class DefaultRuntimeSession implements RuntimeSession {
629
1682
  );
630
1683
  pendingLedgerTurn.finish(error.result);
631
1684
  settled = true;
1685
+ await this.settleClosedTurnSkills();
632
1686
  throw error;
633
1687
  }
634
1688
 
@@ -644,6 +1698,10 @@ class DefaultRuntimeSession implements RuntimeSession {
644
1698
  await this.appendTerminalEvent(turn, result, projectedMessageCount);
645
1699
  pendingLedgerTurn.finish(result);
646
1700
  settled = true;
1701
+ await this.settleClosedTurnSkills();
1702
+ if (result.status === "completed") {
1703
+ await this.performAutomaticContextMaintenance();
1704
+ }
647
1705
  return result;
648
1706
  } catch (error) {
649
1707
  if (!settled) {
@@ -654,12 +1712,166 @@ class DefaultRuntimeSession implements RuntimeSession {
654
1712
  } finally {
655
1713
  removeExternalAbortListener();
656
1714
  this.activeTurn = undefined;
1715
+ this.pendingAutomaticContextMaintenance = false;
657
1716
  if (this.state === "executing") {
658
1717
  this.state = "ready";
659
1718
  }
660
1719
  }
661
1720
  }
662
1721
 
1722
+ private async performAutomaticContextMaintenance(): Promise<void> {
1723
+ if (!this.pendingAutomaticContextMaintenance) return;
1724
+ this.pendingAutomaticContextMaintenance = false;
1725
+ const automation = this.requireContextAutomation();
1726
+ if (!automation.automaticSwapOnly) return;
1727
+ if (this.state !== "executing") {
1728
+ throw new Error(
1729
+ `Cannot run automatic context maintenance while RuntimeSession is ${this.state}.`,
1730
+ );
1731
+ }
1732
+ this.store.assertContextRevisionIdle();
1733
+ const qualificationId = requireAutomationQualificationId(automation);
1734
+ this.state = "maintaining_context";
1735
+ try {
1736
+ const swap = await this.performAutomaticCompaction(qualificationId);
1737
+ if (swap === undefined) return;
1738
+ if (automation.automaticPrefixRetirement && automaticSwapNeedsRetirement(swap)) {
1739
+ await this.performAutomaticRetirement(qualificationId);
1740
+ }
1741
+ } finally {
1742
+ if (this.state === "maintaining_context") {
1743
+ this.state = "executing";
1744
+ }
1745
+ }
1746
+ }
1747
+
1748
+ private async performAutomaticCompaction(
1749
+ qualificationId: string,
1750
+ ): Promise<ContextCompactionResult | undefined> {
1751
+ let started = false;
1752
+ try {
1753
+ await this.append({
1754
+ type: "context.revision.started",
1755
+ sessionId: this.sessionId,
1756
+ data: {
1757
+ strategy: "swap",
1758
+ reason: "runtime_pressure",
1759
+ policyVersion: "swap-only-v1",
1760
+ rendererFormat: "swap-observation-v1",
1761
+ qualificationId,
1762
+ },
1763
+ });
1764
+ started = true;
1765
+ const result = await this.requireContextManager().compact(
1766
+ this.dependencies.automaticCompactionTrigger(),
1767
+ );
1768
+ await this.append({
1769
+ type: "context.revision.finished",
1770
+ sessionId: this.sessionId,
1771
+ data: contextRevisionFinishedData(result, "runtime_pressure", qualificationId),
1772
+ });
1773
+ return result;
1774
+ } catch (error) {
1775
+ if (started && !(error instanceof RuntimeEventAppendError)) {
1776
+ const failure = automaticContextFailure(error, "compaction");
1777
+ await this.append({
1778
+ type: "context.revision.failed",
1779
+ sessionId: this.sessionId,
1780
+ data: {
1781
+ strategy: "swap",
1782
+ reason: "runtime_pressure",
1783
+ stage: failure.stage,
1784
+ errorCode: boundedContextErrorCode(failure.code),
1785
+ error: `Automatic context compaction failed at ${failure.stage}.`,
1786
+ qualificationId,
1787
+ },
1788
+ }).catch(() => undefined);
1789
+ }
1790
+ if (error instanceof ContextManagerError && !error.fatal) {
1791
+ return undefined;
1792
+ }
1793
+ throw error;
1794
+ }
1795
+ }
1796
+
1797
+ private async performAutomaticRetirement(
1798
+ qualificationId: string,
1799
+ ): Promise<ContextRetirementResult | undefined> {
1800
+ const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
1801
+ let started = false;
1802
+ try {
1803
+ await this.append({
1804
+ type: "context.revision.started",
1805
+ sessionId: this.sessionId,
1806
+ data: {
1807
+ strategy: "retire_prefix",
1808
+ reason: "runtime_pressure",
1809
+ policyVersion: "recall-first-retirement-v1",
1810
+ baseRevisionNumber,
1811
+ qualificationId,
1812
+ },
1813
+ });
1814
+ started = true;
1815
+ const result = await this.requireContextManager().retirePrefix(
1816
+ this.dependencies.automaticRetirementTrigger(),
1817
+ );
1818
+ await this.append({
1819
+ type: "context.revision.finished",
1820
+ sessionId: this.sessionId,
1821
+ data: contextRetirementFinishedData(
1822
+ result,
1823
+ "runtime_pressure",
1824
+ qualificationId,
1825
+ ),
1826
+ });
1827
+ return result;
1828
+ } catch (error) {
1829
+ if (started && !(error instanceof RuntimeEventAppendError)) {
1830
+ const failure = automaticContextFailure(error, "retirement");
1831
+ await this.append({
1832
+ type: "context.revision.failed",
1833
+ sessionId: this.sessionId,
1834
+ data: {
1835
+ strategy: "retire_prefix",
1836
+ reason: "runtime_pressure",
1837
+ stage: failure.stage,
1838
+ errorCode: boundedContextErrorCode(failure.code),
1839
+ error: `Automatic context retirement failed at ${failure.stage}.`,
1840
+ committed: failure.committed,
1841
+ qualificationId,
1842
+ },
1843
+ }).catch(() => undefined);
1844
+ }
1845
+ if (error instanceof ContextManagerError && !error.fatal) {
1846
+ return undefined;
1847
+ }
1848
+ throw error;
1849
+ }
1850
+ }
1851
+
1852
+ private async settleClosedTurnSkills(): Promise<void> {
1853
+ const unresolved = this.store.loadSkillActivations(["pending", "dispatched"]);
1854
+ if (unresolved.length === 0) {
1855
+ return;
1856
+ }
1857
+ const summary = await this.commitSkillSettlements({
1858
+ reason: "activation",
1859
+ unresolved,
1860
+ });
1861
+ await this.append({
1862
+ type: "skills.updated",
1863
+ sessionId: this.sessionId,
1864
+ data: {
1865
+ reason: "activation",
1866
+ activated: summary.activated,
1867
+ refreshed: summary.refreshed,
1868
+ deactivated: summary.deactivated,
1869
+ unavailable: summary.unavailable,
1870
+ revisionNumber: summary.revisionNumber,
1871
+ },
1872
+ });
1873
+ }
1874
+
663
1875
  private async appendTerminalEvent(
664
1876
  turn: TurnIdentity,
665
1877
  result: RunAgentResult,
@@ -702,10 +1914,15 @@ class DefaultRuntimeSession implements RuntimeSession {
702
1914
 
703
1915
  this.state = "disposing";
704
1916
  const errors: unknown[] = this.faultCause === undefined ? [] : [this.faultCause];
705
- const activeCompaction = this.activeCompaction;
706
- if (activeCompaction !== undefined) {
1917
+ const activeAdmission = this.activeAdmission;
1918
+ if (activeAdmission !== undefined) {
1919
+ activeAdmission.controller.abort(new TurnCancelledError("session_dispose"));
1920
+ await activeAdmission.settled;
1921
+ }
1922
+ const activeContextRevision = this.activeContextRevision;
1923
+ if (activeContextRevision !== undefined) {
707
1924
  try {
708
- await activeCompaction;
1925
+ await activeContextRevision;
709
1926
  } catch {
710
1927
  // The compactContext caller owns its primary error. Disposal still continues.
711
1928
  }
@@ -794,10 +2011,8 @@ class DefaultRuntimeSession implements RuntimeSession {
794
2011
  throw new Error("Initialization error collection unexpectedly returned.");
795
2012
  }
796
2013
 
797
- private stageTurn(userPrompt: string): TurnIdentity {
798
- if (userPrompt.trim() === "") {
799
- throw new Error("Cannot create an AgentTurn for an empty prompt.");
800
- }
2014
+ private stageTurn(userMessage: UserMessage): TurnIdentity {
2015
+ validateUserMessage(userMessage);
801
2016
 
802
2017
  const identity: TurnIdentity = {
803
2018
  sessionId: this.sessionId,
@@ -1074,18 +2289,26 @@ function validateCreateInput(input: CreateRuntimeSessionInput): void {
1074
2289
  if (!path.isAbsolute(input.workspaceRoot)) {
1075
2290
  throw new Error("RuntimeSession workspaceRoot must be an absolute path.");
1076
2291
  }
2292
+ if (
2293
+ input.skillCatalog !== undefined &&
2294
+ input.skillCatalog.workspaceRoot !== input.workspaceRoot
2295
+ ) {
2296
+ throw new Error("RuntimeSession Agent Skill catalog belongs to another workspace.");
2297
+ }
1077
2298
  if (input.modelName.trim() === "") {
1078
2299
  throw new Error("RuntimeSession modelName must not be empty.");
1079
2300
  }
1080
2301
  requirePositiveNumber(input.maxIterations, "maxIterations");
1081
- if (isNewSessionInput(input) && input.systemPrompt.trim() === "") {
2302
+ if (input.systemPrompt.trim() === "") {
1082
2303
  throw new Error("RuntimeSession systemPrompt must not be empty.");
1083
2304
  }
1084
2305
  if (
1085
2306
  typeof input.modelClient !== "object" ||
1086
2307
  input.modelClient === null ||
1087
2308
  typeof input.modelClient.prepare !== "function" ||
1088
- typeof input.modelClient.request !== "function"
2309
+ typeof input.modelClient.request !== "function" ||
2310
+ typeof input.modelClient.messageProtocol !== "object" ||
2311
+ input.modelClient.messageProtocol === null
1089
2312
  ) {
1090
2313
  throw new Error(
1091
2314
  "RuntimeSession modelClient must implement prepare() and request().",
@@ -1094,12 +2317,47 @@ function validateCreateInput(input: CreateRuntimeSessionInput): void {
1094
2317
  assertMatchingContextBudget(input.contextProfile, input.contextBudget);
1095
2318
  }
1096
2319
 
2320
+ function assertPreparedMatchesSurface(
2321
+ prepared: ReturnType<ModelClient["prepare"]>,
2322
+ surface: StoredContextSurfaceV8,
2323
+ ): void {
2324
+ if (
2325
+ prepared.requestConfigHash !== surface.requestConfigSha256 ||
2326
+ prepared.toolSchemaHash !== surface.toolSchemaSha256 ||
2327
+ prepared.requestMaxOutputTokens !== surface.requestMaxOutputTokens
2328
+ ) {
2329
+ throw new Error("Prepared model request does not match its context surface.");
2330
+ }
2331
+ }
2332
+
2333
+ function elapsedMs(startedAt: number): number {
2334
+ return Math.round((performance.now() - startedAt) * 100) / 100;
2335
+ }
2336
+
1097
2337
  function isNewSessionInput(
1098
2338
  input: CreateRuntimeSessionInput,
1099
2339
  ): input is CreateNewRuntimeSessionInput {
1100
2340
  return input.selection.mode === "new";
1101
2341
  }
1102
2342
 
2343
+ function createActiveAdmission(controller: AbortController): ActiveAdmission {
2344
+ let resolve!: () => void;
2345
+ const settled = new Promise<void>((settle) => {
2346
+ resolve = settle;
2347
+ });
2348
+ let didSettle = false;
2349
+ return {
2350
+ controller,
2351
+ settled,
2352
+ settle: () => {
2353
+ if (!didSettle) {
2354
+ didSettle = true;
2355
+ resolve();
2356
+ }
2357
+ },
2358
+ };
2359
+ }
2360
+
1103
2361
  function forwardExternalAbort(
1104
2362
  externalSignal: AbortSignal,
1105
2363
  internalController: AbortController,
@@ -1150,33 +2408,36 @@ function errorMessage(error: unknown): string {
1150
2408
 
1151
2409
  function contextRevisionFinishedData(
1152
2410
  result: ContextCompactionResult,
2411
+ reason: "manual" | "runtime_pressure" = "manual",
2412
+ qualificationId?: string,
1153
2413
  ): ContextRevisionFinishedData {
1154
2414
  if (result.status === "unchanged") {
1155
2415
  return {
1156
2416
  strategy: "swap",
1157
- reason: "manual",
2417
+ reason,
1158
2418
  policyVersion: "swap-only-v1",
1159
2419
  outcome: result.outcome,
1160
2420
  baseRevisionNumber: result.revisionNumber,
1161
2421
  addedOverrideCount: 0,
1162
- totalOverrideCount: result.totalOverrideCount,
2422
+ activeOverrideCount: result.activeOverrideCount,
1163
2423
  originalObservationBytes: 0,
1164
2424
  projectedObservationBytes: 0,
1165
2425
  rawTokensBefore: result.rawTokensBefore,
1166
2426
  guardedTokensBefore: result.guardedTokensBefore,
1167
2427
  targetTokens: result.targetTokens,
1168
2428
  durationMs: result.durationMs,
2429
+ ...(qualificationId === undefined ? {} : { qualificationId }),
1169
2430
  };
1170
2431
  }
1171
2432
  return {
1172
2433
  strategy: "swap",
1173
- reason: "manual",
2434
+ reason,
1174
2435
  policyVersion: "swap-only-v1",
1175
2436
  outcome: result.outcome,
1176
2437
  baseRevisionNumber: result.previousRevisionNumber,
1177
2438
  revisionNumber: result.revisionNumber,
1178
2439
  addedOverrideCount: result.addedOverrideCount,
1179
- totalOverrideCount: result.totalOverrideCount,
2440
+ activeOverrideCount: result.activeOverrideCount,
1180
2441
  originalObservationBytes: result.originalObservationBytes,
1181
2442
  projectedObservationBytes: result.projectedObservationBytes,
1182
2443
  rawTokensBefore: result.rawTokensBefore,
@@ -1186,9 +2447,99 @@ function contextRevisionFinishedData(
1186
2447
  targetTokens: result.targetTokens,
1187
2448
  planHash: result.planHash,
1188
2449
  durationMs: result.durationMs,
2450
+ ...(qualificationId === undefined ? {} : { qualificationId }),
1189
2451
  };
1190
2452
  }
1191
2453
 
2454
+ function contextRetirementFinishedData(
2455
+ result: ContextRetirementResult,
2456
+ reason: "manual" | "runtime_pressure" = "manual",
2457
+ qualificationId?: string,
2458
+ ): ContextRevisionFinishedData {
2459
+ if (result.status === "unchanged") {
2460
+ return {
2461
+ strategy: "retire_prefix",
2462
+ reason,
2463
+ policyVersion: "recall-first-retirement-v1",
2464
+ outcome: result.outcome,
2465
+ baseRevisionNumber: result.revisionNumber,
2466
+ previousKeepFromOrdinal: result.keepFromOrdinal,
2467
+ keepFromOrdinal: result.keepFromOrdinal,
2468
+ retiredTurnCount: 0,
2469
+ retiredFrameCount: 0,
2470
+ retiredMessageCount: 0,
2471
+ activeOverrideCount: result.activeOverrideCount,
2472
+ guardedTokensBefore: result.guardedTokensBefore,
2473
+ targetTokens: result.targetTokens,
2474
+ planningDurationMs: result.planningDurationMs,
2475
+ durationMs: result.durationMs,
2476
+ ...(qualificationId === undefined ? {} : { qualificationId }),
2477
+ };
2478
+ }
2479
+ return {
2480
+ strategy: "retire_prefix",
2481
+ reason,
2482
+ policyVersion: "recall-first-retirement-v1",
2483
+ outcome: result.outcome,
2484
+ baseRevisionNumber: result.previousRevisionNumber,
2485
+ revisionNumber: result.revisionNumber,
2486
+ previousKeepFromOrdinal: result.previousKeepFromOrdinal,
2487
+ keepFromOrdinal: result.keepFromOrdinal,
2488
+ retiredTurnCount: result.retiredTurnCount,
2489
+ retiredFrameCount: result.retiredFrameCount,
2490
+ retiredMessageCount: result.retiredMessageCount,
2491
+ activeOverrideCount: result.activeOverrideCount,
2492
+ rawTokensBefore: result.rawTokensBefore,
2493
+ rawTokensAfter: result.rawTokensAfter,
2494
+ guardedTokensBefore: result.guardedTokensBefore,
2495
+ guardedTokensAfter: result.guardedTokensAfter,
2496
+ targetTokens: result.targetTokens,
2497
+ planHash: result.planHash,
2498
+ planningDurationMs: result.planningDurationMs,
2499
+ validationDurationMs: result.validationDurationMs,
2500
+ transactionDurationMs: result.transactionDurationMs,
2501
+ activationDurationMs: result.activationDurationMs,
2502
+ durationMs: result.durationMs,
2503
+ ...(qualificationId === undefined ? {} : { qualificationId }),
2504
+ };
2505
+ }
2506
+
2507
+ function requireAutomationQualificationId(decision: ContextAutomationDecision): string {
2508
+ if (
2509
+ !decision.automaticSwapOnly ||
2510
+ (decision.reason !== "qualified" && decision.reason !== "swap_only_qualified") ||
2511
+ decision.qualificationId === undefined
2512
+ ) {
2513
+ throw new Error("Automatic context maintenance has no qualification identity.");
2514
+ }
2515
+ return decision.qualificationId;
2516
+ }
2517
+
2518
+ function automaticSwapNeedsRetirement(result: ContextCompactionResult): boolean {
2519
+ return (
2520
+ result.outcome === "no_eligible_candidates" ||
2521
+ result.outcome === "insufficient_candidates"
2522
+ );
2523
+ }
2524
+
2525
+ function automaticContextFailure(
2526
+ error: unknown,
2527
+ strategy: "compaction" | "retirement",
2528
+ ): ContextManagerError {
2529
+ return error instanceof ContextManagerError
2530
+ ? error
2531
+ : new ContextManagerError(
2532
+ "activate",
2533
+ error instanceof Error
2534
+ ? error.name
2535
+ : `AUTOMATIC_CONTEXT_${strategy.toUpperCase()}_FAILED`,
2536
+ true,
2537
+ false,
2538
+ `Automatic context ${strategy} failed.`,
2539
+ { cause: error },
2540
+ );
2541
+ }
2542
+
1192
2543
  function boundedContextErrorCode(code: string): string {
1193
2544
  return /^[A-Za-z0-9_]+$/.test(code) && code.length <= 80
1194
2545
  ? code
@@ -1201,6 +2552,10 @@ function requirePositiveNumber(value: number, name: string): void {
1201
2552
  }
1202
2553
  }
1203
2554
 
2555
+ function compareText(left: string, right: string): number {
2556
+ return left < right ? -1 : left > right ? 1 : 0;
2557
+ }
2558
+
1204
2559
  function isCanonicalRuntimeFault(error: unknown): boolean {
1205
2560
  return (
1206
2561
  error instanceof ContextProtocolError ||