tinker-agent 1.9.0 → 1.11.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 (52) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +64 -6
  3. package/package.json +1 -1
  4. package/src/agent/loop.ts +17 -0
  5. package/src/agent/runtime-session.ts +341 -1
  6. package/src/agent/session-ledger.ts +100 -3
  7. package/src/cli/config.ts +11 -2
  8. package/src/cli/model-profiles.ts +58 -0
  9. package/src/cli/public-config-contract.ts +73 -7
  10. package/src/cli/run-runner.ts +4 -1
  11. package/src/cli/runner-dependencies.ts +28 -4
  12. package/src/cli/tui-memory.ts +4 -0
  13. package/src/cli/tui-runner.tsx +8 -1
  14. package/src/context/context-automation-policy.ts +22 -21
  15. package/src/context/context-manager.ts +91 -15
  16. package/src/context/context-policy.ts +0 -2
  17. package/src/context/context-swap-renderer.ts +1 -1
  18. package/src/context/prefix-retirement-planner.ts +58 -8
  19. package/src/context/recall-retirement-contract.ts +5 -4
  20. package/src/context/swap-planner.ts +33 -27
  21. package/src/events/observation-text-log.ts +4 -0
  22. package/src/events/stdout-event-printer.ts +5 -0
  23. package/src/events/types.ts +5 -1
  24. package/src/model/fake-model-client.ts +55 -16
  25. package/src/model/model-api.ts +12 -0
  26. package/src/model/model-client.ts +9 -1
  27. package/src/model/moonshot-input-token-estimator.ts +5 -1
  28. package/src/model/openai-chat-mapping.ts +2 -24
  29. package/src/model/openai-chat-model-client.ts +18 -294
  30. package/src/model/openai-image-mapping.ts +20 -0
  31. package/src/model/openai-model-utils.ts +304 -0
  32. package/src/model/openai-responses-mapping.ts +532 -0
  33. package/src/model/openai-responses-model-client.ts +295 -0
  34. package/src/model/openai-responses-stream.ts +96 -0
  35. package/src/model/openai-responses-token-estimator.ts +155 -0
  36. package/src/model/reasoning-effort.ts +60 -0
  37. package/src/session/session-catalog.ts +2 -2
  38. package/src/session/session-history-reader.ts +6 -1
  39. package/src/session/session-schema.ts +268 -4
  40. package/src/session/session-store.ts +134 -26
  41. package/src/skills/skill-context.ts +2 -2
  42. package/src/tools/bounded-output-preview.ts +276 -0
  43. package/src/tools/recall.ts +67 -36
  44. package/src/tools/registry.ts +7 -2
  45. package/src/tools/task-output-snapshot.ts +6 -22
  46. package/src/tools/task-output.ts +23 -27
  47. package/src/tui/app.tsx +153 -11
  48. package/src/tui/components/footer.tsx +6 -1
  49. package/src/tui/components/prompt-input.tsx +9 -1
  50. package/src/tui/event-store.ts +15 -0
  51. package/src/tui/slash-commands.ts +20 -0
  52. package/src/tui/tui-session-controller.ts +14 -0
