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
@@ -0,0 +1,272 @@
1
+ type ProviderContext = { provider: string; model: string };
2
+
3
+ type ToolCallAccumulator = {
4
+ id?: string;
5
+ type?: "function";
6
+ name?: string;
7
+ arguments: string;
8
+ };
9
+
10
+ /**
11
+ * Reassembles OpenAI chat completion stream chunks into a non-streaming
12
+ * chat.completion-shaped object so the result can be validated and mapped by
13
+ * fromOpenAIChatCompletion exactly like a non-streaming response.
14
+ */
15
+ export function accumulateOpenAIChatCompletionChunks(
16
+ chunks: readonly unknown[],
17
+ options: ProviderContext,
18
+ ): Record<string, unknown> {
19
+ if (chunks.length === 0) {
20
+ throw providerStreamError(options, "chunks", "must not be empty");
21
+ }
22
+
23
+ let role: "assistant" | undefined;
24
+ let content: string | undefined;
25
+ let reasoningContent: string | undefined;
26
+ let finishReason: string | undefined;
27
+ let resolvedModel: string | undefined;
28
+ let usage: Record<string, unknown> | undefined;
29
+ const toolCalls: ToolCallAccumulator[] = [];
30
+
31
+ chunks.forEach((chunk, chunkIndex) => {
32
+ const path = `chunk[${chunkIndex}]`;
33
+ const record = requireRecord(chunk, path, options);
34
+
35
+ if (record.model !== undefined && record.model !== null) {
36
+ if (typeof record.model !== "string" || record.model.trim() === "") {
37
+ throw providerStreamError(options, `${path}.model`, "must be a string");
38
+ }
39
+ if (resolvedModel !== undefined && resolvedModel !== record.model) {
40
+ throw providerStreamError(
41
+ options,
42
+ `${path}.model`,
43
+ `conflicts with previously streamed model ${JSON.stringify(resolvedModel)}`,
44
+ );
45
+ }
46
+ resolvedModel = record.model;
47
+ }
48
+
49
+ if (record.usage !== undefined && record.usage !== null) {
50
+ usage = requireRecord(record.usage, `${path}.usage`, options);
51
+ }
52
+
53
+ if (record.choices === undefined || record.choices === null) {
54
+ return;
55
+ }
56
+ if (!Array.isArray(record.choices)) {
57
+ throw providerStreamError(options, `${path}.choices`, "must be an array");
58
+ }
59
+ // The usage-only final chunk from stream_options.include_usage has empty choices.
60
+ for (const [choiceIndex, rawChoice] of record.choices.entries()) {
61
+ const choicePath = `${path}.choices[${choiceIndex}]`;
62
+ const choice = requireRecord(rawChoice, choicePath, options);
63
+ if (choice.index !== 0) {
64
+ throw providerStreamError(options, `${choicePath}.index`, "must be 0");
65
+ }
66
+ if (typeof choice.finish_reason === "string") {
67
+ finishReason = choice.finish_reason;
68
+ } else if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
69
+ throw providerStreamError(
70
+ options,
71
+ `${choicePath}.finish_reason`,
72
+ "must be a string or null",
73
+ );
74
+ }
75
+
76
+ if (choice.delta === undefined || choice.delta === null) {
77
+ continue;
78
+ }
79
+ const deltaPath = `${choicePath}.delta`;
80
+ const delta = asRecord(choice.delta, deltaPath, options);
81
+ if (delta.role !== undefined && delta.role !== null) {
82
+ if (delta.role !== "assistant") {
83
+ throw providerStreamError(
84
+ options,
85
+ `${deltaPath}.role`,
86
+ 'must be "assistant"',
87
+ );
88
+ }
89
+ role = delta.role;
90
+ }
91
+ content = appendOptionalText(
92
+ content,
93
+ delta.content,
94
+ `${deltaPath}.content`,
95
+ options,
96
+ );
97
+ reasoningContent = appendOptionalText(
98
+ reasoningContent,
99
+ delta.reasoning_content,
100
+ `${deltaPath}.reasoning_content`,
101
+ options,
102
+ );
103
+ if (delta.tool_calls === undefined || delta.tool_calls === null) {
104
+ continue;
105
+ }
106
+ if (!Array.isArray(delta.tool_calls)) {
107
+ throw providerStreamError(
108
+ options,
109
+ `${deltaPath}.tool_calls`,
110
+ "must be an array",
111
+ );
112
+ }
113
+ for (const [fragmentIndex, rawFragment] of delta.tool_calls.entries()) {
114
+ mergeToolCallFragment(
115
+ toolCalls,
116
+ rawFragment,
117
+ `${deltaPath}.tool_calls[${fragmentIndex}]`,
118
+ options,
119
+ );
120
+ }
121
+ }
122
+ });
123
+
124
+ // Only fields the provider actually streamed are emitted; the strict
125
+ // non-streaming mapper rejects a missing role, id, type, or name.
126
+ const message: Record<string, unknown> = {
127
+ ...(role === undefined ? {} : { role }),
128
+ content: content ?? null,
129
+ ...(reasoningContent === undefined ? {} : { reasoning_content: reasoningContent }),
130
+ ...(toolCalls.length === 0
131
+ ? {}
132
+ : {
133
+ tool_calls: toolCalls.map((call) => ({
134
+ ...(call.id === undefined ? {} : { id: call.id }),
135
+ ...(call.type === undefined ? {} : { type: call.type }),
136
+ function: {
137
+ ...(call.name === undefined ? {} : { name: call.name }),
138
+ arguments: call.arguments,
139
+ },
140
+ })),
141
+ }),
142
+ };
143
+
144
+ return {
145
+ object: "chat.completion",
146
+ choices: [
147
+ {
148
+ index: 0,
149
+ message,
150
+ finish_reason: finishReason ?? null,
151
+ },
152
+ ],
153
+ ...(usage === undefined ? {} : { usage }),
154
+ ...(resolvedModel === undefined ? {} : { model: resolvedModel }),
155
+ };
156
+ }
157
+
158
+ function mergeToolCallFragment(
159
+ toolCalls: ToolCallAccumulator[],
160
+ rawFragment: unknown,
161
+ path: string,
162
+ options: ProviderContext,
163
+ ): void {
164
+ const fragment = requireRecord(rawFragment, path, options);
165
+ const index = fragment.index;
166
+ if (!Number.isSafeInteger(index) || (index as number) < 0) {
167
+ throw providerStreamError(
168
+ options,
169
+ `${path}.index`,
170
+ "must be a non-negative integer",
171
+ );
172
+ }
173
+ if ((index as number) > toolCalls.length) {
174
+ throw providerStreamError(
175
+ options,
176
+ `${path}.index`,
177
+ `skips tool call index ${toolCalls.length}`,
178
+ );
179
+ }
180
+ const entry =
181
+ toolCalls[index as number] ?? (toolCalls[index as number] = { arguments: "" });
182
+
183
+ if (fragment.type !== undefined && fragment.type !== null) {
184
+ if (fragment.type !== "function") {
185
+ throw providerStreamError(options, `${path}.type`, 'must be "function"');
186
+ }
187
+ entry.type = fragment.type;
188
+ }
189
+ if (fragment.id !== undefined && fragment.id !== null) {
190
+ if (typeof fragment.id !== "string" || fragment.id === "") {
191
+ throw providerStreamError(options, `${path}.id`, "must be a non-empty string");
192
+ }
193
+ if (entry.id !== undefined && entry.id !== fragment.id) {
194
+ throw providerStreamError(
195
+ options,
196
+ `${path}.id`,
197
+ `conflicts with previously streamed id ${JSON.stringify(entry.id)}`,
198
+ );
199
+ }
200
+ entry.id = fragment.id;
201
+ }
202
+ if (fragment.function === undefined || fragment.function === null) {
203
+ return;
204
+ }
205
+ const fn = asRecord(fragment.function, `${path}.function`, options);
206
+ if (fn.name !== undefined && fn.name !== null) {
207
+ if (typeof fn.name !== "string") {
208
+ throw providerStreamError(options, `${path}.function.name`, "must be a string");
209
+ }
210
+ // Later name fragments replace the value (OpenAI aggregation semantics);
211
+ // only arguments accumulate.
212
+ entry.name = fn.name;
213
+ }
214
+ if (fn.arguments !== undefined && fn.arguments !== null) {
215
+ if (typeof fn.arguments !== "string") {
216
+ throw providerStreamError(
217
+ options,
218
+ `${path}.function.arguments`,
219
+ "must be a string",
220
+ );
221
+ }
222
+ entry.arguments += fn.arguments;
223
+ }
224
+ }
225
+
226
+ function appendOptionalText(
227
+ current: string | undefined,
228
+ value: unknown,
229
+ path: string,
230
+ options: ProviderContext,
231
+ ): string | undefined {
232
+ if (value === undefined || value === null) {
233
+ return current;
234
+ }
235
+ if (typeof value !== "string") {
236
+ throw providerStreamError(options, path, "must be a string or null");
237
+ }
238
+ return (current ?? "") + value;
239
+ }
240
+
241
+ function asRecord(
242
+ value: unknown,
243
+ path: string,
244
+ options: ProviderContext,
245
+ ): Record<string, unknown> {
246
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
247
+ throw providerStreamError(options, path, "must be an object");
248
+ }
249
+ return value as Record<string, unknown>;
250
+ }
251
+
252
+ function requireRecord(
253
+ value: unknown,
254
+ path: string,
255
+ options: ProviderContext,
256
+ ): Record<string, unknown> {
257
+ const record = asRecord(value, path, options);
258
+ if (Object.keys(record).length === 0) {
259
+ throw providerStreamError(options, path, "must be a non-empty object");
260
+ }
261
+ return record;
262
+ }
263
+
264
+ function providerStreamError(
265
+ options: ProviderContext,
266
+ path: string,
267
+ detail: string,
268
+ ): Error {
269
+ return new Error(
270
+ `Invalid provider stream (provider=${options.provider}, model=${options.model}): ${path} ${detail}.`,
271
+ );
272
+ }
@@ -1,5 +1,5 @@
1
1
  import type { PreparedModelRequest, PreparedPromptSegment } from "./model-client";
