tinker-agent 2.7.0 → 2.9.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 +55 -1
- package/README.md +64 -10
- package/package.json +4 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +253 -2117
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/context/context-automation-policy.ts +12 -118
- package/src/events/types.ts +13 -1
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +41 -11
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +20 -2
- package/src/tools/bash.ts +1 -1
- package/src/tools/context-maintenance.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/types.ts +9 -0
- package/src/tools/wait.ts +1 -3
- package/src/tui/event-store.ts +8 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import {
|
|
3
|
+
canonicalToolResultContentHash,
|
|
4
|
+
toolResultDisplayText,
|
|
5
|
+
validateToolResultContent,
|
|
6
|
+
} from "../agent/tool-result-content";
|
|
7
|
+
import {
|
|
8
|
+
validateStoredContextSurface,
|
|
9
|
+
type StoredContextSurfaceV8,
|
|
10
|
+
} from "../context/context-surface";
|
|
11
|
+
import {
|
|
12
|
+
userMessageHash,
|
|
13
|
+
type CanonicalMessageRecord,
|
|
14
|
+
type ProtocolFrame,
|
|
15
|
+
type ToolResultRecord,
|
|
16
|
+
} from "../context/protocol-frame";
|
|
17
|
+
import { validateUserMessage, type ImageAssetRef } from "../image/image-types";
|
|
18
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
19
|
+
import { imageAssetRefFromAttachment } from "./session-store-record-codecs";
|
|
20
|
+
import { requireItem } from "./session-store-sql";
|
|
21
|
+
import { numberFromSql, timestampFromSql } from "./session-store-value-codecs";
|
|
22
|
+
|
|
23
|
+
export function insertPendingSkillActivation(
|
|
24
|
+
database: Database,
|
|
25
|
+
message: CanonicalMessageRecord,
|
|
26
|
+
result: ToolResultRecord,
|
|
27
|
+
now: string,
|
|
28
|
+
): void {
|
|
29
|
+
if (
|
|
30
|
+
message.role !== "tool" ||
|
|
31
|
+
result.completion.kind !== "returned" ||
|
|
32
|
+
result.completion.raw.kind !== "skill" ||
|
|
33
|
+
!result.completion.raw.ok ||
|
|
34
|
+
result.completion.raw.status !== "loaded"
|
|
35
|
+
) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const raw = result.completion.raw;
|
|
39
|
+
if (message.name !== "Skill" || message.messageId !== result.toolMessageId) {
|
|
40
|
+
throw new Error("Loaded Agent Skill completion has invalid tool identity.");
|
|
41
|
+
}
|
|
42
|
+
database
|
|
43
|
+
.query(
|
|
44
|
+
`INSERT INTO skill_activations (
|
|
45
|
+
activation_message_id, tool_call_id, session_id, name, scope,
|
|
46
|
+
skill_file_sha256, state, dispatched_iteration_id, settled_revision_id,
|
|
47
|
+
rejection_reason, created_at, updated_at
|
|
48
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL, ?, ?)`,
|
|
49
|
+
)
|
|
50
|
+
.run(
|
|
51
|
+
message.messageId,
|
|
52
|
+
result.toolCallId,
|
|
53
|
+
result.sessionId,
|
|
54
|
+
raw.name,
|
|
55
|
+
raw.scope,
|
|
56
|
+
raw.sha256,
|
|
57
|
+
now,
|
|
58
|
+
now,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function insertFrame(database: Database, frame: ProtocolFrame): void {
|
|
63
|
+
database
|
|
64
|
+
.query(
|
|
65
|
+
`INSERT INTO protocol_frames (
|
|
66
|
+
frame_id, session_id, turn_id, iteration_id, kind, state,
|
|
67
|
+
first_ordinal, last_ordinal, created_at, closed_at
|
|
68
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
69
|
+
)
|
|
70
|
+
.run(
|
|
71
|
+
frame.frameId,
|
|
72
|
+
frame.sessionId,
|
|
73
|
+
frame.turnId ?? null,
|
|
74
|
+
frame.iterationId ?? null,
|
|
75
|
+
frame.kind,
|
|
76
|
+
frame.state,
|
|
77
|
+
frame.firstOrdinal,
|
|
78
|
+
frame.lastOrdinal ?? null,
|
|
79
|
+
frame.createdAt,
|
|
80
|
+
frame.closedAt ?? null,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function insertMessage(
|
|
85
|
+
database: Database,
|
|
86
|
+
message: CanonicalMessageRecord,
|
|
87
|
+
): void {
|
|
88
|
+
const assistant = message.role === "assistant" ? message : undefined;
|
|
89
|
+
const tool = message.role === "tool" ? message : undefined;
|
|
90
|
+
const turnId = "turnId" in message ? message.turnId : null;
|
|
91
|
+
const iterationId = "iterationId" in message ? message.iterationId : null;
|
|
92
|
+
const reasoningPresent =
|
|
93
|
+
assistant !== undefined && assistant.reasoningContent !== undefined ? 1 : 0;
|
|
94
|
+
database
|
|
95
|
+
.query(
|
|
96
|
+
`INSERT INTO messages (
|
|
97
|
+
message_id, session_id, frame_id, ordinal, role, turn_id, iteration_id,
|
|
98
|
+
content, content_sha256, reasoning_content, reasoning_content_present,
|
|
99
|
+
tool_calls_json, provider, model, tool_call_id, provider_tool_call_id,
|
|
100
|
+
name, origin, created_at
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
102
|
+
)
|
|
103
|
+
.run(
|
|
104
|
+
message.messageId,
|
|
105
|
+
message.sessionId,
|
|
106
|
+
message.frameId,
|
|
107
|
+
message.ordinal,
|
|
108
|
+
message.role,
|
|
109
|
+
turnId,
|
|
110
|
+
iterationId,
|
|
111
|
+
message.role === "tool" ? message.displayText : message.content,
|
|
112
|
+
message.contentSha256,
|
|
113
|
+
assistant?.reasoningContent ?? null,
|
|
114
|
+
reasoningPresent,
|
|
115
|
+
assistant?.toolCalls === undefined
|
|
116
|
+
? null
|
|
117
|
+
: stableJsonStringify(assistant.toolCalls),
|
|
118
|
+
assistant?.provider ?? null,
|
|
119
|
+
assistant?.model ?? null,
|
|
120
|
+
tool?.toolCallId ?? null,
|
|
121
|
+
tool?.providerToolCallId ?? null,
|
|
122
|
+
tool?.name ?? null,
|
|
123
|
+
message.origin,
|
|
124
|
+
message.createdAt,
|
|
125
|
+
);
|
|
126
|
+
if (message.role === "user" && message.attachments !== undefined) {
|
|
127
|
+
insertMessageImageAttachments(database, message);
|
|
128
|
+
}
|
|
129
|
+
if (message.role === "tool") {
|
|
130
|
+
insertToolMessageContentBlocks(database, message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function insertMessageImageAttachments(
|
|
135
|
+
database: Database,
|
|
136
|
+
message: Extract<CanonicalMessageRecord, { role: "user" }>,
|
|
137
|
+
): void {
|
|
138
|
+
const userMessage = {
|
|
139
|
+
role: "user" as const,
|
|
140
|
+
content: message.content,
|
|
141
|
+
attachments: message.attachments,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
validateUserMessage(userMessage);
|
|
145
|
+
if (userMessageHash(userMessage) !== message.contentSha256) {
|
|
146
|
+
throw new Error("User image attachment hash does not match the message hash.");
|
|
147
|
+
}
|
|
148
|
+
for (let position = 0; position < message.attachments!.length; position += 1) {
|
|
149
|
+
const attachment = requireItem(message.attachments!, position, "image attachment");
|
|
150
|
+
ensureImageAsset(
|
|
151
|
+
database,
|
|
152
|
+
imageAssetRefFromAttachment(attachment),
|
|
153
|
+
message.createdAt,
|
|
154
|
+
);
|
|
155
|
+
database
|
|
156
|
+
.query(
|
|
157
|
+
`INSERT INTO message_image_attachments (
|
|
158
|
+
message_id, attachment_id, asset_id, position, label,
|
|
159
|
+
range_start, range_end, original_name
|
|
160
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
161
|
+
)
|
|
162
|
+
.run(
|
|
163
|
+
message.messageId,
|
|
164
|
+
attachment.attachmentId,
|
|
165
|
+
attachment.assetId,
|
|
166
|
+
position,
|
|
167
|
+
attachment.label,
|
|
168
|
+
attachment.range.start,
|
|
169
|
+
attachment.range.end,
|
|
170
|
+
attachment.originalName,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function insertToolMessageContentBlocks(
|
|
176
|
+
database: Database,
|
|
177
|
+
message: Extract<CanonicalMessageRecord, { role: "tool" }>,
|
|
178
|
+
): void {
|
|
179
|
+
validateToolResultContent(message.content);
|
|
180
|
+
if (
|
|
181
|
+
canonicalToolResultContentHash(message.content) !== message.contentSha256 ||
|
|
182
|
+
toolResultDisplayText(message.content) !== message.displayText
|
|
183
|
+
) {
|
|
184
|
+
throw new Error("Tool content blocks do not match canonical message metadata.");
|
|
185
|
+
}
|
|
186
|
+
for (let position = 0; position < message.content.length; position += 1) {
|
|
187
|
+
const block = requireItem(message.content, position, "tool content block");
|
|
188
|
+
if (block.type === "image") {
|
|
189
|
+
ensureImageAsset(database, block.asset, message.createdAt);
|
|
190
|
+
}
|
|
191
|
+
database
|
|
192
|
+
.query(
|
|
193
|
+
`INSERT INTO tool_message_content_blocks (
|
|
194
|
+
message_id, position, kind, text_content, asset_id
|
|
195
|
+
) VALUES (?, ?, ?, ?, ?)`,
|
|
196
|
+
)
|
|
197
|
+
.run(
|
|
198
|
+
message.messageId,
|
|
199
|
+
position,
|
|
200
|
+
block.type,
|
|
201
|
+
block.type === "text" ? block.text : null,
|
|
202
|
+
block.type === "image" ? block.asset.assetId : null,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function ensureImageAsset(
|
|
208
|
+
database: Database,
|
|
209
|
+
asset: ImageAssetRef,
|
|
210
|
+
createdAt: string,
|
|
211
|
+
): void {
|
|
212
|
+
const existing = database
|
|
213
|
+
.query(
|
|
214
|
+
`SELECT mime_type, byte_length, width, height, created_at
|
|
215
|
+
FROM image_assets WHERE asset_id = ?`,
|
|
216
|
+
)
|
|
217
|
+
.get(asset.assetId) as {
|
|
218
|
+
mime_type: unknown;
|
|
219
|
+
byte_length: unknown;
|
|
220
|
+
width: unknown;
|
|
221
|
+
height: unknown;
|
|
222
|
+
created_at: unknown;
|
|
223
|
+
} | null;
|
|
224
|
+
if (existing === null) {
|
|
225
|
+
database
|
|
226
|
+
.query(
|
|
227
|
+
`INSERT INTO image_assets (
|
|
228
|
+
asset_id, mime_type, byte_length, width, height, created_at
|
|
229
|
+
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
230
|
+
)
|
|
231
|
+
.run(
|
|
232
|
+
asset.assetId,
|
|
233
|
+
asset.mimeType,
|
|
234
|
+
asset.byteLength,
|
|
235
|
+
asset.width,
|
|
236
|
+
asset.height,
|
|
237
|
+
createdAt,
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
timestampFromSql(existing.created_at, "image asset created_at");
|
|
243
|
+
if (
|
|
244
|
+
existing.mime_type !== asset.mimeType ||
|
|
245
|
+
numberFromSql(existing.byte_length, "image asset byte_length") !==
|
|
246
|
+
asset.byteLength ||
|
|
247
|
+
numberFromSql(existing.width, "image asset width") !== asset.width ||
|
|
248
|
+
numberFromSql(existing.height, "image asset height") !== asset.height
|
|
249
|
+
) {
|
|
250
|
+
throw new Error(`Image asset metadata conflicts for ${asset.assetId}.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function insertToolResult(database: Database, result: ToolResultRecord): void {
|
|
255
|
+
const returned = result.completion.kind === "returned" ? result.completion : null;
|
|
256
|
+
const synthetic = result.completion.kind === "synthetic" ? result.completion : null;
|
|
257
|
+
database
|
|
258
|
+
.query(
|
|
259
|
+
`INSERT INTO tool_results (
|
|
260
|
+
tool_call_id, session_id, frame_id, tool_message_id, completion_kind,
|
|
261
|
+
raw_json, raw_sha256, observation_format, synthetic_reason,
|
|
262
|
+
synthetic_detail, observation_sha256, created_at
|
|
263
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
264
|
+
)
|
|
265
|
+
.run(
|
|
266
|
+
result.toolCallId,
|
|
267
|
+
result.sessionId,
|
|
268
|
+
result.frameId,
|
|
269
|
+
result.toolMessageId,
|
|
270
|
+
result.completion.kind,
|
|
271
|
+
returned === null ? null : stableJsonStringify(returned.raw),
|
|
272
|
+
returned?.rawSha256 ?? null,
|
|
273
|
+
returned?.observationFormat ?? null,
|
|
274
|
+
synthetic?.reason ?? null,
|
|
275
|
+
synthetic?.detail ?? null,
|
|
276
|
+
result.observationSha256,
|
|
277
|
+
result.createdAt,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function insertContextSurface(
|
|
282
|
+
database: Database,
|
|
283
|
+
surface: StoredContextSurfaceV8,
|
|
284
|
+
): void {
|
|
285
|
+
validateStoredContextSurface(surface);
|
|
286
|
+
database
|
|
287
|
+
.query(
|
|
288
|
+
`INSERT INTO context_surfaces (
|
|
289
|
+
surface_id, session_id, system_prompt, system_prompt_sha256,
|
|
290
|
+
recall_contract_version,
|
|
291
|
+
project_instruction_json, skill_catalog_json, skill_catalog_sha256,
|
|
292
|
+
active_skills_json, active_skills_sha256, tool_definitions_json,
|
|
293
|
+
tool_definitions_sha256, tool_schema_sha256, request_config_sha256,
|
|
294
|
+
request_max_output_tokens, surface_sha256, created_at
|
|
295
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
296
|
+
)
|
|
297
|
+
.run(
|
|
298
|
+
surface.surfaceId,
|
|
299
|
+
surface.sessionId,
|
|
300
|
+
surface.systemPrompt,
|
|
301
|
+
surface.systemPromptSha256,
|
|
302
|
+
surface.recallContractVersion,
|
|
303
|
+
surface.projectInstruction === undefined
|
|
304
|
+
? null
|
|
305
|
+
: stableJsonStringify(surface.projectInstruction),
|
|
306
|
+
stableJsonStringify(surface.skillCatalog),
|
|
307
|
+
surface.skillCatalogSha256,
|
|
308
|
+
stableJsonStringify(surface.activeSkills),
|
|
309
|
+
surface.activeSkillsSha256,
|
|
310
|
+
stableJsonStringify(surface.toolDefinitions),
|
|
311
|
+
surface.toolDefinitionsSha256,
|
|
312
|
+
surface.toolSchemaSha256,
|
|
313
|
+
surface.requestConfigSha256,
|
|
314
|
+
surface.requestMaxOutputTokens,
|
|
315
|
+
surface.surfaceSha256,
|
|
316
|
+
surface.createdAt,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import {
|
|
3
|
+
canonicalToolResultContentHash,
|
|
4
|
+
toolResultDisplayText,
|
|
5
|
+
} from "../agent/tool-result-content";
|
|
6
|
+
import { ContextProtocolValidator } from "../context/context-protocol-validator";
|
|
7
|
+
import {
|
|
8
|
+
immutableRecord,
|
|
9
|
+
interruptedCompletionInputs,
|
|
10
|
+
observationForCompletion,
|
|
11
|
+
type CanonicalMessageRecord,
|
|
12
|
+
type ProtocolContextView,
|
|
13
|
+
type ProtocolFrame,
|
|
14
|
+
type ToolCompletion,
|
|
15
|
+
type ToolResultRecord,
|
|
16
|
+
} from "../context/protocol-frame";
|
|
17
|
+
import type {
|
|
18
|
+
IterationId,
|
|
19
|
+
RuntimeIdFactory,
|
|
20
|
+
SessionId,
|
|
21
|
+
TurnId,
|
|
22
|
+
} from "../ids/runtime-id";
|
|
23
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
24
|
+
import { SessionError } from "./session-errors";
|
|
25
|
+
import type { SessionStore } from "./session-store";
|
|
26
|
+
import { type SessionRecoveryResult } from "./session-store-contracts";
|
|
27
|
+
import type { SessionStoreLedgerWriter } from "./session-store-ledger-writer";
|
|
28
|
+
import { insertMessage, insertToolResult } from "./session-store-record-writer";
|
|
29
|
+
import { requireItem, requireSingleChange, runTransaction } from "./session-store-sql";
|
|
30
|
+
|
|
31
|
+
/** Repairs only the interrupted canonical tail before the session is resumed. */
|
|
32
|
+
export class SessionStoreRecovery {
|
|
33
|
+
private readonly validator = new ContextProtocolValidator();
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly database: Database,
|
|
36
|
+
private readonly sessionId: SessionId,
|
|
37
|
+
private readonly clock: () => string,
|
|
38
|
+
private readonly requireOpen: () => void,
|
|
39
|
+
private readonly ledgerWriter: Pick<
|
|
40
|
+
SessionStoreLedgerWriter,
|
|
41
|
+
"markOpenTurnInterrupted" | "markTerminalRows"
|
|
42
|
+
>,
|
|
43
|
+
private readonly store: Pick<SessionStore, "loadProtocolView" | "validateAll">,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
recoverInterruptedState(
|
|
47
|
+
idFactory: RuntimeIdFactory,
|
|
48
|
+
recallIndexRebuilt: boolean,
|
|
49
|
+
): SessionRecoveryResult {
|
|
50
|
+
this.requireOpen();
|
|
51
|
+
const view = this.store.loadProtocolView();
|
|
52
|
+
const openTurns = this.database
|
|
53
|
+
.query("SELECT turn_id FROM turns WHERE status = 'open' ORDER BY turn_number")
|
|
54
|
+
.all() as Array<{ turn_id: string }>;
|
|
55
|
+
const openFrames = view.frames.filter((frame) => frame.state === "open");
|
|
56
|
+
if (openTurns.length === 0 && openFrames.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
syntheticCompletionCount: 0,
|
|
59
|
+
recallIndexRebuilt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (openTurns.length !== 1 || openFrames.length > 1) {
|
|
63
|
+
throw this.recoveryError(
|
|
64
|
+
"Session has an invalid number of open turns or frames.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const turnId = openTurns[0].turn_id as TurnId;
|
|
68
|
+
const openIterations = this.database
|
|
69
|
+
.query(
|
|
70
|
+
"SELECT iteration_id FROM iterations WHERE turn_id = ? AND outcome = 'open' ORDER BY iteration_number",
|
|
71
|
+
)
|
|
72
|
+
.all(turnId) as Array<{ iteration_id: string }>;
|
|
73
|
+
if (openIterations.length > 1) {
|
|
74
|
+
throw this.recoveryError(`Turn ${turnId} has multiple open iterations.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const frame = openFrames[0];
|
|
78
|
+
if (frame === undefined) {
|
|
79
|
+
this.ledgerWriter.markOpenTurnInterrupted(
|
|
80
|
+
turnId,
|
|
81
|
+
openIterations[0]?.iteration_id as IterationId | undefined,
|
|
82
|
+
);
|
|
83
|
+
this.store.validateAll({ allowOpenTail: false });
|
|
84
|
+
return {
|
|
85
|
+
recoveredTurnId: turnId,
|
|
86
|
+
syntheticCompletionCount: 0,
|
|
87
|
+
recallIndexRebuilt,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
frame.turnId !== turnId ||
|
|
92
|
+
frame !== view.frames.at(-1) ||
|
|
93
|
+
openIterations.length !== 1 ||
|
|
94
|
+
frame.iterationId !== openIterations[0]?.iteration_id
|
|
95
|
+
) {
|
|
96
|
+
throw this.recoveryError(`Open frame ${frame.frameId} has invalid ownership.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const frameMessages = view.messages.filter(
|
|
100
|
+
(message) => message.frameId === frame.frameId,
|
|
101
|
+
);
|
|
102
|
+
const assistant = frameMessages[0];
|
|
103
|
+
if (assistant?.role !== "assistant" || assistant.toolCalls === undefined) {
|
|
104
|
+
throw this.recoveryError(`Open frame ${frame.frameId} has no tool calls.`);
|
|
105
|
+
}
|
|
106
|
+
const missingCalls = assistant.toolCalls.slice(frameMessages.length - 1);
|
|
107
|
+
if (missingCalls.length === 0) {
|
|
108
|
+
throw this.recoveryError(`Open frame ${frame.frameId} has no missing call.`);
|
|
109
|
+
}
|
|
110
|
+
const completionInputs = interruptedCompletionInputs(missingCalls);
|
|
111
|
+
const messages: CanonicalMessageRecord[] = [];
|
|
112
|
+
const toolResults: ToolResultRecord[] = [];
|
|
113
|
+
for (const input of completionInputs) {
|
|
114
|
+
const createdAt = this.clock();
|
|
115
|
+
const content = observationForCompletion(input);
|
|
116
|
+
const displayText = toolResultDisplayText(content);
|
|
117
|
+
const messageId = idFactory.createMessageId();
|
|
118
|
+
const message = immutableRecord<CanonicalMessageRecord>({
|
|
119
|
+
messageId,
|
|
120
|
+
sessionId: this.sessionId,
|
|
121
|
+
frameId: frame.frameId,
|
|
122
|
+
ordinal: view.messages.length + messages.length + 1,
|
|
123
|
+
contentSha256: canonicalToolResultContentHash(content),
|
|
124
|
+
createdAt,
|
|
125
|
+
role: "tool",
|
|
126
|
+
turnId,
|
|
127
|
+
iterationId: frame.iterationId,
|
|
128
|
+
toolCallId: input.call.toolCallId,
|
|
129
|
+
providerToolCallId: input.call.providerToolCallId,
|
|
130
|
+
name: input.call.name,
|
|
131
|
+
content,
|
|
132
|
+
displayText,
|
|
133
|
+
origin: "runtime",
|
|
134
|
+
});
|
|
135
|
+
const completion: ToolCompletion = immutableRecord({
|
|
136
|
+
kind: "synthetic",
|
|
137
|
+
reason: input.reason,
|
|
138
|
+
});
|
|
139
|
+
const result = immutableRecord<ToolResultRecord>({
|
|
140
|
+
sessionId: this.sessionId,
|
|
141
|
+
frameId: frame.frameId,
|
|
142
|
+
toolCallId: input.call.toolCallId,
|
|
143
|
+
toolMessageId: messageId,
|
|
144
|
+
completion,
|
|
145
|
+
observationSha256: canonicalToolResultContentHash(content),
|
|
146
|
+
createdAt,
|
|
147
|
+
});
|
|
148
|
+
messages.push(message);
|
|
149
|
+
toolResults.push(result);
|
|
150
|
+
}
|
|
151
|
+
const closedAt = this.clock();
|
|
152
|
+
const closedFrame = immutableRecord<ProtocolFrame>({
|
|
153
|
+
...frame,
|
|
154
|
+
state: "closed",
|
|
155
|
+
lastOrdinal: view.messages.length + messages.length,
|
|
156
|
+
closedAt,
|
|
157
|
+
});
|
|
158
|
+
const candidate: ProtocolContextView = Object.freeze({
|
|
159
|
+
...view,
|
|
160
|
+
frames: Object.freeze(
|
|
161
|
+
view.frames.map((entry) =>
|
|
162
|
+
entry.frameId === frame.frameId ? closedFrame : entry,
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
messages: Object.freeze([...view.messages, ...messages]),
|
|
166
|
+
toolResults: Object.freeze([...view.toolResults, ...toolResults]),
|
|
167
|
+
});
|
|
168
|
+
this.validator.validate(candidate, { fullIntegrity: true });
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
runTransaction(this.database, () => {
|
|
172
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
173
|
+
insertMessage(
|
|
174
|
+
this.database,
|
|
175
|
+
requireItem(messages, index, "recovery message"),
|
|
176
|
+
);
|
|
177
|
+
insertToolResult(
|
|
178
|
+
this.database,
|
|
179
|
+
requireItem(toolResults, index, "recovery tool result"),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const frameUpdate = this.database
|
|
183
|
+
.query(
|
|
184
|
+
`UPDATE protocol_frames SET state = 'closed', last_ordinal = ?, closed_at = ?
|
|
185
|
+
WHERE frame_id = ? AND state = 'open' AND last_ordinal IS NULL`,
|
|
186
|
+
)
|
|
187
|
+
.run(closedFrame.lastOrdinal!, closedAt, frame.frameId);
|
|
188
|
+
requireSingleChange(
|
|
189
|
+
this.database,
|
|
190
|
+
frameUpdate.changes,
|
|
191
|
+
"close recovered frame",
|
|
192
|
+
);
|
|
193
|
+
this.ledgerWriter.markTerminalRows(
|
|
194
|
+
turnId,
|
|
195
|
+
frame.iterationId!,
|
|
196
|
+
"interrupted",
|
|
197
|
+
"interrupted",
|
|
198
|
+
null,
|
|
199
|
+
stableJsonStringify({ version: 1, reason: "process_interrupted" }),
|
|
200
|
+
closedAt,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new SessionError(
|
|
205
|
+
"SESSION_RECOVERY_FAILED",
|
|
206
|
+
"recover_open_frame",
|
|
207
|
+
`Failed to recover open frame ${frame.frameId}.`,
|
|
208
|
+
{ sessionId: this.sessionId, frameId: frame.frameId, cause: error },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
this.store.validateAll({ allowOpenTail: false });
|
|
212
|
+
return {
|
|
213
|
+
recoveredTurnId: turnId,
|
|
214
|
+
recoveredFrameId: frame.frameId,
|
|
215
|
+
syntheticCompletionCount: messages.length,
|
|
216
|
+
recallIndexRebuilt,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private recoveryError(message: string): SessionError {
|
|
221
|
+
return new SessionError("SESSION_RECOVERY_FAILED", "recover_session", message, {
|
|
222
|
+
sessionId: this.sessionId,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|