tinker-agent 1.0.65

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 (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,178 @@
1
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
4
+ import { defineToolExecutor } from "../tools/types";
5
+ import type {
6
+ JsonSchema,
7
+ McpToolRawResult,
8
+ ToolExecutionContext,
9
+ ToolExecutor,
10
+ } from "../tools/types";
11
+
12
+ export const MCP_TOOL_NAME_PREFIX = "mcp__";
13
+ export const DEFAULT_MCP_TIMEOUT_MS = 60_000;
14
+ export const DEFAULT_MCP_MAX_OBSERVATION_CHARS = 40_000;
15
+
16
+ export function mcpToolName(serverName: string, toolName: string): string {
17
+ return `${MCP_TOOL_NAME_PREFIX}${serverName}__${toolName}`;
18
+ }
19
+
20
+ export function isMcpToolName(name: string): boolean {
21
+ return name.startsWith(MCP_TOOL_NAME_PREFIX);
22
+ }
23
+
24
+ export function sanitizeInputSchema(schema: unknown): JsonSchema {
25
+ if (
26
+ typeof schema !== "object" ||
27
+ schema === null ||
28
+ Array.isArray(schema) ||
29
+ (schema as Record<string, unknown>).type !== "object"
30
+ ) {
31
+ return { type: "object", properties: {} };
32
+ }
33
+
34
+ const rest = { ...(schema as Record<string, unknown>) };
35
+ delete rest.$schema;
36
+ return rest;
37
+ }
38
+
39
+ export type CreateMcpToolExecutorOptions = {
40
+ client: Client;
41
+ serverName: string;
42
+ tool: Tool;
43
+ timeoutMs?: number;
44
+ maxObservationChars?: number;
45
+ };
46
+
47
+ export function createMcpToolExecutor(
48
+ options: CreateMcpToolExecutorOptions,
49
+ ): ToolExecutor {
50
+ const toolName = mcpToolName(options.serverName, options.tool.name);
51
+ const timeoutMs = options.timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
52
+ const maxObservationChars =
53
+ options.maxObservationChars ?? DEFAULT_MCP_MAX_OBSERVATION_CHARS;
54
+
55
+ const base = {
56
+ toolName,
57
+ serverName: options.serverName,
58
+ serverToolName: options.tool.name,
59
+ };
60
+
61
+ return defineToolExecutor("mcp", {
62
+ definition: {
63
+ name: toolName,
64
+ description:
65
+ options.tool.description ??
66
+ `MCP tool ${options.tool.name} from server ${options.serverName}`,
67
+ parameters: sanitizeInputSchema(options.tool.inputSchema),
68
+ },
69
+ async execute(
70
+ args: unknown,
71
+ _call,
72
+ context: ToolExecutionContext,
73
+ ): Promise<McpToolRawResult> {
74
+ throwIfTurnCancelled(context.signal);
75
+ if (args !== undefined && !isRecord(args)) {
76
+ return {
77
+ ok: false,
78
+ ...base,
79
+ error: `MCP tool arguments must be an object; received ${JSON.stringify(args)}`,
80
+ };
81
+ }
82
+
83
+ let result;
84
+
85
+ try {
86
+ result = await options.client.callTool(
87
+ {
88
+ name: options.tool.name,
89
+ arguments: args ?? {},
90
+ },
91
+ undefined,
92
+ { timeout: timeoutMs, signal: context.signal },
93
+ );
94
+ } catch (error) {
95
+ if (context.signal.aborted) {
96
+ throw cancellationError(context.signal, error);
97
+ }
98
+
99
+ return {
100
+ ok: false,
101
+ ...base,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ };
104
+ }
105
+
106
+ const blocks = Array.isArray(result.content) ? result.content : [];
107
+ const rendered = renderContentBlocks(blocks, maxObservationChars);
108
+ const isError = result.isError === true;
109
+
110
+ return {
111
+ ok: !isError,
112
+ ...base,
113
+ isError,
114
+ text: rendered.text,
115
+ truncated: rendered.truncated,
116
+ contentBlockCount: blocks.length,
117
+ };
118
+ },
119
+ });
120
+ }
121
+
122
+ function renderContentBlocks(
123
+ blocks: unknown[],
124
+ maxChars: number,
125
+ ): { text: string; truncated: boolean } {
126
+ const parts: string[] = [];
127
+
128
+ for (const block of blocks) {
129
+ if (!isRecord(block)) {
130
+ continue;
131
+ }
132
+
133
+ if (block.type === "text" && typeof block.text === "string") {
134
+ parts.push(block.text);
135
+ continue;
136
+ }
137
+
138
+ parts.push(renderNonTextBlockPlaceholder(block));
139
+ }
140
+
141
+ const text = parts.join("\n");
142
+
143
+ if (text.length <= maxChars) {
144
+ return { text, truncated: false };
145
+ }
146
+
147
+ return { text: text.slice(0, maxChars), truncated: true };
148
+ }
149
+
150
+ function renderNonTextBlockPlaceholder(block: Record<string, unknown>): string {
151
+ const type = typeof block.type === "string" ? block.type : "unknown";
152
+
153
+ if (type === "image" || type === "audio") {
154
+ const mimeType = typeof block.mimeType === "string" ? ` ${block.mimeType}` : "";
155
+ return `[${type}${mimeType} content omitted]`;
156
+ }
157
+
158
+ if (type === "resource_link") {
159
+ const uri = typeof block.uri === "string" ? ` ${block.uri}` : "";
160
+ return `[resource link${uri}]`;
161
+ }
162
+
163
+ if (type === "resource") {
164
+ const resource = isRecord(block.resource) ? block.resource : {};
165
+ if (typeof resource.text === "string") {
166
+ return resource.text;
167
+ }
168
+
169
+ const uri = typeof resource.uri === "string" ? ` ${resource.uri}` : "";
170
+ return `[resource${uri} content omitted]`;
171
+ }
172
+
173
+ return `[${type} content omitted]`;
174
+ }
175
+
176
+ function isRecord(value: unknown): value is Record<string, unknown> {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
178
+ }
@@ -0,0 +1,68 @@
1
+ import type { ContextRevisionId } from "../ids/runtime-id";
2
+ import type { PreparedModelRequest } from "./model-client";
3
+ import {
4
+ lastPromptPrefixHash,
5
+ promptPrefixHashes,
6
+ type PromptPrefixFingerprint,
7
+ } from "./prompt-prefix-hash";
8
+
9
+ export type CommittedPrefixAnchor = PromptPrefixFingerprint & {
10
+ readonly revisionId: ContextRevisionId;
11
+ };
12
+
13
+ export class CommittedPrefixAuditError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = "CommittedPrefixAuditError";
17
+ }
18
+ }
19
+
20
+ export class CommittedPrefixAuditor {
21
+ private anchor?: CommittedPrefixAnchor;
22
+
23
+ audit(
24
+ revisionId: ContextRevisionId,
25
+ prepared: PreparedModelRequest,
26
+ ): CommittedPrefixAnchor {
27
+ const hashes = promptPrefixHashes(
28
+ prepared.requestConfigHash,
29
+ prepared.promptSegments,
30
+ );
31
+ const current = Object.freeze<PromptPrefixFingerprint>({
32
+ requestConfigHash: prepared.requestConfigHash,
33
+ toolSchemaHash: prepared.toolSchemaHash,
34
+ segmentCount: prepared.promptSegments.length,
35
+ prefixHash: lastPromptPrefixHash(hashes),
36
+ });
37
+ const previous = this.anchor;
38
+ if (previous !== undefined && previous.revisionId === revisionId) {
39
+ if (previous.requestConfigHash !== current.requestConfigHash) {
40
+ throw new CommittedPrefixAuditError(
41
+ "Committed request config changed within one context revision.",
42
+ );
43
+ }
44
+ if (previous.toolSchemaHash !== current.toolSchemaHash) {
45
+ throw new CommittedPrefixAuditError(
46
+ "Committed tool schema changed within one context revision.",
47
+ );
48
+ }
49
+ if (current.segmentCount < previous.segmentCount) {
50
+ throw new CommittedPrefixAuditError(
51
+ "Committed prompt segment count shrank within one context revision.",
52
+ );
53
+ }
54
+ if (hashes[previous.segmentCount] !== previous.prefixHash) {
55
+ throw new CommittedPrefixAuditError(
56
+ "Committed prompt prefix changed within one context revision.",
57
+ );
58
+ }
59
+ }
60
+ const anchor = Object.freeze({ revisionId, ...current });
61
+ this.anchor = anchor;
62
+ return anchor;
63
+ }
64
+
65
+ current(): CommittedPrefixAnchor | undefined {
66
+ return this.anchor;
67
+ }
68
+ }
@@ -0,0 +1,280 @@
1
+ import type { AgentMessage, AssistantMessage } from "../agent/types";
2
+ import { cancellationError } from "../agent/turn-cancellation";
3
+ import type { ModelContextBudget } from "./model-context-profile";
4
+ import type {
5
+ ModelClient,
6
+ ModelRequestInput,
7
+ ModelRequestOptions,
8
+ ModelRequestOutput,
9
+ PreparedModelRequest,
10
+ PreparedPromptSegment,
11
+ } from "./model-client";
12
+ import { sha256, stableJsonStringify } from "./model-request-preflight";
13
+ import { estimatePromptSegments } from "./token-estimator";
14
+
15
+ export class FakeModelClient implements ModelClient {
16
+ private steps = 0;
17
+ private readonly preparedInputs = new WeakMap<object, ModelRequestInput>();
18
+
19
+ constructor(
20
+ private readonly mode: string,
21
+ private readonly options: {
22
+ model: string;
23
+ contextBudget: ModelContextBudget;
24
+ },
25
+ ) {}
26
+
27
+ prepare(input: ModelRequestInput): PreparedModelRequest {
28
+ const toolSegments = input.tools.map(
29
+ (tool): PreparedPromptSegment => ({
30
+ kind: "tool_schema",
31
+ normalizedText: stableJsonStringify(tool),
32
+ }),
33
+ );
34
+ const messageSegments = input.messages.map(toPromptSegment);
35
+ const requestConfigHash = sha256(
36
+ stableJsonStringify({
37
+ adapter: "fake-v1",
38
+ mode: this.mode,
39
+ model: this.options.model,
40
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
41
+ }),
42
+ );
43
+ const prepared: PreparedModelRequest = Object.freeze({
44
+ provider: "fake",
45
+ model: this.options.model,
46
+ payload: Object.freeze({
47
+ messages: Object.freeze([...input.messages]),
48
+ tools: Object.freeze([...input.tools]),
49
+ maxTokens: this.options.contextBudget.requestMaxOutputTokens,
50
+ }),
51
+ promptSegments: Object.freeze([...toolSegments, ...messageSegments]),
52
+ requestConfigHash,
53
+ toolSchemaHash: sha256(
54
+ toolSegments.map((segment) => segment.normalizedText).join("\n"),
55
+ ),
56
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
57
+ assistantReplaySegments: (message: AssistantMessage) => [
58
+ toPromptSegment(message),
59
+ ],
60
+ });
61
+ this.preparedInputs.set(prepared, {
62
+ messages: [...input.messages],
63
+ tools: [...input.tools],
64
+ });
65
+ return prepared;
66
+ }
67
+
68
+ async request(
69
+ prepared: PreparedModelRequest,
70
+ options: ModelRequestOptions,
71
+ ): Promise<ModelRequestOutput> {
72
+ const input = this.preparedInputs.get(prepared);
73
+ if (input === undefined) {
74
+ throw new Error("Fake model request was not prepared by this client.");
75
+ }
76
+ this.steps += 1;
77
+
78
+ if (this.mode === "write-notes") {
79
+ return this.writeNotes(input, prepared, options);
80
+ }
81
+ if (this.mode === "wait-for-cancel") {
82
+ return waitForCancellation(options.signal);
83
+ }
84
+ if (this.mode === "recall-smoke") {
85
+ return this.recallSmoke(input, prepared, options);
86
+ }
87
+
88
+ return outputWithUsage(
89
+ prepared,
90
+ {
91
+ role: "assistant",
92
+ content: `Fake model received: ${lastUserMessage(input.messages)}`,
93
+ },
94
+ "stop",
95
+ );
96
+ }
97
+
98
+ private writeNotes(
99
+ input: ModelRequestInput,
100
+ prepared: PreparedModelRequest,
101
+ options: ModelRequestOptions,
102
+ ): ModelRequestOutput {
103
+ const sawToolResult = input.messages.some((message) => message.role === "tool");
104
+
105
+ if (!sawToolResult) {
106
+ if (options.identity === undefined) {
107
+ throw new Error("Fake tool call requires an iteration identity context.");
108
+ }
109
+ return outputWithUsage(
110
+ prepared,
111
+ {
112
+ role: "assistant",
113
+ content: "I will create notes.txt.",
114
+ toolCalls: [
115
+ {
116
+ ...options.identity.runtimeSession.createToolCall(
117
+ options.identity.iteration,
118
+ 1,
119
+ ),
120
+ providerToolCallId: "fake-write-notes-1",
121
+ name: "Write",
122
+ args: {
123
+ file_path: "notes.txt",
124
+ content: "hello.\n",
125
+ },
126
+ },
127
+ ],
128
+ },
129
+ "tool_calls",
130
+ );
131
+ }
132
+
133
+ return outputWithUsage(
134
+ prepared,
135
+ {
136
+ role: "assistant",
137
+ content: "Created notes.txt with one line: hello.",
138
+ },
139
+ "stop",
140
+ );
141
+ }
142
+
143
+ private recallSmoke(
144
+ input: ModelRequestInput,
145
+ prepared: PreparedModelRequest,
146
+ options: ModelRequestOptions,
147
+ ): ModelRequestOutput {
148
+ if (options.identity === undefined) {
149
+ throw new Error("Fake Recall call requires an iteration identity context.");
150
+ }
151
+ const lastUserIndex = lastMessageIndex(input.messages, "user");
152
+ const latestRecallResult = input.messages
153
+ .slice(lastUserIndex + 1)
154
+ .reverse()
155
+ .find(
156
+ (message): message is Extract<AgentMessage, { role: "tool" }> =>
157
+ message.role === "tool" && message.name === "Recall",
158
+ );
159
+ if (latestRecallResult === undefined) {
160
+ return outputWithUsage(
161
+ prepared,
162
+ {
163
+ role: "assistant",
164
+ toolCalls: [
165
+ {
166
+ ...options.identity.runtimeSession.createToolCall(
167
+ options.identity.iteration,
168
+ 1,
169
+ ),
170
+ providerToolCallId: "fake-recall-search-1",
171
+ name: "Recall",
172
+ args: { mode: "search", query: "recall-smoke-marker" },
173
+ },
174
+ ],
175
+ },
176
+ "tool_calls",
177
+ );
178
+ }
179
+ if (latestRecallResult.content.startsWith("Recall searched")) {
180
+ const source = latestRecallResult.content.match(
181
+ /^source=(ctx:\/\/message\/[0-9a-f-]+)$/m,
182
+ )?.[1];
183
+ if (source === undefined) {
184
+ throw new Error("Fake Recall search did not return a source.");
185
+ }
186
+ return outputWithUsage(
187
+ prepared,
188
+ {
189
+ role: "assistant",
190
+ toolCalls: [
191
+ {
192
+ ...options.identity.runtimeSession.createToolCall(
193
+ options.identity.iteration,
194
+ 1,
195
+ ),
196
+ providerToolCallId: "fake-recall-get-1",
197
+ name: "Recall",
198
+ args: { mode: "get", source },
199
+ },
200
+ ],
201
+ },
202
+ "tool_calls",
203
+ );
204
+ }
205
+ if (!latestRecallResult.content.includes("recall-smoke-marker")) {
206
+ throw new Error("Fake Recall get did not recover the expected marker.");
207
+ }
208
+ return outputWithUsage(
209
+ prepared,
210
+ {
211
+ role: "assistant",
212
+ content: "Recall search and get completed.",
213
+ },
214
+ "stop",
215
+ );
216
+ }
217
+ }
218
+
219
+ function outputWithUsage(
220
+ prepared: PreparedModelRequest,
221
+ message: AssistantMessage,
222
+ finishReason: string,
223
+ ): ModelRequestOutput {
224
+ const promptTokens = estimatePromptSegments(prepared.promptSegments).totalTokens;
225
+ const completionTokens = Math.max(
226
+ 1,
227
+ estimatePromptSegments(prepared.assistantReplaySegments(message)).totalTokens,
228
+ );
229
+ return {
230
+ message,
231
+ finishReason,
232
+ usage: {
233
+ promptTokens,
234
+ completionTokens,
235
+ totalTokens: promptTokens + completionTokens,
236
+ },
237
+ };
238
+ }
239
+
240
+ function waitForCancellation(signal: AbortSignal): Promise<ModelRequestOutput> {
241
+ return new Promise((_resolve, reject) => {
242
+ const abort = () => reject(cancellationError(signal));
243
+ if (signal.aborted) {
244
+ abort();
245
+ return;
246
+ }
247
+ signal.addEventListener("abort", abort, { once: true });
248
+ });
249
+ }
250
+
251
+ function lastUserMessage(messages: AgentMessage[]): string {
252
+ const users = messages.filter(
253
+ (message): message is { role: "user"; content: string } => message.role === "user",
254
+ );
255
+ return users.at(-1)?.content ?? "";
256
+ }
257
+
258
+ function lastMessageIndex(
259
+ messages: AgentMessage[],
260
+ role: AgentMessage["role"],
261
+ ): number {
262
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
263
+ if (messages[index]?.role === role) {
264
+ return index;
265
+ }
266
+ }
267
+ return -1;
268
+ }
269
+
270
+ function toPromptSegment(message: AgentMessage): PreparedPromptSegment {
271
+ return {
272
+ kind:
273
+ message.role === "system"
274
+ ? "kernel"
275
+ : message.role === "user"
276
+ ? "user"
277
+ : message.role,
278
+ normalizedText: stableJsonStringify(message),
279
+ };
280
+ }
@@ -0,0 +1,64 @@
1
+ import type { AgentMessage, AssistantMessage, IterationIdentity } from "../agent/types";
2
+ import type { RuntimeSessionContext } from "../agent/runtime-session";
3
+ import type { ToolDefinition } from "../tools/types";
4
+
5
+ export interface ModelClient {
6
+ prepare(input: ModelRequestInput): PreparedModelRequest;
7
+ request(
8
+ prepared: PreparedModelRequest,
9
+ options: ModelRequestOptions,
10
+ ): Promise<ModelRequestOutput>;
11
+ }
12
+
13
+ export type ModelRequestOptions = {
14
+ signal: AbortSignal;
15
+ identity?: {
16
+ iteration: IterationIdentity;
17
+ runtimeSession: RuntimeSessionContext;
18
+ };
19
+ };
20
+
21
+ export type ModelRequestInput = {
22
+ messages: AgentMessage[];
23
+ tools: ToolDefinition[];
24
+ };
25
+
26
+ export type PreparedPromptSegmentKind =
27
+ | "kernel"
28
+ | "user"
29
+ | "assistant"
30
+ | "tool"
31
+ | "tool_schema"
32
+ | "protocol";
33
+
34
+ export type PreparedPromptSegment = {
35
+ kind: PreparedPromptSegmentKind;
36
+ normalizedText: string;
37
+ };
38
+
39
+ export type PreparedModelRequest = {
40
+ provider: string;
41
+ model: string;
42
+ payload: unknown;
43
+ promptSegments: readonly PreparedPromptSegment[];
44
+ requestConfigHash: string;
45
+ toolSchemaHash: string;
46
+ requestMaxOutputTokens: number;
47
+ assistantReplaySegments(message: AssistantMessage): PreparedPromptSegment[];
48
+ };
49
+
50
+ export type ModelRequestOutput = {
51
+ message: AssistantMessage;
52
+ finishReason?: string;
53
+ usage: ModelUsage;
54
+ rawResponse?: unknown;
55
+ };
56
+
57
+ export type ModelUsage = {
58
+ promptTokens: number;
59
+ completionTokens: number;
60
+ totalTokens: number;
61
+ promptCacheHitTokens?: number;
62
+ promptCacheMissTokens?: number;
63
+ reasoningTokens?: number;
64
+ };