2
- import { sha256 } from "./model-request-preflight";
2
+ import { sha256, stableJsonStringify } from "./model-request-preflight";
3
3
 
4
4
  export type PromptPrefixFingerprint = {
5
5
  readonly requestConfigHash: string;
@@ -18,9 +18,7 @@ export function promptPrefixHashes(
18
18
  if (previous === undefined) {
19
19
  throw new Error("Prompt prefix hash chain has no seed.");
20
20
  }
21
- hashes.push(
22
- sha256(`${previous}\u0000${segment.kind}\u0000${segment.normalizedText}`),
23
- );
21
+ hashes.push(sha256(`${previous}\u0000${stableJsonStringify(segment)}`));
24
22
  }
25
23
  return Object.freeze(hashes);
26
24
  }
@@ -40,6 +40,10 @@ export function estimatePromptSegments(
40
40
 
41
41
  for (const segment of segments) {
42
42
  exact[breakdownKey(segment.kind)] += estimateText(segment.normalizedText);
43
+ exact[breakdownKey(segment.kind)] += (segment.media ?? []).reduce(
44
+ (total, media) => total + media.planningTokens,
45
+ 0,
46
+ );
43
47
  }
44
48
 
45
49
  const totalTokens = Math.ceil(
@@ -8,6 +8,7 @@ import type {
8
8
  McpToolRawResult,
9
9
  ReadFileRawResult,
10
10
  RecallRawResult,
11
+ SkillRawResult,
11
12
  TaskListRawResult,
12
13
  TaskOutputRawResult,
13
14
  TaskStopRawResult,
@@ -32,6 +33,8 @@ export class ObservationBuilder {
32
33
  return { content: renderReadObservation(input.raw) };
33
34
  case "recall":
34
35
  return { content: renderRecallObservation(input.raw) };
36
+ case "skill":
37
+ return { content: renderSkillObservation(input.raw) };
35
38
  case "write":
36
39
  return { content: renderWriteObservation(input.raw) };
37
40
  case "edit":
@@ -224,6 +227,32 @@ function renderRecallObservation(raw: RecallRawResult): string {
224
227
  return [header, ...hits].join("\n\n");
225
228
  }
226
229
 
230
+ export function renderSkillObservation(raw: SkillRawResult): string {
231
+ if (!raw.ok) {
232
+ return `Skill failed for ${raw.name || "(unknown skill)"} (${raw.errorCode}): ${raw.error}`;
233
+ }
234
+ if (raw.status === "already_loaded") {
235
+ return `Agent Skill ${raw.name} is already loaded for this turn (lifecycle=${raw.lifecycle}).`;
236
+ }
237
+ if (raw.status === "already_active") {
238
+ return `Agent Skill ${raw.name} is already active in the current system surface.`;
239
+ }
240
+
241
+ const content = raw.content + (raw.content.endsWith("\n") ? "" : "\n");
242
+ return `<agent_skill name=${JSON.stringify(raw.name)} scope=${JSON.stringify(raw.scope)}>
243
+ Skill directory: ${raw.directory}
244
+ Relative paths in this skill resolve from that directory.
245
+ Do not modify the skill itself unless the user explicitly asks.
246
+
247
+ <skill_file>
248
+ ${content}</skill_file>
249
+
250
+ <skill_resources truncated=${JSON.stringify(String(raw.resourcesTruncated))}>
251
+ ${raw.resources.length === 0 ? "(no listed resources)" : raw.resources.join("\n")}
252
+ </skill_resources>
253
+ </agent_skill>`;
254
+ }
255
+
227
256
  function renderWriteObservation(raw: WriteFileRawResult): string {
228
257
  if (!raw.ok) {
229
258
  const guidance = raw.requiredReadBeforeWrite
@@ -4,8 +4,8 @@ import { Database } from "bun:sqlite";
4
4
  import type { ToolCall } from "../agent/types";
5
5
  import type { SessionId } from "../ids/runtime-id";
6
6
  import {
7
- assertMatchingContextBudget,
8
7
  createModelContextProfile,
8
+ deriveModelContextBudget,
9
9
  type ModelContextBudget,
10
10
  type ModelContextProfile,
11
11
  } from "../model/model-context-profile";
@@ -25,6 +25,20 @@ import {
25
25
  import { SessionError } from "./session-errors";
26
26
  import { verifySessionSchema } from "./session-schema";
27
27
  import { decodeStoredToolCalls, decodeStoredToolRawResult } from "./session-store";
28
+ import {
29
+ MAX_TIMELINE_PROMPT_CODE_POINTS,
30
+ projectUserMessage,
31
+ truncateUserPromptProjection,
32
+ type UserPromptProjection,
33
+ } from "../agent/user-prompt-projection";
34
+ import {
35
+ parseImageAssetId,
36
+ parseImageAttachmentId,
37
+ validateUserMessage,
38
+ type UserImageAttachment,
39
+ type UserMessage,
40
+ } from "../image/image-types";
41
+ import { userMessageHash } from "../context/protocol-frame";
28
42
 
29
43
  export class ResumeProjectionReader {
30
44
  static async read(input: {
@@ -78,7 +92,7 @@ export class ResumeProjectionReader {
78
92
  const recentTurns = turns.map((turn) => projectTurn(database, turn, policy));
79
93
  const last = recentTurns.at(-1);
80
94
  const terminal = terminalProjection(last);
81
- const context = decodeContextContract(meta.runtime_contract_json);
95
+ const context = decodeContextCompatibility(meta.session_compatibility_json);
82
96
  return {
83
97
  status: terminal.status,
84
98
  sessionId: input.sessionId,
@@ -100,20 +114,19 @@ export class ResumeProjectionReader {
100
114
  }
101
115
  }
102
116
 
103
- function decodeContextContract(value: unknown): {
117
+ function decodeContextCompatibility(value: unknown): {
104
118
  profile: ModelContextProfile;
105
119
  budget: ModelContextBudget;
106
120
  } {
107
121
  if (typeof value !== "string") {
108
- throw new Error("Session runtime contract is missing.");
122
+ throw new Error("Session compatibility contract is missing.");
109
123
  }
110
124
  const parsed = JSON.parse(value) as unknown;
111
125
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
112
- throw new Error("Session runtime contract must be an object.");
126
+ throw new Error("Session compatibility contract must be an object.");
113
127
  }
114
128
  const record = parsed as Record<string, unknown>;
115
129
  const profileRecord = objectValue(record.contextProfile, "contextProfile");
116
- const budgetRecord = objectValue(record.contextBudget, "contextBudget");
117
130
  const profile = createModelContextProfile({
118
131
  contextWindowTokens: positiveInteger(
119
132
  profileRecord.contextWindowTokens,
@@ -124,31 +137,7 @@ function decodeContextContract(value: unknown): {
124
137
  "maxSupportedOutputTokens",
125
138
  ),
126
139
  });
127
- const budget: ModelContextBudget = {
128
- contextWindowTokens: positiveInteger(
129
- budgetRecord.contextWindowTokens,
130
- "budget.contextWindowTokens",
131
- ),
132
- maxSupportedOutputTokens: positiveInteger(
133
- budgetRecord.maxSupportedOutputTokens,
134
- "budget.maxSupportedOutputTokens",
135
- ),
136
- requestMaxOutputTokens: positiveInteger(
137
- budgetRecord.requestMaxOutputTokens,
138
- "requestMaxOutputTokens",
139
- ),
140
- inputBudgetTokens: positiveInteger(
141
- budgetRecord.inputBudgetTokens,
142
- "inputBudgetTokens",
143
- ),
144
- triggerRatio: 0.8,
145
- triggerTokens: positiveInteger(budgetRecord.triggerTokens, "triggerTokens"),
146
- };
147
- if (budgetRecord.triggerRatio !== 0.8) {
148
- throw new Error("Session context triggerRatio must be 0.8.");
149
- }
150
- assertMatchingContextBudget(profile, budget);
151
- return { profile, budget };
140
+ return { profile, budget: deriveModelContextBudget(profile) };
152
141
  }
153
142
 
154
143
  function projectTurn(
@@ -178,11 +167,16 @@ function projectTurn(
178
167
  if (prompt === undefined) {
179
168
  throw new Error(`Turn ${turnId} user message disappeared.`);
180
169
  }
170
+ const userPrompt = truncateUserPromptProjection(
171
+ readUserPromptProjection(database, prompt),
172
+ MAX_TIMELINE_PROMPT_CODE_POINTS,
173
+ );
181
174
  const allItems: TimelineItem[] = [
182
175
  {
183
176
  id: `resume-${requireString(prompt.message_id, "message_id")}`,
184
177
  label: "prompt",
185
- text: boundedText(requireString(prompt.content, "user content")),
178
+ text: userPrompt.text,
179
+ userPrompt,
186
180
  status: "text",
187
181
  },
188
182
  ];
@@ -269,6 +263,71 @@ function projectTurn(
269
263
  };
270
264
  }
271
265
 
266
+ function readUserPromptProjection(
267
+ database: Database,
268
+ prompt: Record<string, unknown>,
269
+ ): UserPromptProjection {
270
+ const messageId = requireString(prompt.message_id, "message_id");
271
+ const text = requireString(prompt.content, "user content");
272
+ const chars = [...text];
273
+ const rows = database
274
+ .query(
275
+ `SELECT mia.attachment_id, mia.position, mia.label, mia.range_start,
276
+ mia.range_end, mia.original_name, ia.asset_id, ia.mime_type,
277
+ ia.byte_length, ia.width, ia.height
278
+ FROM message_image_attachments mia
279
+ JOIN image_assets ia ON ia.asset_id = mia.asset_id
280
+ WHERE mia.message_id = ? ORDER BY mia.position`,
281
+ )
282
+ .all(messageId) as Array<Record<string, unknown>>;
283
+ const attachments = rows.map((row, position): UserImageAttachment => {
284
+ if (safeNumber(row.position, "image position") !== position) {
285
+ throw new Error(`Image positions for message ${messageId} are not continuous.`);
286
+ }
287
+ const start = safeNumber(row.range_start, "image range_start");
288
+ const end = safeNumber(row.range_end, "image range_end");
289
+ const label = requireString(row.label, "image label");
290
+ if (
291
+ start < 0 ||
292
+ end <= start ||
293
+ end > chars.length ||
294
+ chars.slice(start, end).join("") !== label
295
+ ) {
296
+ throw new Error(`Image range for message ${messageId} is invalid.`);
297
+ }
298
+ return Object.freeze({
299
+ attachmentId: parseImageAttachmentId(
300
+ requireString(row.attachment_id, "image attachment_id"),
301
+ ),
302
+ assetId: parseImageAssetId(requireString(row.asset_id, "image asset_id")),
303
+ label,
304
+ range: Object.freeze({ start, end }),
305
+ mimeType: enumValue(
306
+ row.mime_type,
307
+ ["image/png", "image/jpeg", "image/webp"] as const,
308
+ "image mime_type",
309
+ ),
310
+ byteLength: positiveInteger(row.byte_length, "image byte_length"),
311
+ width: positiveInteger(row.width, "image width"),
312
+ height: positiveInteger(row.height, "image height"),
313
+ originalName: requireString(row.original_name, "image original_name"),
314
+ });
315
+ });
316
+ const message: UserMessage = Object.freeze({
317
+ role: "user",
318
+ content: text,
319
+ ...(attachments.length === 0 ? {} : { attachments: Object.freeze(attachments) }),
320
+ });
321
+ validateUserMessage(message);
322
+ if (
323
+ userMessageHash(message) !==
324
+ requireString(prompt.content_sha256, "user content_sha256")
325
+ ) {
326
+ throw new Error(`User message ${messageId} failed its integrity check.`);
327
+ }
328
+ return projectUserMessage(message);
329
+ }
330
+
272
331
  function projectIteration(
273
332
  row: Record<string, unknown>,
274
333
  turnId: string,
@@ -586,10 +645,11 @@ function objectValue(value: unknown, name: string): Record<string, unknown> {
586
645
  }
587
646
 
588
647
  function positiveInteger(value: unknown, name: string): number {
589
- if (!Number.isSafeInteger(value) || (value as number) < 1) {
648
+ const number = typeof value === "bigint" ? Number(value) : value;
649
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1) {
590
650
  throw new Error(`${name} must be a positive integer.`);
591
651
  }
592
- return value as number;
652
+ return number;
593
653
  }
594
654
 
595
655
  function timestamp(value: unknown, name: string): string {
@@ -178,7 +178,7 @@ async function readSummary(
178
178
  return {
179
179
  sessionId,
180
180
  modelName: requireString(row.model_name, "model_name"),
181
- ...profileNameFromRuntimeContract(row.runtime_contract_json),
181
+ ...profileNameFromCompatibility(row.session_compatibility_json),
182
182
  createdAt: requireTimestamp(row.created_at, "created_at"),
183
183
  updatedAt: requireTimestamp(row.updated_at, "updated_at"),
184
184
  turnCount,
@@ -206,16 +206,16 @@ async function readSummary(
206
206
  }
207
207
  }
208
208
 
209
- function profileNameFromRuntimeContract(value: unknown): { profileName?: string } {
209
+ function profileNameFromCompatibility(value: unknown): { profileName?: string } {
210
210
  if (value === null) {
211
211
  return {};
212
212
  }
213
213
  if (typeof value !== "string") {
214
- throw new Error("Session runtime contract must be JSON text.");
214
+ throw new Error("Session compatibility contract must be JSON text.");
215
215
  }
216
216
  const parsed = JSON.parse(value) as unknown;
217
217
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
218
- throw new Error("Session runtime contract must be an object.");
218
+ throw new Error("Session compatibility contract must be an object.");
219
219
  }
220
220
  const profileName = (parsed as Record<string, unknown>).profileName;
221
221
  if (profileName === undefined) {
@@ -16,7 +16,7 @@ export type SessionErrorCode =
16
16
  | "SESSION_SCHEMA_INVALID"
17
17
  | "SESSION_INTEGRITY_FAILED"
18
18
  | "SESSION_WORKSPACE_MISMATCH"
19
- | "SESSION_RUNTIME_MISMATCH"
19
+ | "SESSION_COMPATIBILITY_MISMATCH"
20
20
  | "SESSION_PROTOCOL_INVALID"
21
21
  | "SESSION_RECALL_INDEX_INVALID"
22
22
  | "SESSION_READ_FAILED"