tinker-agent 1.0.65 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -4,7 +4,14 @@ import {
|
|
|
4
4
|
parseMessageSource,
|
|
5
5
|
type MessageSource,
|
|
6
6
|
} from "../context/context-source";
|
|
7
|
-
import { contentHash } from "../context/protocol-frame";
|
|
7
|
+
import { contentHash, userMessageHash } from "../context/protocol-frame";
|
|
8
|
+
import {
|
|
9
|
+
parseImageAssetId,
|
|
10
|
+
parseImageAttachmentId,
|
|
11
|
+
validateUserMessage,
|
|
12
|
+
type UserImageAttachment,
|
|
13
|
+
type UserMessage,
|
|
14
|
+
} from "../image/image-types";
|
|
8
15
|
import type { MessageId, SessionId } from "../ids/runtime-id";
|
|
9
16
|
import { SessionError, sessionReadError } from "./session-errors";
|
|
10
17
|
|
|
@@ -110,6 +117,21 @@ type RecallRow = {
|
|
|
110
117
|
observation_sha256?: unknown;
|
|
111
118
|
};
|
|
112
119
|
|
|
120
|
+
type RecallImageRow = {
|
|
121
|
+
message_id: unknown;
|
|
122
|
+
attachment_id: unknown;
|
|
123
|
+
position: unknown;
|
|
124
|
+
label: unknown;
|
|
125
|
+
range_start: unknown;
|
|
126
|
+
range_end: unknown;
|
|
127
|
+
original_name: unknown;
|
|
128
|
+
asset_id: unknown;
|
|
129
|
+
mime_type: unknown;
|
|
130
|
+
byte_length: unknown;
|
|
131
|
+
width: unknown;
|
|
132
|
+
height: unknown;
|
|
133
|
+
};
|
|
134
|
+
|
|
113
135
|
export function createSessionHistoryReader(input: {
|
|
114
136
|
database: Database;
|
|
115
137
|
sessionId: SessionId;
|
|
@@ -203,11 +225,18 @@ class SqliteSessionHistoryReader implements SessionHistoryReader {
|
|
|
203
225
|
|
|
204
226
|
let decoded: Array<RecallSearchHit & { content: string }>;
|
|
205
227
|
try {
|
|
228
|
+
const imageAttachments = this.loadImageAttachments(
|
|
229
|
+
rows.map((row) => nonEmptyString(row.message_id, "message_id") as MessageId),
|
|
230
|
+
);
|
|
206
231
|
decoded = rows.map((row) => {
|
|
207
232
|
const metadata = decodeRecallRow(row);
|
|
233
|
+
const attachments = imageAttachments.get(metadata.messageId) ?? [];
|
|
234
|
+
verifyRecallContent(metadata, attachments);
|
|
235
|
+
const content = renderRecallContent(metadata.content, attachments);
|
|
208
236
|
return {
|
|
209
237
|
...metadata,
|
|
210
|
-
|
|
238
|
+
content,
|
|
239
|
+
excerpt: buildExcerpt(content, input.query),
|
|
211
240
|
};
|
|
212
241
|
});
|
|
213
242
|
} catch (error) {
|
|
@@ -258,12 +287,15 @@ class SqliteSessionHistoryReader implements SessionHistoryReader {
|
|
|
258
287
|
}
|
|
259
288
|
|
|
260
289
|
let metadata: ReturnType<typeof decodeRecallRow>;
|
|
290
|
+
let attachments: readonly UserImageAttachment[];
|
|
261
291
|
try {
|
|
262
292
|
metadata = decodeRecallRow(row);
|
|
293
|
+
attachments =
|
|
294
|
+
this.loadImageAttachments([metadata.messageId]).get(metadata.messageId) ?? [];
|
|
263
295
|
} catch (error) {
|
|
264
296
|
throw sessionReadError("decode_recall_get", this.sessionId, error);
|
|
265
297
|
}
|
|
266
|
-
const actualHash =
|
|
298
|
+
const actualHash = recallContentHash(metadata, attachments);
|
|
267
299
|
const observationHash = row.observation_sha256;
|
|
268
300
|
if (
|
|
269
301
|
actualHash !== metadata.contentSha256 ||
|
|
@@ -277,7 +309,8 @@ class SqliteSessionHistoryReader implements SessionHistoryReader {
|
|
|
277
309
|
);
|
|
278
310
|
}
|
|
279
311
|
|
|
280
|
-
const
|
|
312
|
+
const renderedContent = renderRecallContent(metadata.content, attachments);
|
|
313
|
+
const page = sliceUtf8Page(renderedContent, input.byteOffset, input.byteLimit);
|
|
281
314
|
return Object.freeze({
|
|
282
315
|
source: formatMessageSource(metadata.messageId),
|
|
283
316
|
messageId: metadata.messageId,
|
|
@@ -324,6 +357,112 @@ class SqliteSessionHistoryReader implements SessionHistoryReader {
|
|
|
324
357
|
}
|
|
325
358
|
return requested ?? maximum;
|
|
326
359
|
}
|
|
360
|
+
|
|
361
|
+
private loadImageAttachments(
|
|
362
|
+
messageIds: readonly MessageId[],
|
|
363
|
+
): ReadonlyMap<MessageId, readonly UserImageAttachment[]> {
|
|
364
|
+
if (messageIds.length === 0) {
|
|
365
|
+
return new Map();
|
|
366
|
+
}
|
|
367
|
+
const unique = [...new Set(messageIds)];
|
|
368
|
+
let rows: RecallImageRow[];
|
|
369
|
+
try {
|
|
370
|
+
this.requireStoreOpen();
|
|
371
|
+
rows = this.database
|
|
372
|
+
.query(
|
|
373
|
+
`SELECT
|
|
374
|
+
mia.message_id, mia.attachment_id, mia.position, mia.label,
|
|
375
|
+
mia.range_start, mia.range_end, mia.original_name,
|
|
376
|
+
ia.asset_id, ia.mime_type, ia.byte_length, ia.width, ia.height
|
|
377
|
+
FROM message_image_attachments mia
|
|
378
|
+
JOIN image_assets ia ON ia.asset_id = mia.asset_id
|
|
379
|
+
WHERE mia.message_id IN (${unique.map(() => "?").join(", ")})
|
|
380
|
+
ORDER BY mia.message_id, mia.position`,
|
|
381
|
+
)
|
|
382
|
+
.all(...unique) as RecallImageRow[];
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw sessionReadError("read_recall_images", this.sessionId, error);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const grouped = new Map<MessageId, UserImageAttachment[]>();
|
|
388
|
+
for (const row of rows) {
|
|
389
|
+
const messageId = nonEmptyString(row.message_id, "message_id") as MessageId;
|
|
390
|
+
const attachments = grouped.get(messageId) ?? [];
|
|
391
|
+
const position = nonNegativeSafeInteger(row.position, "image position");
|
|
392
|
+
if (position !== attachments.length) {
|
|
393
|
+
throw new Error("Recall image positions are not contiguous.");
|
|
394
|
+
}
|
|
395
|
+
attachments.push(
|
|
396
|
+
Object.freeze({
|
|
397
|
+
attachmentId: parseImageAttachmentId(
|
|
398
|
+
nonEmptyString(row.attachment_id, "attachment_id"),
|
|
399
|
+
),
|
|
400
|
+
assetId: parseImageAssetId(nonEmptyString(row.asset_id, "asset_id")),
|
|
401
|
+
label: nonEmptyString(row.label, "image label"),
|
|
402
|
+
range: Object.freeze({
|
|
403
|
+
start: nonNegativeSafeInteger(row.range_start, "image range start"),
|
|
404
|
+
end: safeInteger(row.range_end, "image range end"),
|
|
405
|
+
}),
|
|
406
|
+
mimeType: enumValue(
|
|
407
|
+
row.mime_type,
|
|
408
|
+
["image/png", "image/jpeg", "image/webp"] as const,
|
|
409
|
+
"image MIME type",
|
|
410
|
+
),
|
|
411
|
+
byteLength: safeInteger(row.byte_length, "image byte length"),
|
|
412
|
+
width: safeInteger(row.width, "image width"),
|
|
413
|
+
height: safeInteger(row.height, "image height"),
|
|
414
|
+
originalName: nonEmptyString(row.original_name, "original image name"),
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
grouped.set(messageId, attachments);
|
|
418
|
+
}
|
|
419
|
+
return new Map(
|
|
420
|
+
[...grouped].map(([messageId, attachments]) => [
|
|
421
|
+
messageId,
|
|
422
|
+
Object.freeze(attachments),
|
|
423
|
+
]),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function renderRecallContent(
|
|
429
|
+
content: string,
|
|
430
|
+
attachments: readonly UserImageAttachment[],
|
|
431
|
+
): string {
|
|
432
|
+
if (attachments.length === 0) return content;
|
|
433
|
+
const notes = attachments.map(
|
|
434
|
+
(attachment) =>
|
|
435
|
+
`[Historical image omitted: label=${attachment.label}, originalName=${JSON.stringify(attachment.originalName)}. Ask the user to reattach it.]`,
|
|
436
|
+
);
|
|
437
|
+
return `${content}\n\n${notes.join("\n")}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function verifyRecallContent(
|
|
441
|
+
metadata: ReturnType<typeof decodeRecallRow>,
|
|
442
|
+
attachments: readonly UserImageAttachment[],
|
|
443
|
+
): void {
|
|
444
|
+
if (recallContentHash(metadata, attachments) !== metadata.contentSha256) {
|
|
445
|
+
throw new Error("Canonical Recall content failed its integrity check.");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function recallContentHash(
|
|
450
|
+
metadata: ReturnType<typeof decodeRecallRow>,
|
|
451
|
+
attachments: readonly UserImageAttachment[],
|
|
452
|
+
): string {
|
|
453
|
+
if (metadata.role !== "user") {
|
|
454
|
+
if (attachments.length > 0) {
|
|
455
|
+
throw new Error("Non-user Recall message has image attachments.");
|
|
456
|
+
}
|
|
457
|
+
return contentHash(metadata.content);
|
|
458
|
+
}
|
|
459
|
+
const message: UserMessage = Object.freeze({
|
|
460
|
+
role: "user",
|
|
461
|
+
content: metadata.content,
|
|
462
|
+
...(attachments.length === 0 ? {} : { attachments }),
|
|
463
|
+
});
|
|
464
|
+
validateUserMessage(message);
|
|
465
|
+
return userMessageHash(message);
|
|
327
466
|
}
|
|
328
467
|
|
|
329
468
|
function searchPredicates(
|
|
@@ -523,6 +662,14 @@ function safeInteger(value: unknown, name: string): number {
|
|
|
523
662
|
return number;
|
|
524
663
|
}
|
|
525
664
|
|
|
665
|
+
function nonNegativeSafeInteger(value: unknown, name: string): number {
|
|
666
|
+
const number = typeof value === "bigint" ? Number(value) : value;
|
|
667
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) {
|
|
668
|
+
throw new Error(`${name} must be a non-negative safe integer.`);
|
|
669
|
+
}
|
|
670
|
+
return number;
|
|
671
|
+
}
|
|
672
|
+
|
|
526
673
|
function enumValue<const TValues extends readonly string[]>(
|
|
527
674
|
value: unknown,
|
|
528
675
|
values: TValues,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { contentHash } from "../context/protocol-frame";
|
|
4
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
5
|
+
import { SessionError, sessionOpenError, sessionReadError } from "./session-errors";
|
|
6
|
+
import { verifySessionSchema } from "./session-schema";
|
|
7
|
+
import { decodeStoredToolCalls, sessionDatabasePath } from "./session-store";
|
|
8
|
+
|
|
9
|
+
const OPERATION = "read_last_assistant_response";
|
|
10
|
+
|
|
11
|
+
export async function readLastAssistantResponse(input: {
|
|
12
|
+
workspaceRoot: string;
|
|
13
|
+
sessionId: SessionId;
|
|
14
|
+
}): Promise<string | undefined> {
|
|
15
|
+
const workspaceRoot = await realpath(input.workspaceRoot);
|
|
16
|
+
let database: Database;
|
|
17
|
+
try {
|
|
18
|
+
database = new Database(sessionDatabasePath(workspaceRoot, input.sessionId), {
|
|
19
|
+
readonly: true,
|
|
20
|
+
strict: true,
|
|
21
|
+
safeIntegers: true,
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw sessionOpenError(OPERATION, input.sessionId, error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return readResponse(database, {
|
|
29
|
+
workspaceRoot,
|
|
30
|
+
sessionId: input.sessionId,
|
|
31
|
+
});
|
|
32
|
+
} finally {
|
|
33
|
+
database.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readResponse(
|
|
38
|
+
database: Database,
|
|
39
|
+
input: { workspaceRoot: string; sessionId: SessionId },
|
|
40
|
+
): string | undefined {
|
|
41
|
+
try {
|
|
42
|
+
verifySessionSchema(database, input.sessionId);
|
|
43
|
+
const metaRows = database
|
|
44
|
+
.query("SELECT session_id, workspace_root FROM session_meta")
|
|
45
|
+
.all() as Array<Record<string, unknown>>;
|
|
46
|
+
if (
|
|
47
|
+
metaRows.length !== 1 ||
|
|
48
|
+
metaRows[0]?.session_id !== input.sessionId ||
|
|
49
|
+
metaRows[0]?.workspace_root !== input.workspaceRoot
|
|
50
|
+
) {
|
|
51
|
+
throw integrityError(
|
|
52
|
+
input.sessionId,
|
|
53
|
+
"Session identity does not match the requested last response.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const row = database
|
|
58
|
+
.query(
|
|
59
|
+
`SELECT
|
|
60
|
+
t.turn_id,
|
|
61
|
+
t.turn_number,
|
|
62
|
+
t.final_message_id,
|
|
63
|
+
m.message_id,
|
|
64
|
+
m.session_id AS message_session_id,
|
|
65
|
+
m.turn_id AS message_turn_id,
|
|
66
|
+
m.role,
|
|
67
|
+
m.content,
|
|
68
|
+
m.content_sha256,
|
|
69
|
+
m.tool_calls_json
|
|
70
|
+
FROM turns t
|
|
71
|
+
LEFT JOIN messages m ON m.message_id = t.final_message_id
|
|
72
|
+
WHERE t.session_id = ? AND t.status = 'completed'
|
|
73
|
+
ORDER BY t.turn_number DESC
|
|
74
|
+
LIMIT 1`,
|
|
75
|
+
)
|
|
76
|
+
.get(input.sessionId) as Record<string, unknown> | null;
|
|
77
|
+
if (row === null) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return decodeResponse(row, input.sessionId);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw sessionReadError(OPERATION, input.sessionId, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function decodeResponse(
|
|
88
|
+
row: Record<string, unknown>,
|
|
89
|
+
sessionId: SessionId,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
const turnId = requireNonEmptyString(row.turn_id, "turn ID", sessionId);
|
|
92
|
+
requirePositiveInteger(row.turn_number, "turn number", sessionId);
|
|
93
|
+
const finalMessageId = requireNonEmptyString(
|
|
94
|
+
row.final_message_id,
|
|
95
|
+
"final message ID",
|
|
96
|
+
sessionId,
|
|
97
|
+
);
|
|
98
|
+
if (
|
|
99
|
+
row.message_id !== finalMessageId ||
|
|
100
|
+
row.message_session_id !== sessionId ||
|
|
101
|
+
row.message_turn_id !== turnId
|
|
102
|
+
) {
|
|
103
|
+
throw integrityError(
|
|
104
|
+
sessionId,
|
|
105
|
+
`Final message ${finalMessageId} does not match turn ${turnId}.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (row.role !== "assistant") {
|
|
109
|
+
throw integrityError(
|
|
110
|
+
sessionId,
|
|
111
|
+
`Final message ${finalMessageId} is not assistant.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const toolCalls =
|
|
116
|
+
row.tool_calls_json === null
|
|
117
|
+
? []
|
|
118
|
+
: decodeStoredToolCalls(
|
|
119
|
+
requireNonEmptyString(
|
|
120
|
+
row.tool_calls_json,
|
|
121
|
+
"final message tool calls",
|
|
122
|
+
sessionId,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
if (toolCalls.length !== 0) {
|
|
126
|
+
throw integrityError(
|
|
127
|
+
sessionId,
|
|
128
|
+
`Final message ${finalMessageId} still contains tool calls.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = requireNullableString(
|
|
133
|
+
row.content,
|
|
134
|
+
"final message content",
|
|
135
|
+
sessionId,
|
|
136
|
+
);
|
|
137
|
+
const storedHash = requireNonEmptyString(
|
|
138
|
+
row.content_sha256,
|
|
139
|
+
"final message content hash",
|
|
140
|
+
sessionId,
|
|
141
|
+
);
|
|
142
|
+
if (contentHash(content) !== storedHash) {
|
|
143
|
+
throw integrityError(
|
|
144
|
+
sessionId,
|
|
145
|
+
`Final message ${finalMessageId} content hash does not match.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return content === null || content.trim() === "" ? undefined : content;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function requireNonEmptyString(
|
|
152
|
+
value: unknown,
|
|
153
|
+
name: string,
|
|
154
|
+
sessionId: SessionId,
|
|
155
|
+
): string {
|
|
156
|
+
if (typeof value !== "string" || value === "") {
|
|
157
|
+
throw integrityError(sessionId, `Stored ${name} must be non-empty text.`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function requireNullableString(
|
|
163
|
+
value: unknown,
|
|
164
|
+
name: string,
|
|
165
|
+
sessionId: SessionId,
|
|
166
|
+
): string | null {
|
|
167
|
+
if (value !== null && typeof value !== "string") {
|
|
168
|
+
throw integrityError(sessionId, `Stored ${name} must be text or null.`);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function requirePositiveInteger(
|
|
174
|
+
value: unknown,
|
|
175
|
+
name: string,
|
|
176
|
+
sessionId: SessionId,
|
|
177
|
+
): number {
|
|
178
|
+
const number = typeof value === "bigint" ? Number(value) : value;
|
|
179
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1) {
|
|
180
|
+
throw integrityError(sessionId, `Stored ${name} must be a positive integer.`);
|
|
181
|
+
}
|
|
182
|
+
return number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function integrityError(sessionId: SessionId, message: string): SessionError {
|
|
186
|
+
return new SessionError("SESSION_INTEGRITY_FAILED", OPERATION, message, {
|
|
187
|
+
sessionId,
|
|
188
|
+
});
|
|
189
|
+
}
|