tinker-agent 2.6.0 → 2.7.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 +24 -1
- package/package.json +1 -1
- package/src/agent/loop.ts +22 -0
- package/src/agent/runtime-session.ts +158 -0
- package/src/cli/tui-runner.tsx +14 -1
- package/src/events/stdout-event-printer.ts +14 -0
- package/src/events/types.ts +9 -0
- package/src/memory/contracts.ts +46 -0
- package/src/memory/memory-coordinator.ts +445 -4
- package/src/memory/memory-create-tool.ts +117 -0
- package/src/memory/memory-delete-tool.ts +88 -0
- package/src/memory/memory-store.ts +239 -0
- package/src/memory/memory-update-tool.ts +142 -0
- package/src/observation/observation-builder.ts +70 -0
- package/src/session/session-tool-result-codec.ts +4 -0
- package/src/tools/ask-user.ts +100 -0
- package/src/tools/registry.ts +30 -0
- package/src/tools/types.ts +71 -0
- package/src/tui/app.tsx +33 -5
- package/src/tui/components/ask-user.tsx +61 -0
- package/src/tui/components/footer.tsx +14 -1
- package/src/tui/components/resume-session-picker.tsx +118 -37
- package/src/tui/event-store.ts +57 -0
- package/src/tui/tui-session-controller.ts +8 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
2
|
+
import type { ToolCall } from "../agent/types";
|
|
3
|
+
import {
|
|
4
|
+
defineToolExecutor,
|
|
5
|
+
type MemoryDeleteRawResult,
|
|
6
|
+
type ToolDefinition,
|
|
7
|
+
type ToolExecutor,
|
|
8
|
+
} from "../tools/types";
|
|
9
|
+
import { MAX_MEMORY_ID_BYTES, MEMORY_DELETE_TOOL_NAME } from "./contracts";
|
|
10
|
+
|
|
11
|
+
export const MEMORY_DELETE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
|
|
12
|
+
name: MEMORY_DELETE_TOOL_NAME,
|
|
13
|
+
description: "Delete one global memory shared across sessions and workspaces.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
id: {
|
|
19
|
+
type: "string",
|
|
20
|
+
minLength: 1,
|
|
21
|
+
maxLength: MAX_MEMORY_ID_BYTES,
|
|
22
|
+
description: "The memoryId returned by MemorySearch or MemoryGet.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["id"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function createMemoryDeleteToolExecutor(options: {
|
|
30
|
+
readonly delete: (
|
|
31
|
+
memoryId: string,
|
|
32
|
+
call: ToolCall,
|
|
33
|
+
signal: AbortSignal,
|
|
34
|
+
) => Promise<MemoryDeleteRawResult>;
|
|
35
|
+
readonly recordInvalidCall: (call: ToolCall) => Promise<void>;
|
|
36
|
+
}): ToolExecutor {
|
|
37
|
+
return defineToolExecutor("memory_delete", {
|
|
38
|
+
definition: MEMORY_DELETE_TOOL_DEFINITION,
|
|
39
|
+
async execute(args, call, context): Promise<MemoryDeleteRawResult> {
|
|
40
|
+
const parsed = parseMemoryDeleteArgs(args);
|
|
41
|
+
if (!parsed.ok) {
|
|
42
|
+
throwIfTurnCancelled(context.signal);
|
|
43
|
+
await options.recordInvalidCall(call);
|
|
44
|
+
throwIfTurnCancelled(context.signal);
|
|
45
|
+
return { ok: false, error: parsed.error };
|
|
46
|
+
}
|
|
47
|
+
const result = await options.delete(parsed.memoryId, call, context.signal);
|
|
48
|
+
throwIfTurnCancelled(context.signal);
|
|
49
|
+
return result;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type ParsedMemoryDeleteArgs =
|
|
55
|
+
| { readonly ok: true; readonly memoryId: string }
|
|
56
|
+
| { readonly ok: false; readonly error: string };
|
|
57
|
+
|
|
58
|
+
function parseMemoryDeleteArgs(args: unknown): ParsedMemoryDeleteArgs {
|
|
59
|
+
if (!isRecord(args)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: "MemoryDelete arguments must be an object containing only id.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const unexpected = Object.keys(args).find((key) => key !== "id");
|
|
66
|
+
if (unexpected !== undefined) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `MemoryDelete received unexpected field: ${unexpected}.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (typeof args.id !== "string") {
|
|
73
|
+
return { ok: false, error: "MemoryDelete.id must be a string." };
|
|
74
|
+
}
|
|
75
|
+
const memoryId = args.id.trim();
|
|
76
|
+
const bytes = Buffer.byteLength(memoryId, "utf8");
|
|
77
|
+
if (bytes < 1 || bytes > MAX_MEMORY_ID_BYTES) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: `MemoryDelete.id must be 1 to ${MAX_MEMORY_ID_BYTES} UTF-8 bytes after trimming.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, memoryId };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
87
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
@@ -7,13 +7,17 @@ import { createUuidV7, isCanonicalUuidV7 } from "../ids/uuid-v7";
|
|
|
7
7
|
import {
|
|
8
8
|
MAX_MEMORY_SUMMARY_BYTES,
|
|
9
9
|
MAX_MEMORY_TEXT_BYTES,
|
|
10
|
+
MAX_MEMORY_ID_BYTES,
|
|
10
11
|
MEMORY_SCHEMA_VERSION,
|
|
11
12
|
MEMORY_SEARCH_LIMIT,
|
|
12
13
|
MemoryError,
|
|
13
14
|
type MemoryEmbeddingIdentity,
|
|
14
15
|
type MemoryFtsMatch,
|
|
16
|
+
type MemoryDeleteStoreResult,
|
|
15
17
|
type MemoryPaths,
|
|
16
18
|
type MemorySearchMatch,
|
|
19
|
+
type MemoryUpdateStoreResult,
|
|
20
|
+
type StoredMemoryMutationRecord,
|
|
17
21
|
type StoredMemoryRecord,
|
|
18
22
|
type StoredMemorySummary,
|
|
19
23
|
type MemoryWriteBatch,
|
|
@@ -417,6 +421,164 @@ export class MemoryStore {
|
|
|
417
421
|
});
|
|
418
422
|
}
|
|
419
423
|
|
|
424
|
+
getByTextHash(text: string): StoredMemoryRecord | undefined {
|
|
425
|
+
this.requireOpen();
|
|
426
|
+
const rowValue = this.database
|
|
427
|
+
.query(
|
|
428
|
+
`SELECT memory_id, text, summary, source_workspace, source_session_id,
|
|
429
|
+
source_turn_id, created_at
|
|
430
|
+
FROM memories
|
|
431
|
+
WHERE text_sha256 = ?`,
|
|
432
|
+
)
|
|
433
|
+
.get(sha256(text));
|
|
434
|
+
return rowValue === null ? undefined : storedMemoryRecord(rowValue);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
getByIdForMutation(memoryId: string): StoredMemoryMutationRecord | undefined {
|
|
438
|
+
this.requireOpen();
|
|
439
|
+
const rowValue = this.database
|
|
440
|
+
.query(
|
|
441
|
+
`SELECT memory_id, text, summary, embedding, source_workspace,
|
|
442
|
+
source_session_id, source_turn_id, created_at
|
|
443
|
+
FROM memories
|
|
444
|
+
WHERE memory_id = ?`,
|
|
445
|
+
)
|
|
446
|
+
.get(memoryId);
|
|
447
|
+
if (rowValue === null) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
const row = sqlRecord(rowValue, "memory row");
|
|
451
|
+
return Object.freeze({
|
|
452
|
+
...storedMemoryRecord(row),
|
|
453
|
+
embedding: decodeEmbedding(
|
|
454
|
+
row.embedding,
|
|
455
|
+
expectedEmbeddingBlobBytes(this.dimensions),
|
|
456
|
+
),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
updateMemory(input: {
|
|
461
|
+
readonly memoryId: string;
|
|
462
|
+
readonly text: string;
|
|
463
|
+
readonly summary: string;
|
|
464
|
+
readonly embedding: Float32Array;
|
|
465
|
+
}): MemoryUpdateStoreResult {
|
|
466
|
+
this.requireOpen();
|
|
467
|
+
validateMemoryMutationInput(input, this.dimensions);
|
|
468
|
+
let outcome: MemoryUpdateStoreResult = Object.freeze({
|
|
469
|
+
ok: false,
|
|
470
|
+
code: "memory_not_found",
|
|
471
|
+
});
|
|
472
|
+
runImmediateTransaction(this.database, () => {
|
|
473
|
+
const targetValue = this.database
|
|
474
|
+
.query(
|
|
475
|
+
`SELECT rowid, text, summary
|
|
476
|
+
FROM memories
|
|
477
|
+
WHERE memory_id = ?`,
|
|
478
|
+
)
|
|
479
|
+
.get(input.memoryId);
|
|
480
|
+
if (targetValue === null) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const target = sqlRecord(targetValue, "memory update target");
|
|
484
|
+
const conflictValue = this.database
|
|
485
|
+
.query(
|
|
486
|
+
`SELECT memory_id
|
|
487
|
+
FROM memories
|
|
488
|
+
WHERE text_sha256 = ? AND memory_id <> ?`,
|
|
489
|
+
)
|
|
490
|
+
.get(sha256(input.text), input.memoryId);
|
|
491
|
+
if (conflictValue !== null) {
|
|
492
|
+
const conflict = sqlRecord(conflictValue, "memory update conflict");
|
|
493
|
+
outcome = Object.freeze({
|
|
494
|
+
ok: false,
|
|
495
|
+
code: "memory_duplicate",
|
|
496
|
+
conflictMemoryId: sqlString(conflict.memory_id, "memory_id"),
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const rowid = sqlInteger(target.rowid, "memory rowid");
|
|
502
|
+
if (this.ftsAvailable) {
|
|
503
|
+
deleteFtsEntry(
|
|
504
|
+
this.database,
|
|
505
|
+
rowid,
|
|
506
|
+
sqlString(target.text, "memory text"),
|
|
507
|
+
sqlSummary(target.summary, "memory summary"),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const result = this.database
|
|
511
|
+
.query(
|
|
512
|
+
`UPDATE memories
|
|
513
|
+
SET text = ?, summary = ?, text_sha256 = ?, embedding = ?
|
|
514
|
+
WHERE memory_id = ?`,
|
|
515
|
+
)
|
|
516
|
+
.run(
|
|
517
|
+
input.text,
|
|
518
|
+
input.summary,
|
|
519
|
+
sha256(input.text),
|
|
520
|
+
encodeEmbedding(input.embedding),
|
|
521
|
+
input.memoryId,
|
|
522
|
+
);
|
|
523
|
+
if (Number(result.changes) !== 1) {
|
|
524
|
+
throw new MemoryError(
|
|
525
|
+
"memory_write_failed",
|
|
526
|
+
`Memory update changed ${String(result.changes)} rows.`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (this.ftsAvailable) {
|
|
530
|
+
this.database
|
|
531
|
+
.query(
|
|
532
|
+
`INSERT INTO ${MEMORIES_FTS_TABLE}(rowid, text, summary) VALUES (?, ?, ?)`,
|
|
533
|
+
)
|
|
534
|
+
.run(rowid, input.text, input.summary);
|
|
535
|
+
}
|
|
536
|
+
outcome = Object.freeze({ ok: true, memoryId: input.memoryId });
|
|
537
|
+
});
|
|
538
|
+
return outcome;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
deleteMemory(memoryId: string): MemoryDeleteStoreResult {
|
|
542
|
+
this.requireOpen();
|
|
543
|
+
validateMemoryId(memoryId);
|
|
544
|
+
let outcome: MemoryDeleteStoreResult = Object.freeze({
|
|
545
|
+
ok: false,
|
|
546
|
+
code: "memory_not_found",
|
|
547
|
+
});
|
|
548
|
+
runImmediateTransaction(this.database, () => {
|
|
549
|
+
const targetValue = this.database
|
|
550
|
+
.query(
|
|
551
|
+
`SELECT rowid, text, summary
|
|
552
|
+
FROM memories
|
|
553
|
+
WHERE memory_id = ?`,
|
|
554
|
+
)
|
|
555
|
+
.get(memoryId);
|
|
556
|
+
if (targetValue === null) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const target = sqlRecord(targetValue, "memory delete target");
|
|
560
|
+
if (this.ftsAvailable) {
|
|
561
|
+
deleteFtsEntry(
|
|
562
|
+
this.database,
|
|
563
|
+
sqlInteger(target.rowid, "memory rowid"),
|
|
564
|
+
sqlString(target.text, "memory text"),
|
|
565
|
+
sqlSummary(target.summary, "memory summary"),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const result = this.database
|
|
569
|
+
.query("DELETE FROM memories WHERE memory_id = ?")
|
|
570
|
+
.run(memoryId);
|
|
571
|
+
if (Number(result.changes) !== 1) {
|
|
572
|
+
throw new MemoryError(
|
|
573
|
+
"memory_write_failed",
|
|
574
|
+
`Memory delete changed ${String(result.changes)} rows.`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
outcome = Object.freeze({ ok: true, memoryId });
|
|
578
|
+
});
|
|
579
|
+
return outcome;
|
|
580
|
+
}
|
|
581
|
+
|
|
420
582
|
count(): number {
|
|
421
583
|
this.requireOpen();
|
|
422
584
|
const row = this.database.query("SELECT COUNT(*) AS count FROM memories").get();
|
|
@@ -662,6 +824,20 @@ function backfillFtsIndex(database: Database): void {
|
|
|
662
824
|
);
|
|
663
825
|
}
|
|
664
826
|
|
|
827
|
+
function deleteFtsEntry(
|
|
828
|
+
database: Database,
|
|
829
|
+
rowid: number,
|
|
830
|
+
text: string,
|
|
831
|
+
summary: string,
|
|
832
|
+
): void {
|
|
833
|
+
database
|
|
834
|
+
.query(
|
|
835
|
+
`INSERT INTO ${MEMORIES_FTS_TABLE}(${MEMORIES_FTS_TABLE}, rowid, text, summary)
|
|
836
|
+
VALUES ('delete', ?, ?, ?)`,
|
|
837
|
+
)
|
|
838
|
+
.run(rowid, text, summary);
|
|
839
|
+
}
|
|
840
|
+
|
|
665
841
|
function countRows(database: Database, table: string): number {
|
|
666
842
|
const row = sqlRecord(
|
|
667
843
|
database.query(`SELECT COUNT(*) AS count FROM ${table}`).get(),
|
|
@@ -740,6 +916,42 @@ function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
|
|
|
740
916
|
}
|
|
741
917
|
}
|
|
742
918
|
|
|
919
|
+
function validateMemoryMutationInput(
|
|
920
|
+
input: {
|
|
921
|
+
readonly memoryId: string;
|
|
922
|
+
readonly text: string;
|
|
923
|
+
readonly summary: string;
|
|
924
|
+
readonly embedding: Float32Array;
|
|
925
|
+
},
|
|
926
|
+
dimensions: number,
|
|
927
|
+
): void {
|
|
928
|
+
validateMemoryId(input.memoryId);
|
|
929
|
+
if (
|
|
930
|
+
input.text.trim() !== input.text ||
|
|
931
|
+
Buffer.byteLength(input.text, "utf8") < 1 ||
|
|
932
|
+
Buffer.byteLength(input.text, "utf8") > MAX_MEMORY_TEXT_BYTES ||
|
|
933
|
+
input.summary.trim() !== input.summary ||
|
|
934
|
+
Buffer.byteLength(input.summary, "utf8") > MAX_MEMORY_SUMMARY_BYTES ||
|
|
935
|
+
input.embedding.length !== dimensions ||
|
|
936
|
+
[...input.embedding].some((value) => !Number.isFinite(value))
|
|
937
|
+
) {
|
|
938
|
+
throw new MemoryError(
|
|
939
|
+
"memory_write_invalid",
|
|
940
|
+
"Memory update text, summary, or embedding is invalid.",
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function validateMemoryId(memoryId: string): void {
|
|
946
|
+
if (
|
|
947
|
+
memoryId.trim() !== memoryId ||
|
|
948
|
+
Buffer.byteLength(memoryId, "utf8") < 1 ||
|
|
949
|
+
Buffer.byteLength(memoryId, "utf8") > MAX_MEMORY_ID_BYTES
|
|
950
|
+
) {
|
|
951
|
+
throw new MemoryError("memory_write_invalid", "Memory ID is invalid.");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
743
955
|
function validateEmbeddingIdentity(identity: MemoryEmbeddingIdentity): void {
|
|
744
956
|
if (
|
|
745
957
|
identity.name.trim() === "" ||
|
|
@@ -871,6 +1083,33 @@ function sqlSummary(value: unknown, name: string): string {
|
|
|
871
1083
|
return value;
|
|
872
1084
|
}
|
|
873
1085
|
|
|
1086
|
+
function sqlInteger(value: unknown, name: string): number {
|
|
1087
|
+
const integer = typeof value === "bigint" ? Number(value) : value;
|
|
1088
|
+
if (!Number.isSafeInteger(integer) || (integer as number) < 1) {
|
|
1089
|
+
throw new MemoryError(
|
|
1090
|
+
"memory_store_read_failed",
|
|
1091
|
+
`${name} must be a positive safe integer.`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
return integer as number;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function storedMemoryRecord(value: unknown): StoredMemoryRecord {
|
|
1098
|
+
const row = sqlRecord(value, "memory row");
|
|
1099
|
+
return Object.freeze({
|
|
1100
|
+
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
1101
|
+
text: sqlString(row.text, "memory text"),
|
|
1102
|
+
summary: sqlSummary(row.summary, "memory summary"),
|
|
1103
|
+
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
1104
|
+
sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
|
|
1105
|
+
sourceTurnId: sqlString(row.source_turn_id, "memory source_turn_id"),
|
|
1106
|
+
createdAt: requireUtcTimestamp(
|
|
1107
|
+
sqlString(row.created_at, "memory created_at"),
|
|
1108
|
+
"memory created_at",
|
|
1109
|
+
),
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
874
1113
|
function requireUtcTimestamp(value: string, name: string): string {
|
|
875
1114
|
if (!value.endsWith("Z") || Number.isNaN(Date.parse(value))) {
|
|
876
1115
|
throw new MemoryError(
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
2
|
+
import type { ToolCall } from "../agent/types";
|
|
3
|
+
import {
|
|
4
|
+
defineToolExecutor,
|
|
5
|
+
type MemoryUpdateRawResult,
|
|
6
|
+
type ToolDefinition,
|
|
7
|
+
type ToolExecutor,
|
|
8
|
+
} from "../tools/types";
|
|
9
|
+
import {
|
|
10
|
+
MAX_MEMORY_ID_BYTES,
|
|
11
|
+
MAX_MEMORY_SUMMARY_BYTES,
|
|
12
|
+
MAX_MEMORY_TEXT_BYTES,
|
|
13
|
+
MEMORY_UPDATE_TOOL_NAME,
|
|
14
|
+
} from "./contracts";
|
|
15
|
+
|
|
16
|
+
export const MEMORY_UPDATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
|
|
17
|
+
name: MEMORY_UPDATE_TOOL_NAME,
|
|
18
|
+
description: "Replace one global memory shared across sessions and workspaces.",
|
|
19
|
+
parameters: {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
id: {
|
|
24
|
+
type: "string",
|
|
25
|
+
minLength: 1,
|
|
26
|
+
maxLength: MAX_MEMORY_ID_BYTES,
|
|
27
|
+
description: "The memoryId returned by MemorySearch or MemoryGet.",
|
|
28
|
+
},
|
|
29
|
+
text: {
|
|
30
|
+
type: "string",
|
|
31
|
+
minLength: 1,
|
|
32
|
+
maxLength: MAX_MEMORY_TEXT_BYTES,
|
|
33
|
+
description: "The complete replacement one-line searchable index.",
|
|
34
|
+
},
|
|
35
|
+
summary: {
|
|
36
|
+
type: "string",
|
|
37
|
+
maxLength: MAX_MEMORY_SUMMARY_BYTES,
|
|
38
|
+
description:
|
|
39
|
+
"The complete replacement details, reasons, constraints, and scope.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["id", "text", "summary"],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export function createMemoryUpdateToolExecutor(options: {
|
|
47
|
+
readonly update: (
|
|
48
|
+
memoryId: string,
|
|
49
|
+
text: string,
|
|
50
|
+
summary: string,
|
|
51
|
+
call: ToolCall,
|
|
52
|
+
signal: AbortSignal,
|
|
53
|
+
) => Promise<MemoryUpdateRawResult>;
|
|
54
|
+
readonly recordInvalidCall: (call: ToolCall) => Promise<void>;
|
|
55
|
+
}): ToolExecutor {
|
|
56
|
+
return defineToolExecutor("memory_update", {
|
|
57
|
+
definition: MEMORY_UPDATE_TOOL_DEFINITION,
|
|
58
|
+
async execute(args, call, context): Promise<MemoryUpdateRawResult> {
|
|
59
|
+
const parsed = parseMemoryUpdateArgs(args);
|
|
60
|
+
if (!parsed.ok) {
|
|
61
|
+
throwIfTurnCancelled(context.signal);
|
|
62
|
+
await options.recordInvalidCall(call);
|
|
63
|
+
throwIfTurnCancelled(context.signal);
|
|
64
|
+
return { ok: false, error: parsed.error };
|
|
65
|
+
}
|
|
66
|
+
const result = await options.update(
|
|
67
|
+
parsed.memoryId,
|
|
68
|
+
parsed.text,
|
|
69
|
+
parsed.summary,
|
|
70
|
+
call,
|
|
71
|
+
context.signal,
|
|
72
|
+
);
|
|
73
|
+
throwIfTurnCancelled(context.signal);
|
|
74
|
+
return result;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ParsedMemoryUpdateArgs =
|
|
80
|
+
| {
|
|
81
|
+
readonly ok: true;
|
|
82
|
+
readonly memoryId: string;
|
|
83
|
+
readonly text: string;
|
|
84
|
+
readonly summary: string;
|
|
85
|
+
}
|
|
86
|
+
| { readonly ok: false; readonly error: string };
|
|
87
|
+
|
|
88
|
+
function parseMemoryUpdateArgs(args: unknown): ParsedMemoryUpdateArgs {
|
|
89
|
+
if (!isRecord(args)) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error:
|
|
93
|
+
"MemoryUpdate arguments must be an object containing only id, text, and summary.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const unexpected = Object.keys(args).find(
|
|
97
|
+
(key) => key !== "id" && key !== "text" && key !== "summary",
|
|
98
|
+
);
|
|
99
|
+
if (unexpected !== undefined) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `MemoryUpdate received unexpected field: ${unexpected}.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (typeof args.id !== "string") {
|
|
106
|
+
return { ok: false, error: "MemoryUpdate.id must be a string." };
|
|
107
|
+
}
|
|
108
|
+
const memoryId = args.id.trim();
|
|
109
|
+
const idBytes = Buffer.byteLength(memoryId, "utf8");
|
|
110
|
+
if (idBytes < 1 || idBytes > MAX_MEMORY_ID_BYTES) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `MemoryUpdate.id must be 1 to ${MAX_MEMORY_ID_BYTES} UTF-8 bytes after trimming.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (typeof args.text !== "string") {
|
|
117
|
+
return { ok: false, error: "MemoryUpdate.text must be a string." };
|
|
118
|
+
}
|
|
119
|
+
const text = args.text.trim();
|
|
120
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
121
|
+
if (textBytes < 1 || textBytes > MAX_MEMORY_TEXT_BYTES) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: `MemoryUpdate.text must be 1 to ${MAX_MEMORY_TEXT_BYTES} UTF-8 bytes after trimming.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (typeof args.summary !== "string") {
|
|
128
|
+
return { ok: false, error: "MemoryUpdate.summary must be a string." };
|
|
129
|
+
}
|
|
130
|
+
const summary = args.summary.trim();
|
|
131
|
+
if (Buffer.byteLength(summary, "utf8") > MAX_MEMORY_SUMMARY_BYTES) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: `MemoryUpdate.summary must be at most ${MAX_MEMORY_SUMMARY_BYTES} UTF-8 bytes after trimming.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, memoryId, text, summary };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
142
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
toolResultDisplayText,
|
|
6
6
|
} from "../agent/tool-result-content";
|
|
7
7
|
import type {
|
|
8
|
+
AskUserRawResult,
|
|
8
9
|
BashRawResult,
|
|
9
10
|
ContextMaintenanceRawResult,
|
|
10
11
|
DeleteFileRawResult,
|
|
@@ -12,8 +13,11 @@ import type {
|
|
|
12
13
|
GenericToolRawResult,
|
|
13
14
|
GlobRawResult,
|
|
14
15
|
GrepRawResult,
|
|
16
|
+
MemoryCreateRawResult,
|
|
17
|
+
MemoryDeleteRawResult,
|
|
15
18
|
MemoryGetRawResult,
|
|
16
19
|
MemorySearchRawResult,
|
|
20
|
+
MemoryUpdateRawResult,
|
|
17
21
|
McpToolRawResult,
|
|
18
22
|
ReadFileRawResult,
|
|
19
23
|
RecallRawResult,
|
|
@@ -30,6 +34,7 @@ import type {
|
|
|
30
34
|
WebSearchRawResult,
|
|
31
35
|
WriteFileRawResult,
|
|
32
36
|
} from "../tools/types";
|
|
37
|
+
import { MAX_MEMORY_TEXT_BYTES, truncateUtf8 } from "../memory/contracts";
|
|
33
38
|
|
|
34
39
|
export type ToolObservation = {
|
|
35
40
|
readonly content: readonly ToolResultContent[];
|
|
@@ -55,6 +60,12 @@ export class ObservationBuilder {
|
|
|
55
60
|
return textObservation(renderMemorySearchObservation(input.raw));
|
|
56
61
|
case "memory_get":
|
|
57
62
|
return textObservation(renderMemoryGetObservation(input.raw));
|
|
63
|
+
case "memory_create":
|
|
64
|
+
return textObservation(renderMemoryCreateObservation(input.raw, input.call));
|
|
65
|
+
case "memory_update":
|
|
66
|
+
return textObservation(renderMemoryUpdateObservation(input.raw, input.call));
|
|
67
|
+
case "memory_delete":
|
|
68
|
+
return textObservation(renderMemoryDeleteObservation(input.raw));
|
|
58
69
|
case "skill":
|
|
59
70
|
return textObservation(renderSkillObservation(input.raw));
|
|
60
71
|
case "write":
|
|
@@ -69,6 +80,8 @@ export class ObservationBuilder {
|
|
|
69
80
|
return textObservation(renderUpdatePlanObservation(input.raw));
|
|
70
81
|
case "wait":
|
|
71
82
|
return textObservation(renderWaitObservation(input.raw));
|
|
83
|
+
case "ask_user":
|
|
84
|
+
return textObservation(renderAskUserObservation(input.raw));
|
|
72
85
|
case "task_list":
|
|
73
86
|
return textObservation(renderTaskListObservation(input.raw));
|
|
74
87
|
case "task_output":
|
|
@@ -120,6 +133,15 @@ function renderWaitObservation(raw: WaitRawResult): string {
|
|
|
120
133
|
: `Wait failed: ${raw.error}`;
|
|
121
134
|
}
|
|
122
135
|
|
|
136
|
+
function renderAskUserObservation(raw: AskUserRawResult): string {
|
|
137
|
+
if (!raw.ok) {
|
|
138
|
+
return `AskUser failed: ${raw.error}`;
|
|
139
|
+
}
|
|
140
|
+
return raw.outcome === "selected"
|
|
141
|
+
? `User selected: ${raw.answer}`
|
|
142
|
+
: "The user did not select an option. Decide how to proceed.";
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
function renderContextMaintenanceObservation(raw: ContextMaintenanceRawResult): string {
|
|
124
146
|
if (!raw.ok) {
|
|
125
147
|
if (raw.operation === "swap" && raw.rejected.length > 0) {
|
|
@@ -371,6 +393,54 @@ function renderMemoryGetObservation(raw: MemoryGetRawResult): string {
|
|
|
371
393
|
].join("\n\n");
|
|
372
394
|
}
|
|
373
395
|
|
|
396
|
+
function renderMemoryCreateObservation(
|
|
397
|
+
raw: MemoryCreateRawResult,
|
|
398
|
+
call: ToolCall,
|
|
399
|
+
): string {
|
|
400
|
+
if (!raw.ok) {
|
|
401
|
+
return `MemoryCreate failed: ${raw.error}`;
|
|
402
|
+
}
|
|
403
|
+
const text = memoryMutationText(call);
|
|
404
|
+
const result = `MemoryCreate ${raw.status} memory=${raw.memoryId} created_at=${raw.createdAt}.`;
|
|
405
|
+
return text === undefined ? result : `${result}\ntext: ${text}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderMemoryUpdateObservation(
|
|
409
|
+
raw: MemoryUpdateRawResult,
|
|
410
|
+
call: ToolCall,
|
|
411
|
+
): string {
|
|
412
|
+
if (!raw.ok) {
|
|
413
|
+
if (raw.code === "memory_duplicate") {
|
|
414
|
+
return `MemoryUpdate failed: code=${raw.code} conflict_memory=${raw.conflictMemoryId} error=${raw.error}`;
|
|
415
|
+
}
|
|
416
|
+
return raw.code === "memory_not_found"
|
|
417
|
+
? `MemoryUpdate failed: code=${raw.code} error=${raw.error}`
|
|
418
|
+
: `MemoryUpdate failed: ${raw.error}`;
|
|
419
|
+
}
|
|
420
|
+
const text = memoryMutationText(call);
|
|
421
|
+
const result = `MemoryUpdate ${raw.status} memory=${raw.memoryId}.`;
|
|
422
|
+
return text === undefined ? result : `${result}\ntext: ${text}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function renderMemoryDeleteObservation(raw: MemoryDeleteRawResult): string {
|
|
426
|
+
if (!raw.ok) {
|
|
427
|
+
return raw.code === "memory_not_found"
|
|
428
|
+
? `MemoryDelete failed: code=${raw.code} error=${raw.error}`
|
|
429
|
+
: `MemoryDelete failed: ${raw.error}`;
|
|
430
|
+
}
|
|
431
|
+
return `MemoryDelete ${raw.status} memory=${raw.memoryId}.`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function memoryMutationText(call: ToolCall): string | undefined {
|
|
435
|
+
if (typeof call.args !== "object" || call.args === null || Array.isArray(call.args)) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
const text = (call.args as Record<string, unknown>).text;
|
|
439
|
+
return typeof text === "string"
|
|
440
|
+
? truncateUtf8(text.trim(), MAX_MEMORY_TEXT_BYTES)
|
|
441
|
+
: undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
374
444
|
export function renderSkillObservation(raw: SkillRawResult): string {
|
|
375
445
|
if (!raw.ok) {
|
|
376
446
|
return `Skill failed for ${raw.name || "(unknown skill)"} (${raw.errorCode}): ${raw.error}`;
|
|
@@ -50,7 +50,11 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
|
50
50
|
"context_maintenance",
|
|
51
51
|
"memory_search",
|
|
52
52
|
"memory_get",
|
|
53
|
+
"memory_create",
|
|
54
|
+
"memory_update",
|
|
55
|
+
"memory_delete",
|
|
53
56
|
"wait",
|
|
57
|
+
"ask_user",
|
|
54
58
|
"skill",
|
|
55
59
|
"mcp",
|
|
56
60
|
"generic",
|