tinker-agent 2.8.0 → 2.10.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 +79 -1
- package/README.md +81 -11
- package/package.json +5 -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-hosted-session.ts +443 -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 +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- 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 +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- 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-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { AdmissionStaleError, type LedgerMutation } from "../agent/session-ledger";
|
|
3
|
+
import type { IterationId, MessageId, SessionId, TurnId } from "../ids/runtime-id";
|
|
4
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
5
|
+
import { SessionError, sessionWriteError } from "./session-errors";
|
|
6
|
+
import type { SessionStore } from "./session-store";
|
|
7
|
+
import {
|
|
8
|
+
insertFrame,
|
|
9
|
+
insertMessage,
|
|
10
|
+
insertPendingSkillActivation,
|
|
11
|
+
insertToolResult,
|
|
12
|
+
} from "./session-store-record-writer";
|
|
13
|
+
import { requireItem, requireSingleChange, runTransaction } from "./session-store-sql";
|
|
14
|
+
import { numberFromSql } from "./session-store-value-codecs";
|
|
15
|
+
|
|
16
|
+
/** Writes canonical ledger mutations using the session's existing connection. */
|
|
17
|
+
export class SessionStoreLedgerWriter {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly database: Database,
|
|
20
|
+
private readonly sessionId: SessionId,
|
|
21
|
+
private readonly clock: () => string,
|
|
22
|
+
private readonly requireOpen: () => void,
|
|
23
|
+
private readonly store: Pick<SessionStore, "readMeta" | "loadContextSnapshot">,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
commit(mutation: LedgerMutation): void {
|
|
27
|
+
this.requireOpen();
|
|
28
|
+
const now = this.clock();
|
|
29
|
+
try {
|
|
30
|
+
runTransaction(this.database, () => {
|
|
31
|
+
switch (mutation.kind) {
|
|
32
|
+
case "begin_turn":
|
|
33
|
+
this.commitBeginTurn(mutation, now);
|
|
34
|
+
break;
|
|
35
|
+
case "append_steering_users":
|
|
36
|
+
this.commitSteeringUsers(mutation, now);
|
|
37
|
+
break;
|
|
38
|
+
case "append_assistant":
|
|
39
|
+
this.commitAssistant(mutation, now);
|
|
40
|
+
break;
|
|
41
|
+
case "commit_tool_completions":
|
|
42
|
+
this.commitToolCompletions(mutation, now);
|
|
43
|
+
break;
|
|
44
|
+
case "finish_turn":
|
|
45
|
+
this.commitFinishTurn(mutation, now);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof AdmissionStaleError) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
throw sessionWriteError(mutation.kind, this.sessionId, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private commitBeginTurn(
|
|
58
|
+
mutation: Extract<LedgerMutation, { kind: "begin_turn" }>,
|
|
59
|
+
now: string,
|
|
60
|
+
): void {
|
|
61
|
+
const meta = this.store.readMeta();
|
|
62
|
+
if (mutation.admissionBase !== undefined) {
|
|
63
|
+
const snapshot = this.store.loadContextSnapshot();
|
|
64
|
+
const head = snapshot.canonical.messages.at(-1);
|
|
65
|
+
const base = mutation.admissionBase;
|
|
66
|
+
if (
|
|
67
|
+
head === undefined ||
|
|
68
|
+
base.canonicalMessageCount !== snapshot.canonical.messages.length ||
|
|
69
|
+
base.canonicalHeadMessageId !== head.messageId ||
|
|
70
|
+
base.canonicalHeadContentSha256 !== head.contentSha256 ||
|
|
71
|
+
base.activeRevisionId !== snapshot.revision.revisionId ||
|
|
72
|
+
base.activeRevisionNumber !== snapshot.revision.revisionNumber ||
|
|
73
|
+
base.surfaceSha256 !== snapshot.surface.surfaceSha256 ||
|
|
74
|
+
base.sessionCompatibilitySha256 !== meta.sessionCompatibilitySha256 ||
|
|
75
|
+
base.nextTurnNumber !== meta.nextTurnNumber
|
|
76
|
+
) {
|
|
77
|
+
throw new AdmissionStaleError();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
meta.initializationState !== "ready" ||
|
|
82
|
+
meta.nextTurnNumber !== mutation.turn.turnNumber
|
|
83
|
+
) {
|
|
84
|
+
throw new Error("Session turn counter or state changed before begin_turn.");
|
|
85
|
+
}
|
|
86
|
+
this.database
|
|
87
|
+
.query(
|
|
88
|
+
`INSERT INTO turns (
|
|
89
|
+
session_id, turn_id, turn_number, status, next_iteration_number,
|
|
90
|
+
last_iteration_id, final_message_id, terminal_detail_json, started_at, finished_at
|
|
91
|
+
) VALUES (?, ?, ?, 'open', 1, NULL, NULL, NULL, ?, NULL)`,
|
|
92
|
+
)
|
|
93
|
+
.run(this.sessionId, mutation.turn.turnId, mutation.turn.turnNumber, now);
|
|
94
|
+
insertFrame(this.database, mutation.frame);
|
|
95
|
+
insertMessage(this.database, mutation.message);
|
|
96
|
+
const updated = this.database
|
|
97
|
+
.query(
|
|
98
|
+
`UPDATE session_meta SET next_turn_number = ?, updated_at = ?
|
|
99
|
+
WHERE singleton = 1 AND next_turn_number = ?`,
|
|
100
|
+
)
|
|
101
|
+
.run(mutation.turn.turnNumber + 1, now, mutation.turn.turnNumber);
|
|
102
|
+
requireSingleChange(this.database, updated.changes, "advance turn counter");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private commitSteeringUsers(
|
|
106
|
+
mutation: Extract<LedgerMutation, { kind: "append_steering_users" }>,
|
|
107
|
+
now: string,
|
|
108
|
+
): void {
|
|
109
|
+
const turn = this.requireTurnRow(mutation.turn.turnId);
|
|
110
|
+
if (turn.status !== "open") {
|
|
111
|
+
throw new Error(`Turn ${mutation.turn.turnId} is not open.`);
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
mutation.frames.length === 0 ||
|
|
115
|
+
mutation.frames.length !== mutation.messages.length
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Steering user mutation must contain matching frames and messages.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
for (let index = 0; index < mutation.frames.length; index += 1) {
|
|
122
|
+
insertFrame(this.database, requireItem(mutation.frames, index, "steering frame"));
|
|
123
|
+
insertMessage(
|
|
124
|
+
this.database,
|
|
125
|
+
requireItem(mutation.messages, index, "steering message"),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
this.touch(now);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private commitAssistant(
|
|
132
|
+
mutation: Extract<LedgerMutation, { kind: "append_assistant" }>,
|
|
133
|
+
now: string,
|
|
134
|
+
): void {
|
|
135
|
+
const iteration = this.requireIterationRow(mutation.iteration.iterationId);
|
|
136
|
+
if (iteration.outcome !== "open") {
|
|
137
|
+
throw new Error(`Iteration ${mutation.iteration.iterationId} is not open.`);
|
|
138
|
+
}
|
|
139
|
+
insertFrame(this.database, mutation.frame);
|
|
140
|
+
insertMessage(this.database, mutation.message);
|
|
141
|
+
if (
|
|
142
|
+
mutation.message.role === "assistant" &&
|
|
143
|
+
mutation.message.toolCalls !== undefined
|
|
144
|
+
) {
|
|
145
|
+
const expected = numberFromSql(
|
|
146
|
+
iteration.next_tool_call_number,
|
|
147
|
+
"next_tool_call_number",
|
|
148
|
+
);
|
|
149
|
+
if (expected !== 1) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Assistant tool calls were already allocated for this iteration.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const updated = this.database
|
|
155
|
+
.query(
|
|
156
|
+
`UPDATE iterations SET next_tool_call_number = ?
|
|
157
|
+
WHERE iteration_id = ? AND outcome = 'open' AND next_tool_call_number = 1`,
|
|
158
|
+
)
|
|
159
|
+
.run(mutation.message.toolCalls.length + 1, mutation.iteration.iterationId);
|
|
160
|
+
requireSingleChange(this.database, updated.changes, "advance tool call counter");
|
|
161
|
+
}
|
|
162
|
+
this.touch(now);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private commitToolCompletions(
|
|
166
|
+
mutation: Extract<LedgerMutation, { kind: "commit_tool_completions" }>,
|
|
167
|
+
now: string,
|
|
168
|
+
): void {
|
|
169
|
+
const current = this.database
|
|
170
|
+
.query("SELECT state, last_ordinal FROM protocol_frames WHERE frame_id = ?")
|
|
171
|
+
.get(mutation.frameBefore.frameId) as {
|
|
172
|
+
state: string;
|
|
173
|
+
last_ordinal: unknown;
|
|
174
|
+
} | null;
|
|
175
|
+
if (current?.state !== "open" || current.last_ordinal !== null) {
|
|
176
|
+
throw new Error(`Frame ${mutation.frameBefore.frameId} is not open.`);
|
|
177
|
+
}
|
|
178
|
+
for (let index = 0; index < mutation.messages.length; index += 1) {
|
|
179
|
+
const message = requireItem(mutation.messages, index, "tool message");
|
|
180
|
+
const result = requireItem(mutation.toolResults, index, "tool result");
|
|
181
|
+
insertMessage(this.database, message);
|
|
182
|
+
insertToolResult(this.database, result);
|
|
183
|
+
insertPendingSkillActivation(this.database, message, result, now);
|
|
184
|
+
}
|
|
185
|
+
if (mutation.frameAfter.state === "closed") {
|
|
186
|
+
const updated = this.database
|
|
187
|
+
.query(
|
|
188
|
+
`UPDATE protocol_frames SET state = 'closed', last_ordinal = ?, closed_at = ?
|
|
189
|
+
WHERE frame_id = ? AND state = 'open' AND last_ordinal IS NULL`,
|
|
190
|
+
)
|
|
191
|
+
.run(
|
|
192
|
+
mutation.frameAfter.lastOrdinal!,
|
|
193
|
+
mutation.frameAfter.closedAt!,
|
|
194
|
+
mutation.frameAfter.frameId,
|
|
195
|
+
);
|
|
196
|
+
requireSingleChange(this.database, updated.changes, "close tool exchange frame");
|
|
197
|
+
}
|
|
198
|
+
this.touch(now);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private commitFinishTurn(
|
|
202
|
+
mutation: Extract<LedgerMutation, { kind: "finish_turn" }>,
|
|
203
|
+
now: string,
|
|
204
|
+
): void {
|
|
205
|
+
const result = mutation.result;
|
|
206
|
+
const turnStatus = result.status;
|
|
207
|
+
const iterationOutcome = result.status;
|
|
208
|
+
const detail =
|
|
209
|
+
result.status === "completed"
|
|
210
|
+
? stableJsonStringify({ version: 1, finalTextLength: result.finalText.length })
|
|
211
|
+
: result.status === "failed"
|
|
212
|
+
? stableJsonStringify({ version: 1, error: result.error.slice(0, 2_000) })
|
|
213
|
+
: stableJsonStringify({ version: 1, cancellation: result.cancellation });
|
|
214
|
+
this.markTerminalRows(
|
|
215
|
+
mutation.turn.turnId,
|
|
216
|
+
result.lastIteration.iterationId,
|
|
217
|
+
turnStatus,
|
|
218
|
+
iterationOutcome,
|
|
219
|
+
mutation.finalMessageId ?? null,
|
|
220
|
+
detail,
|
|
221
|
+
now,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
markTerminalRows(
|
|
226
|
+
turnId: TurnId,
|
|
227
|
+
iterationId: IterationId,
|
|
228
|
+
turnStatus: "completed" | "failed" | "cancelled" | "interrupted",
|
|
229
|
+
iterationOutcome: "completed" | "failed" | "cancelled" | "interrupted",
|
|
230
|
+
finalMessageId: MessageId | null,
|
|
231
|
+
terminalDetailJson: string,
|
|
232
|
+
now: string,
|
|
233
|
+
): void {
|
|
234
|
+
const iteration = this.database
|
|
235
|
+
.query(
|
|
236
|
+
`UPDATE iterations SET outcome = ?, finished_at = ?
|
|
237
|
+
WHERE iteration_id = ? AND turn_id = ? AND outcome = 'open'`,
|
|
238
|
+
)
|
|
239
|
+
.run(iterationOutcome, now, iterationId, turnId);
|
|
240
|
+
requireSingleChange(this.database, iteration.changes, "finish iteration");
|
|
241
|
+
const turn = this.database
|
|
242
|
+
.query(
|
|
243
|
+
`UPDATE turns SET status = ?, last_iteration_id = ?, final_message_id = ?,
|
|
244
|
+
terminal_detail_json = ?, finished_at = ?
|
|
245
|
+
WHERE turn_id = ? AND status = 'open'`,
|
|
246
|
+
)
|
|
247
|
+
.run(turnStatus, iterationId, finalMessageId, terminalDetailJson, now, turnId);
|
|
248
|
+
requireSingleChange(this.database, turn.changes, "finish turn");
|
|
249
|
+
this.touch(now);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
markOpenTurnInterrupted(turnId: TurnId, iterationId: IterationId | undefined): void {
|
|
253
|
+
const now = this.clock();
|
|
254
|
+
try {
|
|
255
|
+
runTransaction(this.database, () => {
|
|
256
|
+
if (iterationId !== undefined) {
|
|
257
|
+
const iteration = this.database
|
|
258
|
+
.query(
|
|
259
|
+
`UPDATE iterations SET outcome = 'interrupted', finished_at = ?
|
|
260
|
+
WHERE iteration_id = ? AND outcome = 'open'`,
|
|
261
|
+
)
|
|
262
|
+
.run(now, iterationId);
|
|
263
|
+
requireSingleChange(this.database, iteration.changes, "interrupt iteration");
|
|
264
|
+
}
|
|
265
|
+
const turn = this.database
|
|
266
|
+
.query(
|
|
267
|
+
`UPDATE turns SET status = 'interrupted', finished_at = ?,
|
|
268
|
+
terminal_detail_json = ?
|
|
269
|
+
WHERE turn_id = ? AND status = 'open'`,
|
|
270
|
+
)
|
|
271
|
+
.run(
|
|
272
|
+
now,
|
|
273
|
+
stableJsonStringify({ version: 1, reason: "process_interrupted" }),
|
|
274
|
+
turnId,
|
|
275
|
+
);
|
|
276
|
+
requireSingleChange(this.database, turn.changes, "interrupt turn");
|
|
277
|
+
this.touch(now);
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new SessionError(
|
|
281
|
+
"SESSION_RECOVERY_FAILED",
|
|
282
|
+
"recover_open_turn",
|
|
283
|
+
`Failed to mark turn ${turnId} interrupted.`,
|
|
284
|
+
{ sessionId: this.sessionId, cause: error },
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
requireTurnRow(turnId: TurnId): Record<string, unknown> {
|
|
290
|
+
const row = this.database
|
|
291
|
+
.query("SELECT * FROM turns WHERE turn_id = ?")
|
|
292
|
+
.get(turnId) as Record<string, unknown> | null;
|
|
293
|
+
if (row === null) {
|
|
294
|
+
throw new Error(`Unknown turn ${turnId}.`);
|
|
295
|
+
}
|
|
296
|
+
return row;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
requireIterationRow(iterationId: IterationId): Record<string, unknown> {
|
|
300
|
+
const row = this.database
|
|
301
|
+
.query("SELECT * FROM iterations WHERE iteration_id = ?")
|
|
302
|
+
.get(iterationId) as Record<string, unknown> | null;
|
|
303
|
+
if (row === null) {
|
|
304
|
+
throw new Error(`Unknown iteration ${iterationId}.`);
|
|
305
|
+
}
|
|
306
|
+
return row;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
touch(timestamp: string): void {
|
|
310
|
+
const updated = this.database
|
|
311
|
+
.query("UPDATE session_meta SET updated_at = ? WHERE singleton = 1")
|
|
312
|
+
.run(timestamp);
|
|
313
|
+
requireSingleChange(this.database, updated.changes, "touch session");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -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
|
+
}
|