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.
@@ -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",