@@ -0,0 +1,295 @@
1
+ import OpenAI from "openai";
2
+ import type {
3
+ ResponseCreateParamsNonStreaming,
4
+ ResponseCreateParamsStreaming,
5
+ } from "openai/resources/responses/responses";
6
+ import type { AssistantMessage } from "../agent/types";
7
+ import {
8
+ IMAGE_INPUT_POLICY,
9
+ IMAGE_INPUT_POLICY_VERSION,
10
+ } from "../image/image-input-policy";
11
+ import type { InputTokenEstimator } from "./input-token-estimator";
12
+ import type { ModelContextBudget } from "./model-context-profile";
13
+ import { ProviderResponseError } from "./model-client";
14
+ import type {
15
+ MaterializedModelRequest,
16
+ ModelClient,
17
+ ModelMaterializeOptions,
18
+ ModelMessageProtocol,
19
+ ModelRequestInput,
20
+ ModelRequestOptions,
21
+ ModelRequestOutput,
22
+ PreparedModelRequest,
23
+ PreparedPromptSegment,
24
+ } from "./model-client";
25
+ import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
26
+ import {
27
+ deepFreeze,
28
+ imageUserSegment,
29
+ materializeOpenAIRequest,
30
+ normalizedEndpointPolicy,
31
+ sanitizedProviderError,
32
+ segmentKind,
33
+ } from "./openai-model-utils";
34
+ import {
35
+ fromOpenAIResponse,
36
+ toOpenAIResponsesInput,
37
+ toOpenAIResponsesItems,
38
+ toOpenAIResponsesTools,
39
+ } from "./openai-responses-mapping";
40
+ import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
41
+ import { responsesPayloadForChatTokenEstimator } from "./openai-responses-token-estimator";
42
+ import type { ReasoningEffortController } from "./reasoning-effort";
43
+ import { sha256, stableJsonStringify } from "./model-request-preflight";
44
+
45
+ const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-v1";
46
+ const OPENAI_RESPONSES_TIMEOUT_MS = 30 * 60 * 1_000;
47
+
48
+ export class OpenAIResponsesModelClient implements ModelClient {
49
+ readonly messageProtocol: ModelMessageProtocol = Object.freeze({
50
+ adapter: "openai-responses",
51
+ serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
52
+ });
53
+ readonly inputTokenEstimator?: InputTokenEstimator;
54
+ readonly inputModalities: readonly ("text" | "image")[];
55
+ readonly reasoningEffort?: ReasoningEffortController;
56
+ private readonly client: OpenAI;
57
+ private readonly preparedRequests = new WeakSet<object>();
58
+ private readonly materializedRequests = new WeakSet<object>();
59
+ private readonly provider: string;
60
+ private readonly stream: boolean;
61
+
62
+ constructor(
63
+ private readonly options: {
64
+ apiKey: string;
65
+ contextBudget: ModelContextBudget;
66
+ baseURL?: string;
67
+ inputModalities?: readonly ("text" | "image")[];
68
+ tokenEstimator?: {
69
+ kind: "moonshot-estimate-token-count-v1";
70
+ model: string;
71
+ apiBase: string;
72
+ apiKey: string;
73
+ timeoutMs: number;
74
+ maxRetries: 0;
75
+ };
76
+ model: string;
77
+ providerName?: string;
78
+ reasoningEffort?: ReasoningEffortController;
79
+ stream?: boolean;
80
+ timeoutMs?: number;
81
+ fetch?: typeof fetch;
82
+ },
83
+ ) {
84
+ this.provider = options.providerName ?? "responses-compatible";
85
+ this.stream = options.stream ?? true;
86
+ this.reasoningEffort = options.reasoningEffort;
87
+ this.inputModalities = Object.freeze([...(options.inputModalities ?? ["text"])]);
88
+ const supportsImages = this.inputModalities.includes("image");
89
+ if (!this.inputModalities.includes("text")) {
90
+ throw new Error('OpenAI Responses input modalities must include "text".');
91
+ }
92
+ if (supportsImages && options.tokenEstimator === undefined) {
93
+ throw new Error("Image-capable OpenAI Responses requires a token estimator.");
94
+ }
95
+ this.client = new OpenAI({
96
+ apiKey: options.apiKey,
97
+ baseURL: options.baseURL,
98
+ timeout: options.timeoutMs ?? OPENAI_RESPONSES_TIMEOUT_MS,
99
+ maxRetries: 0,
100
+ fetch: options.fetch,
101
+ });
102
+ if (options.tokenEstimator !== undefined) {
103
+ this.inputTokenEstimator = new MoonshotInputTokenEstimator({
104
+ apiKey: options.tokenEstimator.apiKey,
105
+ baseURL: options.tokenEstimator.apiBase,
106
+ model: options.tokenEstimator.model,
107
+ timeoutMs: options.tokenEstimator.timeoutMs,
108
+ fetch: options.fetch,
109
+ payloadMapper: responsesPayloadForChatTokenEstimator,
110
+ });
111
+ }
112
+ }
113
+
114
+ prepare(input: ModelRequestInput): PreparedModelRequest {
115
+ const itemsByMessage = input.messages.map((message) =>
116
+ toOpenAIResponsesItems(message),
117
+ );
118
+ const items = toOpenAIResponsesInput(input.messages);
119
+ const tools =
120
+ input.tools.length > 0 ? toOpenAIResponsesTools(input.tools) : undefined;
121
+ const reasoningEffort = this.reasoningEffort?.snapshot().effort;
122
+ const payload = deepFreeze({
123
+ model: this.options.model,
124
+ input: items,
125
+ ...(tools === undefined ? {} : { tools, tool_choice: "auto" as const }),
126
+ ...(reasoningEffort === undefined
127
+ ? {}
128
+ : { reasoning: { effort: reasoningEffort } }),
129
+ max_output_tokens: this.options.contextBudget.requestMaxOutputTokens,
130
+ store: false as const,
131
+ ...(this.stream ? { stream: true as const } : {}),
132
+ });
133
+ const toolSegments = (tools ?? []).map(
134
+ (tool): PreparedPromptSegment => ({
135
+ kind: "tool_schema",
136
+ normalizedText: stableJsonStringify(tool),
137
+ }),
138
+ );
139
+ const messageSegments = input.messages.map(
140
+ (message, index): PreparedPromptSegment =>
141
+ message.role === "user" && message.attachments !== undefined
142
+ ? imageUserSegment(message)
143
+ : {
144
+ kind: segmentKind(message.role),
145
+ normalizedText: stableJsonStringify(itemsByMessage[index]),
146
+ },
147
+ );
148
+ const mediaOccurrenceCount = messageSegments.reduce(
149
+ (total, segment) => total + (segment.media?.length ?? 0),
150
+ 0,
151
+ );
152
+ const requestConfigHash = sha256(
153
+ stableJsonStringify({
154
+ adapter: this.messageProtocol.adapter,
155
+ serializationVersion: this.messageProtocol.serializationVersion,
156
+ endpoint: normalizedEndpointPolicy(this.options.baseURL),
157
+ model: this.options.model,
158
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
159
+ stream: this.stream,
160
+ inputModalities: this.inputModalities,
161
+ requestPolicy: { store: false, toolChoice: "auto" },
162
+ imagePolicy: {
163
+ version: IMAGE_INPUT_POLICY_VERSION,
164
+ ...IMAGE_INPUT_POLICY,
165
+ },
166
+ ...(this.inputTokenEstimator === undefined
167
+ ? {}
168
+ : { tokenEstimator: this.inputTokenEstimator.compatibility }),
169
+ }),
170
+ );
171
+ const prepared: PreparedModelRequest = {
172
+ provider: this.provider,
173
+ model: this.options.model,
174
+ payload,
175
+ promptSegments: Object.freeze([...toolSegments, ...messageSegments]),
176
+ requestConfigHash,
177
+ toolSchemaHash: sha256(
178
+ toolSegments.map((segment) => segment.normalizedText).join("\n"),
179
+ ),
180
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
181
+ mediaOccurrenceCount,
182
+ assistantReplaySegments: (message) => this.assistantReplaySegments(message),
183
+ };
184
+ Object.freeze(prepared);
185
+ this.preparedRequests.add(prepared);
186
+ return prepared;
187
+ }
188
+
189
+ async materialize(
190
+ prepared: PreparedModelRequest,
191
+ options: ModelMaterializeOptions,
192
+ ): Promise<MaterializedModelRequest> {
193
+ this.assertPrepared(prepared);
194
+ if (prepared.mediaOccurrenceCount > 0 && !this.inputModalities.includes("image")) {
195
+ throw new Error("Current model profile does not support image input.");
196
+ }
197
+ const materialized = await materializeOpenAIRequest(prepared, options);
198
+ this.materializedRequests.add(materialized);
199
+ return materialized;
200
+ }
201
+
202
+ async request(
203
+ prepared: PreparedModelRequest,
204
+ options: ModelRequestOptions,
205
+ ): Promise<ModelRequestOutput> {
206
+ this.assertPrepared(prepared);
207
+ if (prepared.mediaOccurrenceCount > 0 && !this.materializedRequests.has(prepared)) {
208
+ throw new Error("Image request must be materialized before provider dispatch.");
209
+ }
210
+ const response = this.stream
211
+ ? await this.requestStreaming(prepared, options)
212
+ : await this.requestNonStreaming(prepared, options.signal);
213
+ return fromOpenAIResponse(response, {
214
+ identity: options.identity,
215
+ provider: this.provider,
216
+ model: this.options.model,
217
+ });
218
+ }
219
+
220
+ private async requestStreaming(
221
+ prepared: PreparedModelRequest,
222
+ options: ModelRequestOptions,
223
+ ): Promise<unknown> {
224
+ const accumulator = new OpenAIResponsesStreamAccumulator({
225
+ provider: this.provider,
226
+ model: this.options.model,
227
+ });
228
+ try {
229
+ const stream = await this.client.responses.create(
230
+ prepared.payload as ResponseCreateParamsStreaming,
231
+ { signal: options.signal },
232
+ );
233
+ for await (const event of stream) {
234
+ const content = accumulator.push(event);
235
+ if (content !== undefined && content !== "") {
236
+ options.onTextDelta?.(content);
237
+ }
238
+ }
239
+ return accumulator.finish();
240
+ } catch (error) {
241
+ if (error instanceof ProviderResponseError) {
242
+ throw error;
243
+ }
244
+ throw sanitizedProviderError(error, this.provider, this.options.model);
245
+ }
246
+ }
247
+
248
+ private async requestNonStreaming(
249
+ prepared: PreparedModelRequest,
250
+ signal: AbortSignal,
251
+ ): Promise<unknown> {
252
+ try {
253
+ return await this.client.responses.create(
254
+ prepared.payload as ResponseCreateParamsNonStreaming,
255
+ { signal },
256
+ );
257
+ } catch (error) {
258
+ throw sanitizedProviderError(error, this.provider, this.options.model);
259
+ }
260
+ }
261
+
262
+ private assistantReplaySegments(message: AssistantMessage): PreparedPromptSegment[] {
263
+ const items = toOpenAIResponsesItems(message);
264
+ if (items.length === 0) {
265
+ throw new Error("OpenAI Responses assistant replay mapping produced no items.");
266
+ }
267
+ return [
268
+ {
269
+ kind: "assistant",
270
+ normalizedText: stableJsonStringify(items),
271
+ },
272
+ ];
273
+ }
274
+
275
+ private assertPrepared(prepared: PreparedModelRequest): void {
276
+ if (
277
+ !this.preparedRequests.has(prepared) &&
278
+ !this.materializedRequests.has(prepared)
279
+ ) {
280
+ throw new Error(
281
+ `OpenAI Responses request was not prepared by this client (provider=${this.provider}, model=${this.options.model}).`,
282
+ );
283
+ }
284
+ if (
285
+ prepared.provider !== this.provider ||
286
+ prepared.model !== this.options.model ||
287
+ prepared.requestMaxOutputTokens !==
288
+ this.options.contextBudget.requestMaxOutputTokens
289
+ ) {
290
+ throw new Error(
291
+ "Prepared OpenAI Responses request configuration does not match client.",
292
+ );
293
+ }
294
+ }
295
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ ProviderResponseError,
3
+ type ProviderResponseDiagnostics,
4
+ } from "./model-client";
5
+
6
+ export class OpenAIResponsesStreamAccumulator {
7
+ private eventCount = 0;
8
+ private terminalResponse: unknown;
9
+
10
+ constructor(
11
+ private readonly options: {
12
+ provider: string;
13
+ model: string;
14
+ },
15
+ ) {}
16
+
17
+ push(event: unknown): string | undefined {
18
+ const path = `events[${this.eventCount}]`;
19
+ const record = requireRecord(event, path, this.options);
20
+ const type = requireString(record.type, `${path}.type`, this.options);
21
+ this.eventCount += 1;
22
+
23
+ if (type === "response.output_text.delta") {
24
+ return requireString(record.delta, `${path}.delta`, this.options);
25
+ }
26
+
27
+ if (
28
+ type === "response.completed" ||
29
+ type === "response.incomplete" ||
30
+ type === "response.failed"
31
+ ) {
32
+ if (this.terminalResponse !== undefined) {
33
+ throw streamError(
34
+ this.options,
35
+ path,
36
+ "contains more than one terminal response event",
37
+ );
38
+ }
39
+ this.terminalResponse = record.response;
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ finish(): unknown {
45
+ if (this.eventCount === 0) {
46
+ throw streamError(this.options, "events", "must not be empty");
47
+ }
48
+ if (this.terminalResponse === undefined) {
49
+ throw streamError(
50
+ this.options,
51
+ "events",
52
+ "ended without a terminal response event",
53
+ );
54
+ }
55
+ return this.terminalResponse;
56
+ }
57
+ }
58
+
59
+ function requireRecord(
60
+ value: unknown,
61
+ path: string,
62
+ options: { provider: string; model: string },
63
+ ): Record<string, unknown> {
64
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
65
+ throw streamError(options, path, "must be an object");
66
+ }
67
+ return value as Record<string, unknown>;
68
+ }
69
+
70
+ function requireString(
71
+ value: unknown,
72
+ path: string,
73
+ options: { provider: string; model: string },
74
+ ): string {
75
+ if (typeof value !== "string") {
76
+ throw streamError(options, path, "must be a string");
77
+ }
78
+ return value;
79
+ }
80
+
81
+ function streamError(
82
+ options: { provider: string; model: string },
83
+ path: string,
84
+ detail: string,
85
+ ): ProviderResponseError {
86
+ const diagnostics: ProviderResponseDiagnostics = {
87
+ provider: options.provider,
88
+ model: options.model,
89
+ path,
90
+ };
91
+ return new ProviderResponseError(
92
+ "invalid_provider_stream",
93
+ `Invalid provider stream (provider=${options.provider}, model=${options.model}): ${path} ${detail}.`,
94
+ diagnostics,
95
+ );
96
+ }
@@ -0,0 +1,155 @@
1
+ type ChatEstimatorToolCall = {
2
+ id: string;
3
+ type: "function";
4
+ function: {
5
+ name: string;
6
+ arguments: string;
7
+ };
8
+ };
9
+
10
+ type ChatEstimatorMessage = {
11
+ role: "system" | "user" | "assistant" | "tool";
12
+ content: unknown;
13
+ tool_calls?: ChatEstimatorToolCall[];
14
+ tool_call_id?: string;
15
+ };
16
+
17
+ export function responsesPayloadForChatTokenEstimator(
18
+ payload: unknown,
19
+ ): Record<string, unknown> {
20
+ const root = requireRecord(payload, "Responses token estimator payload");
21
+ if (!Array.isArray(root.input)) {
22
+ throw new Error("Responses token estimator payload input must be an array.");
23
+ }
24
+
25
+ const messages: ChatEstimatorMessage[] = [];
26
+ for (const [index, rawItem] of root.input.entries()) {
27
+ const path = `Responses token estimator input[${index}]`;
28
+ const item = requireRecord(rawItem, path);
29
+ const type = requireString(item.type, `${path}.type`);
30
+ if (type === "message") {
31
+ messages.push(toChatMessage(item, path));
32
+ continue;
33
+ }
34
+ if (type === "function_call") {
35
+ appendFunctionCall(messages, item, path);
36
+ continue;
37
+ }
38
+ if (type === "function_call_output") {
39
+ messages.push({
40
+ role: "tool",
41
+ tool_call_id: requireString(item.call_id, `${path}.call_id`),
42
+ content: requireString(item.output, `${path}.output`),
43
+ });
44
+ continue;
45
+ }
46
+ throw new Error(`${path}.type is unsupported: ${JSON.stringify(type)}.`);
47
+ }
48
+
49
+ return {
50
+ messages,
51
+ ...(root.tools === undefined ? {} : { tools: toChatTools(root.tools) }),
52
+ };
53
+ }
54
+
55
+ function toChatMessage(
56
+ item: Record<string, unknown>,
57
+ path: string,
58
+ ): ChatEstimatorMessage {
59
+ const role = requireString(item.role, `${path}.role`);
60
+ if (role !== "system" && role !== "user" && role !== "assistant") {
61
+ throw new Error(`${path}.role is unsupported: ${JSON.stringify(role)}.`);
62
+ }
63
+ return {
64
+ role,
65
+ content: toChatContent(item.content, `${path}.content`),
66
+ };
67
+ }
68
+
69
+ function toChatContent(value: unknown, path: string): unknown {
70
+ if (typeof value === "string") {
71
+ return value;
72
+ }
73
+ if (!Array.isArray(value)) {
74
+ throw new Error(`${path} must be a string or an array.`);
75
+ }
76
+ return value.map((rawPart, index) => {
77
+ const partPath = `${path}[${index}]`;
78
+ const part = requireRecord(rawPart, partPath);
79
+ const type = requireString(part.type, `${partPath}.type`);
80
+ if (type === "input_text") {
81
+ return {
82
+ type: "text",
83
+ text: requireString(part.text, `${partPath}.text`),
84
+ };
85
+ }
86
+ if (type === "input_image") {
87
+ return {
88
+ type: "image_url",
89
+ image_url: {
90
+ url: requireString(part.image_url, `${partPath}.image_url`),
91
+ ...(part.detail === undefined
92
+ ? {}
93
+ : { detail: requireString(part.detail, `${partPath}.detail`) }),
94
+ },
95
+ };
96
+ }
97
+ throw new Error(`${partPath}.type is unsupported: ${JSON.stringify(type)}.`);
98
+ });
99
+ }
100
+
101
+ function appendFunctionCall(
102
+ messages: ChatEstimatorMessage[],
103
+ item: Record<string, unknown>,
104
+ path: string,
105
+ ): void {
106
+ const call: ChatEstimatorToolCall = {
107
+ id: requireString(item.call_id, `${path}.call_id`),
108
+ type: "function",
109
+ function: {
110
+ name: requireString(item.name, `${path}.name`),
111
+ arguments: requireString(item.arguments, `${path}.arguments`),
112
+ },
113
+ };
114
+ const previous = messages[messages.length - 1];
115
+ if (previous?.role === "assistant") {
116
+ (previous.tool_calls ??= []).push(call);
117
+ return;
118
+ }
119
+ messages.push({ role: "assistant", content: null, tool_calls: [call] });
120
+ }
121
+
122
+ function toChatTools(value: unknown): Record<string, unknown>[] {
123
+ if (!Array.isArray(value)) {
124
+ throw new Error("Responses token estimator payload tools must be an array.");
125
+ }
126
+ return value.map((rawTool, index) => {
127
+ const path = `Responses token estimator tools[${index}]`;
128
+ const tool = requireRecord(rawTool, path);
129
+ if (tool.type !== "function") {
130
+ throw new Error(`${path}.type must be "function".`);
131
+ }
132
+ return {
133
+ type: "function",
134
+ function: {
135
+ name: requireString(tool.name, `${path}.name`),
136
+ description: requireString(tool.description, `${path}.description`),
137
+ parameters: requireRecord(tool.parameters, `${path}.parameters`),
138
+ },
139
+ };
140
+ });
141
+ }
142
+
143
+ function requireRecord(value: unknown, path: string): Record<string, unknown> {
144
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
145
+ throw new Error(`${path} must be an object.`);
146
+ }
147
+ return value as Record<string, unknown>;
148
+ }
149
+
150
+ function requireString(value: unknown, path: string): string {
151
+ if (typeof value !== "string") {
152
+ throw new Error(`${path} must be a string.`);
153
+ }
154
+ return value;
155
+ }
@@ -0,0 +1,60 @@
1
+ export type ReasoningEffortConfig = {
2
+ readonly supportedEfforts: readonly string[];
3
+ readonly defaultEffort: string;
4
+ };
5
+
6
+ export type ReasoningEffortSnapshot = {
7
+ readonly supportedEfforts: readonly string[];
8
+ readonly defaultEffort: string;
9
+ readonly effort: string;
10
+ readonly source: "profile_default" | "session_override";
11
+ };
12
+
13
+ export type ReasoningEffortController = {
14
+ snapshot(): ReasoningEffortSnapshot;
15
+ set(effort: string): ReasoningEffortSnapshot;
16
+ reset(): ReasoningEffortSnapshot;
17
+ };
18
+
19
+ export class RuntimeReasoningEffort implements ReasoningEffortController {
20
+ private effort: string;
21
+ private source: ReasoningEffortSnapshot["source"] = "profile_default";
22
+ private readonly supportedEfforts: readonly string[];
23
+
24
+ constructor(private readonly config: ReasoningEffortConfig) {
25
+ this.supportedEfforts = Object.freeze([...config.supportedEfforts]);
26
+ this.effort = config.defaultEffort;
27
+ }
28
+
29
+ snapshot(): ReasoningEffortSnapshot {
30
+ return Object.freeze({
31
+ supportedEfforts: this.supportedEfforts,
32
+ defaultEffort: this.config.defaultEffort,
33
+ effort: this.effort,
34
+ source: this.source,
35
+ });
36
+ }
37
+
38
+ set(effort: string): ReasoningEffortSnapshot {
39
+ if (!this.supportedEfforts.includes(effort)) {
40
+ throw new Error(
41
+ `Unsupported reasoning effort ${JSON.stringify(effort)}. Available efforts: ${this.supportedEfforts.join(", ")}.`,
42
+ );
43
+ }
44
+ this.effort = effort;
45
+ this.source = "session_override";
46
+ return this.snapshot();
47
+ }
48
+
49
+ reset(): ReasoningEffortSnapshot {
50
+ this.effort = this.config.defaultEffort;
51
+ this.source = "profile_default";
52
+ return this.snapshot();
53
+ }
54
+ }
55
+
56
+ export function createReasoningEffortController(
57
+ config: ReasoningEffortConfig | undefined,
58
+ ): ReasoningEffortController | undefined {
59
+ return config === undefined ? undefined : new RuntimeReasoningEffort(config);
60
+ }
@@ -5,7 +5,7 @@ import { parseSessionId, type SessionId } from "../ids/runtime-id";
5
5
  import { SessionError } from "./session-errors";
