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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- 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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
|
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
|
-
| "
|
|
19
|
+
| "SESSION_COMPATIBILITY_MISMATCH"
|
|
20
20
|
| "SESSION_PROTOCOL_INVALID"
|
|
21
21
|
| "SESSION_RECALL_INDEX_INVALID"
|
|
22
22
|
| "SESSION_READ_FAILED"
|