6
6
  import { inspectSessionLock } from "./session-lock";
7
7
  import { SessionStore } from "./session-store";
8
- import { verifySessionSchema } from "./session-schema";
8
+ import { verifyReadableSessionSchema } from "./session-schema";
9
9
 
10
10
  export type SessionSummary = {
11
11
  sessionId: SessionId;
@@ -129,7 +129,7 @@ async function readSummary(
129
129
  strict: true,
130
130
  safeIntegers: true,
131
131
  });
132
- verifySessionSchema(database, sessionId);
132
+ verifyReadableSessionSchema(database, sessionId);
133
133
  const meta = database.query("SELECT * FROM session_meta").all() as Array<
134
134
  Record<string, unknown>
135
135
  >;
@@ -149,7 +149,12 @@ export function isRecallableMessage(input: {
149
149
  (input.role === "user" || input.role === "assistant" || input.role === "tool") &&
150
150
  input.content !== null &&
151
151
  input.content.length > 0 &&
152
- !(input.role === "tool" && input.toolName === "Recall")
152
+ !(
153
+ input.role === "tool" &&
154
+ (input.toolName === "Recall" ||
155
+ input.toolName === "RecallSearch" ||
156
+ input.toolName === "RecallGet")
157
+ )
153
158
  );
154
159
  }
155
160