tinker-agent 2.0.0 → 2.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +27 -2
  3. package/package.json +2 -1
  4. package/src/agent/context-meter.ts +2 -4
  5. package/src/agent/runtime-session.ts +9 -2
  6. package/src/agent/session-ledger.ts +12 -5
  7. package/src/agent/tool-result-content.ts +76 -0
  8. package/src/agent/types.ts +14 -2
  9. package/src/cli/config.ts +4 -0
  10. package/src/cli/model-profiles.ts +41 -2
  11. package/src/cli/public-config-contract.ts +30 -7
  12. package/src/cli/runner-dependencies.ts +5 -0
  13. package/src/cli/tui-memory.ts +1 -0
  14. package/src/cli/tui-runner.tsx +4 -0
  15. package/src/context/compiled-context-hash.ts +2 -1
  16. package/src/context/compiled-context-validator.ts +13 -4
  17. package/src/context/context-protocol-validator.ts +33 -2
  18. package/src/context/context-revision-compiler.ts +2 -1
  19. package/src/context/context-revision.ts +8 -2
  20. package/src/context/context-swap-renderer.ts +46 -12
  21. package/src/context/prefix-retirement-planner.ts +13 -9
  22. package/src/context/protocol-frame.ts +74 -7
  23. package/src/context/swap-planner.ts +19 -14
  24. package/src/events/observation-text-log.ts +1 -1
  25. package/src/events/stdout-event-printer.ts +6 -0
  26. package/src/image/image-asset-store.ts +32 -3
  27. package/src/memory/contracts.ts +63 -3
  28. package/src/memory/memory-coordinator.ts +319 -49
  29. package/src/memory/memory-extractor.ts +48 -48
  30. package/src/memory/memory-get-tool.ts +86 -0
  31. package/src/memory/memory-search-tool.ts +122 -33
  32. package/src/memory/memory-store.ts +227 -20
  33. package/src/model/fake-model-client.ts +129 -76
  34. package/src/model/model-client.ts +62 -11
  35. package/src/model/openai-chat-mapping.ts +2 -1
  36. package/src/model/openai-chat-model-client.ts +22 -10
  37. package/src/model/openai-model-utils.ts +61 -30
  38. package/src/model/openai-responses-mapping.ts +25 -1
  39. package/src/model/openai-responses-model-client.ts +27 -11
  40. package/src/model/token-estimator.ts +10 -0
  41. package/src/observation/observation-builder.ts +100 -25
  42. package/src/session/session-history-reader.ts +128 -5
  43. package/src/session/session-schema.ts +59 -9
  44. package/src/session/session-store.ts +343 -196
  45. package/src/tools/registry.ts +18 -0
  46. package/src/tools/types.ts +46 -0
  47. package/src/tools/view-image.ts +89 -0
  48. package/src/tools/wait.ts +85 -0
  49. package/src/tui/components/memory-browser.tsx +3 -0
  50. package/src/tui/components/prompt-input.tsx +48 -25
  51. package/src/tui/event-store.ts +61 -2
@@ -12,10 +12,12 @@ import { createTaskInputToolExecutor } from "./task-input";
12
12
  import { createTaskOutputToolExecutor } from "./task-output-tool";
13
13
  import { createTaskStopToolExecutor } from "./task-stop";
14
14
  import { createUpdatePlanToolExecutor } from "./update-plan";
15
+ import { createWaitToolExecutor } from "./wait";
15
16
  import { createWebFetchToolExecutor } from "./web-fetch";
16
17
  import type { Refiner } from "./web-fetch/refiner";
17
18
  import { createWebSearchToolExecutor } from "./web-search";
18
19
  import { createWriteToolExecutor } from "./write";
20
+ import { createViewImageToolExecutor } from "./view-image";
19
21
  import { TurnUndoManager } from "./turn-undo-manager";
20
22
  import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
21
23
  import type {
@@ -39,6 +41,7 @@ import {
39
41
  DEFAULT_PUBLIC_TOOLING_CONFIG,
40
42
  type PublicToolingConfig,
41
43
  } from "../cli/public-config-contract";
44
+ import type { ImageAssetStore } from "../image/image-asset-store";
42
45
 
43
46
  export class ToolRegistry {
44
47
  private readonly tools = new Map<string, ToolExecutor>();
@@ -158,7 +161,10 @@ export function createDefaultTooling(options: {
158
161
  skillCoordinator?: SkillActivationCoordinator;
159
162
  toolingConfig?: PublicToolingConfig;
160
163
  memorySearch?: ToolExecutor;
164
+ memoryGet?: ToolExecutor;
161
165
  enableTurnUndo?: boolean;
166
+ imageAssetStore?: ImageAssetStore;
167
+ supportsViewImage?: boolean;
162
168
  bashGuard?: {
163
169
  readonly surface: "tui" | "one-shot";
164
170
  confirm(
@@ -206,6 +212,14 @@ export function createDefaultTooling(options: {
206
212
  maxContentBytes: options.maxReadContentBytes,
207
213
  }),
208
214
  );
215
+ if (options.supportsViewImage === true) {
216
+ if (options.imageAssetStore === undefined) {
217
+ throw new Error("ViewImage tooling requires an image asset store.");
218
+ }
219
+ registry.register(
220
+ createViewImageToolExecutor({ imageAssetStore: options.imageAssetStore }),
221
+ );
222
+ }
209
223
  registry.register(
210
224
  createRecallSearchToolExecutor({ historyReader: options.historyReader }),
211
225
  );
@@ -215,6 +229,9 @@ export function createDefaultTooling(options: {
215
229
  if (options.memorySearch !== undefined) {
216
230
  registry.register(options.memorySearch);
217
231
  }
232
+ if (options.memoryGet !== undefined) {
233
+ registry.register(options.memoryGet);
234
+ }
218
235
  if (options.skillCatalog !== undefined) {
219
236
  if (options.skillCatalog.skills.size === 0) {
220
237
  throw new Error("An empty Agent Skill catalog must not register tooling.");
@@ -262,6 +279,7 @@ export function createDefaultTooling(options: {
262
279
  }),
263
280
  );
264
281
  registry.register(createUpdatePlanToolExecutor());
282
+ registry.register(createWaitToolExecutor());
265
283
  registry.register(createTaskListToolExecutor({ taskManager }));
266
284
  registry.register(createTaskOutputToolExecutor({ taskManager }));
267
285
  registry.register(createTaskInputToolExecutor({ taskManager }));
@@ -1,4 +1,5 @@
1
1
  import type { ToolCall } from "../agent/types";
2
+ import type { ImageAssetRef } from "../image/image-types";
2
3
  import type { SessionId } from "../ids/runtime-id";
3
4
  import type {
4
5
  RecallGetPage,
@@ -38,6 +39,14 @@ export type ReadFileRawResult = {
38
39
  error?: string;
39
40
  };
40
41
 
42
+ export type ViewImageRawResult = {
43
+ ok: boolean;
44
+ filePath: string;
45
+ originalName?: string;
46
+ asset?: ImageAssetRef;
47
+ error?: string;
48
+ };
49
+
41
50
  export type WriteFileRawResult = {
42
51
  ok: boolean;
43
52
  filePath: string;
@@ -304,10 +313,15 @@ export type RecallRawResult = RecallSearchRawResult | RecallGetRawResult;
304
313
  export type MemorySearchRawResult =
305
314
  | {
306
315
  ok: true;
316
+ degraded: "vector" | "fts" | null;
307
317
  matches: readonly {
318
+ memoryId: string;
308
319
  text: string;
320
+ summary: string;
309
321
  score: number;
322
+ via: readonly ("vector" | "fts")[];
310
323
  sourceWorkspace: string;
324
+ sourceSessionId: string;
311
325
  createdAt: string;
312
326
  }[];
313
327
  }
@@ -316,6 +330,24 @@ export type MemorySearchRawResult =
316
330
  error: string;
317
331
  };
318
332
 
333
+ export type MemoryGetRawResult =
334
+ | {
335
+ ok: true;
336
+ memory: {
337
+ memoryId: string;
338
+ text: string;
339
+ summary: string;
340
+ sourceWorkspace: string;
341
+ sourceSessionId: string;
342
+ sourceTurnId: string;
343
+ createdAt: string;
344
+ } | null;
345
+ }
346
+ | {
347
+ ok: false;
348
+ error: string;
349
+ };
350
+
319
351
  export type SkillRawResult =
320
352
  | {
321
353
  ok: true;
@@ -365,8 +397,20 @@ export type McpToolRawResult = {
365
397
  error?: string;
366
398
  };
367
399
 
400
+ export type WaitRawResult =
401
+ | {
402
+ ok: true;
403
+ seconds: number;
404
+ waitedMs: number;
405
+ }
406
+ | {
407
+ ok: false;
408
+ error: string;
409
+ };
410
+
368
411
  export type ToolRawResultByKind = {
369
412
  read: ReadFileRawResult;
413
+ view_image: ViewImageRawResult;
370
414
  write: WriteFileRawResult;
371
415
  edit: EditFileRawResult;
372
416
  delete: DeleteFileRawResult;
@@ -382,6 +426,8 @@ export type ToolRawResultByKind = {
382
426
  web_fetch: WebFetchRawResult;
383
427
  recall: RecallRawResult;
384
428
  memory_search: MemorySearchRawResult;
429
+ memory_get: MemoryGetRawResult;
430
+ wait: WaitRawResult;
385
431
  skill: SkillRawResult;
386
432
  mcp: McpToolRawResult;
387
433
  generic: GenericToolRawResult;
@@ -0,0 +1,89 @@
1
+ import type { ImageAssetStore } from "../image/image-asset-store";
2
+ import { defineToolExecutor } from "./types";
3
+
4
+ export const VIEW_IMAGE_TOOL_DEFINITION = Object.freeze({
5
+ name: "ViewImage",
6
+ description:
7
+ "View one local image and return it to the model. Supports PNG, JPEG, and WebP. " +
8
+ "Relative paths resolve within the workspace; absolute paths may point outside it. " +
9
+ "Symbolic links are not supported.",
10
+ parameters: Object.freeze({
11
+ type: "object",
12
+ additionalProperties: false,
13
+ properties: Object.freeze({
14
+ file_path: Object.freeze({
15
+ type: "string",
16
+ description: "Workspace-relative path or absolute path to one image file.",
17
+ }),
18
+ }),
19
+ required: Object.freeze(["file_path"]),
20
+ }),
21
+ });
22
+
23
+ export function createViewImageToolExecutor(input: {
24
+ imageAssetStore: ImageAssetStore;
25
+ }) {
26
+ return defineToolExecutor("view_image", {
27
+ definition: VIEW_IMAGE_TOOL_DEFINITION,
28
+ async execute(args, _call, context) {
29
+ context.signal.throwIfAborted();
30
+ const parsed = parseArguments(args);
31
+ if (!parsed.ok) {
32
+ return {
33
+ ok: false,
34
+ filePath: parsed.filePath,
35
+ error: parsed.error,
36
+ };
37
+ }
38
+ try {
39
+ const imported = await input.imageAssetStore.importFile(parsed.filePath, {
40
+ signal: context.signal,
41
+ });
42
+ context.signal.throwIfAborted();
43
+ return {
44
+ ok: true,
45
+ filePath: parsed.filePath,
46
+ originalName: imported.originalName,
47
+ asset: imported.asset,
48
+ };
49
+ } catch (error) {
50
+ context.signal.throwIfAborted();
51
+ return {
52
+ ok: false,
53
+ filePath: parsed.filePath,
54
+ error: error instanceof Error ? error.message : String(error),
55
+ };
56
+ }
57
+ },
58
+ });
59
+ }
60
+
61
+ function parseArguments(
62
+ value: unknown,
63
+ ):
64
+ | { readonly ok: true; readonly filePath: string }
65
+ | { readonly ok: false; readonly filePath: string; readonly error: string } {
66
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
67
+ return {
68
+ ok: false,
69
+ filePath: "",
70
+ error: "Arguments must be an object containing only file_path.",
71
+ };
72
+ }
73
+ const record = value as Record<string, unknown>;
74
+ const keys = Object.keys(record);
75
+ const filePath = typeof record.file_path === "string" ? record.file_path : "";
76
+ if (
77
+ keys.length !== 1 ||
78
+ keys[0] !== "file_path" ||
79
+ typeof record.file_path !== "string" ||
80
+ record.file_path.trim() === ""
81
+ ) {
82
+ return {
83
+ ok: false,
84
+ filePath,
85
+ error: "file_path must be the only argument and must be a non-empty string.",
86
+ };
87
+ }
88
+ return { ok: true, filePath: record.file_path };
89
+ }
@@ -0,0 +1,85 @@
1
+ import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
2
+ import {
3
+ defineToolExecutor,
4
+ type ToolExecutionContext,
5
+ type ToolExecutor,
6
+ type WaitRawResult,
7
+ } from "./types";
8
+
9
+ const MIN_WAIT_SECONDS = 1;
10
+ const MAX_WAIT_SECONDS = 3600;
11
+
12
+ export function createWaitToolExecutor(): ToolExecutor {
13
+ return defineToolExecutor("wait", {
14
+ definition: {
15
+ name: "Wait",
16
+ description:
17
+ `Wait for a given number of integer seconds before continuing, from ${MIN_WAIT_SECONDS} to ${MAX_WAIT_SECONDS}. ` +
18
+ "Use this to pause between polling attempts or to give an external process time to make progress.",
19
+ parameters: {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ properties: {
23
+ seconds: {
24
+ type: "integer",
25
+ minimum: MIN_WAIT_SECONDS,
26
+ maximum: MAX_WAIT_SECONDS,
27
+ description: "Whole seconds to wait.",
28
+ },
29
+ },
30
+ required: ["seconds"],
31
+ },
32
+ },
33
+ async execute(args, _call, context: ToolExecutionContext): Promise<WaitRawResult> {
34
+ throwIfTurnCancelled(context.signal);
35
+ const parsed = parseWaitArgs(args);
36
+ if (!parsed.ok) {
37
+ return parsed;
38
+ }
39
+ const startedAt = Date.now();
40
+ await interruptibleSleep(parsed.seconds * 1_000, context.signal);
41
+ throwIfTurnCancelled(context.signal);
42
+ return { ok: true, seconds: parsed.seconds, waitedMs: Date.now() - startedAt };
43
+ },
44
+ });
45
+ }
46
+
47
+ type ParsedWaitArgs = { ok: true; seconds: number } | { ok: false; error: string };
48
+
49
+ function parseWaitArgs(args: unknown): ParsedWaitArgs {
50
+ if (!isRecord(args)) {
51
+ return { ok: false, error: "Wait arguments must be an object." };
52
+ }
53
+ const unexpected = Object.keys(args).find((key) => key !== "seconds");
54
+ if (unexpected !== undefined) {
55
+ return { ok: false, error: `Wait received unexpected argument: ${unexpected}.` };
56
+ }
57
+ if (typeof args.seconds !== "number" || !Number.isInteger(args.seconds)) {
58
+ return { ok: false, error: "Wait seconds must be an integer." };
59
+ }
60
+ if (args.seconds < MIN_WAIT_SECONDS || args.seconds > MAX_WAIT_SECONDS) {
61
+ return {
62
+ ok: false,
63
+ error: `Wait seconds must be between ${MIN_WAIT_SECONDS} and ${MAX_WAIT_SECONDS}.`,
64
+ };
65
+ }
66
+ return { ok: true, seconds: args.seconds };
67
+ }
68
+
69
+ function interruptibleSleep(ms: number, signal: AbortSignal): Promise<void> {
70
+ return new Promise((resolve, reject) => {
71
+ const onAbort = () => {
72
+ clearTimeout(timer);
73
+ reject(cancellationError(signal));
74
+ };
75
+ const timer = setTimeout(() => {
76
+ signal.removeEventListener("abort", onAbort);
77
+ resolve();
78
+ }, ms);
79
+ signal.addEventListener("abort", onAbort, { once: true });
80
+ });
81
+ }
82
+
83
+ function isRecord(value: unknown): value is Record<string, unknown> {
84
+ return typeof value === "object" && value !== null && !Array.isArray(value);
85
+ }
@@ -93,6 +93,9 @@ export function MemoryBrowser(props: MemoryBrowserProps) {
93
93
  {formatMemoryCreatedAt(memory.createdAt)} · {memory.sourceWorkspace}
94
94
  </Text>
95
95
  <Text>{normalizeMemoryDisplayText(memory.text)}</Text>
96
+ {memory.summary === "" ? null : (
97
+ <Text dimColor>{normalizeMemoryDisplayText(memory.summary)}</Text>
98
+ )}
96
99
  </Box>
97
100
  ))}
98
101
  </Box>
@@ -741,30 +741,42 @@ export function PromptInput(props: PromptInputProps) {
741
741
  </Box>
742
742
  {showSuggestions ? null : (
743
743
  <Box>
744
- <Text dimColor>
745
- {props.modelName}
746
- {props.reasoningEffort === undefined ? null : ` ${props.reasoningEffort}`} ·{" "}
747
- {formatWorkspacePath(props.workspaceRoot)}
748
- {props.gitBranch === undefined ? null : ` · ${props.gitBranch}`}
749
- {state.phase.kind === "idle" ? null : ` · ${phaseLabel(state.phase)}`}
744
+ <Text>
745
+ <Text color={FOOTER_COLORS.model}>{props.modelName}</Text>
746
+ {props.reasoningEffort === undefined ? null : (
747
+ <Text dimColor> {props.reasoningEffort}</Text>
748
+ )}
749
+ <Text dimColor> · </Text>
750
+ <Text color={FOOTER_COLORS.workspacePath}>
751
+ {formatWorkspacePath(props.workspaceRoot)}
752
+ </Text>
753
+ {props.gitBranch === undefined ? null : (
754
+ <>
755
+ <Text dimColor> · </Text>
756
+ <Text color={FOOTER_COLORS.gitBranch}>{props.gitBranch}</Text>
757
+ </>
758
+ )}
759
+ {state.phase.kind === "idle" ? null : (
760
+ <>
761
+ <Text dimColor> · </Text>
762
+ <Text dimColor>{phaseLabel(state.phase)}</Text>
763
+ </>
764
+ )}
765
+ {props.contextUsage === undefined ? null : (
766
+ <>
767
+ <Text dimColor> · </Text>
768
+ <Text color={contextColor(props.contextUsage.pressure)}>
769
+ {formatContextUsageLine(props.contextUsage)}
770
+ </Text>
771
+ </>
772
+ )}
773
+ {cacheRate === undefined ? null : (
774
+ <>
775
+ <Text dimColor> · </Text>
776
+ <Text color={FOOTER_COLORS.cacheRate}>{cacheRate}</Text>
777
+ </>
778
+ )}
750
779
  </Text>
751
- {props.contextUsage === undefined ? null : (
752
- <>
753
- <Text dimColor> · </Text>
754
- <Text
755
- color={contextColor(props.contextUsage.pressure)}
756
- dimColor={props.contextUsage.pressure === "normal"}
757
- >
758
- {formatContextUsageLine(props.contextUsage)}
759
- </Text>
760
- </>
761
- )}
762
- {cacheRate === undefined ? null : (
763
- <>
764
- <Text dimColor> · </Text>
765
- <Text dimColor>{cacheRate}</Text>
766
- </>
767
- )}
768
780
  </Box>
769
781
  )}
770
782
  {showFileSuggestions ? (
@@ -962,14 +974,25 @@ function fileMentionReplacement(
962
974
  : `${filePath} `;
963
975
  }
964
976
 
977
+ // Decorative palette for the footer status line, kept muted so it reads
978
+ // softly against the dim separators. Red and yellow are deliberately
979
+ // excluded: they stay reserved for context-pressure warnings.
980
+ const FOOTER_COLORS = {
981
+ model: "#7b9cc4",
982
+ workspacePath: "#6fa8a8",
983
+ gitBranch: "#a08fbd",
984
+ cacheRate: "#7aa37e",
985
+ contextUsage: "#8a9199",
986
+ } as const;
987
+
965
988
  function contextColor(
966
989
  pressure: ContextUsageSnapshot["pressure"],
967
- ): "yellow" | "red" | undefined {
990
+ ): "yellow" | "red" | typeof FOOTER_COLORS.contextUsage {
968
991
  return pressure === "blocked"
969
992
  ? "red"
970
993
  : pressure === "triggered"
971
994
  ? "yellow"
972
- : undefined;
995
+ : FOOTER_COLORS.contextUsage;
973
996
  }
974
997
 
975
998
  function formatWorkspacePath(workspaceRoot: string): string {
@@ -785,8 +785,11 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
785
785
  if (input.name === "Grep") {
786
786
  return `Grep ${toolPattern(input.args) ?? ""}`.trim();
787
787
  }
788
- if (input.name === "WebSearch" || input.name === "MemorySearch") {
789
- return `${input.name} ${toolQuery(input.args) ?? ""}`.trim();
788
+ if (input.name === "WebSearch") {
789
+ return `WebSearch ${toolQuery(input.args) ?? ""}`.trim();
790
+ }
791
+ if (input.name === "MemorySearch") {
792
+ return `MemorySearch ${memorySearchDetail(input.args)}`.trim();
790
793
  }
791
794
  if (input.name === "WebFetch") {
792
795
  return `WebFetch ${toolUrl(input.args) ?? ""}`.trim();
@@ -801,6 +804,9 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
801
804
  if (input.name === "TaskList") {
802
805
  return "TaskList";
803
806
  }
807
+ if (input.name === "Wait") {
808
+ return `Wait ${toolSeconds(input.args) ?? ""}`.trim();
809
+ }
804
810
  if (input.name === "Skill") {
805
811
  return `Skill ${stringProperty(asRecord(input.args), "name") ?? ""}`.trim();
806
812
  }
@@ -816,6 +822,10 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
816
822
  }
817
823
 
818
824
  switch (raw.kind) {
825
+ case "view_image":
826
+ return raw.asset === undefined
827
+ ? base
828
+ : `${base} -> ${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes`;
819
829
  case "glob":
820
830
  return raw.ok && raw.matchCount !== undefined
821
831
  ? `${base} -> ${raw.matchCount} match${raw.matchCount === 1 ? "" : "es"}`
@@ -878,6 +888,11 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
878
888
  return base;
879
889
  }
880
890
  return `${base} -> ${raw.matches.length} derived memor${raw.matches.length === 1 ? "y" : "ies"}`;
891
+ case "memory_get":
892
+ if (!raw.ok) {
893
+ return base;
894
+ }
895
+ return raw.memory === null ? `${base} -> not found` : `${base} -> found`;
881
896
  case "skill":
882
897
  if (!raw.ok) {
883
898
  return `${base} failed -> ${boundedToolError(raw.error)}`;
@@ -915,6 +930,8 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
915
930
  const completed = raw.plan.filter((step) => step.status === "completed").length;
916
931
  return `${base} -> ${completed}/${raw.plan.length} completed`;
917
932
  }
933
+ case "wait":
934
+ return raw.ok ? `${base} -> done` : base;
918
935
  case "mcp":
919
936
  case "generic":
920
937
  return base;
@@ -951,6 +968,7 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
951
968
  return detail === undefined ? {} : { bash: detail };
952
969
  }
953
970
  case "read":
971
+ case "view_image":
954
972
  case "write":
955
973
  case "edit":
956
974
  case "delete":
@@ -963,6 +981,8 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
963
981
  case "web_fetch":
964
982
  case "recall":
965
983
  case "memory_search":
984
+ case "memory_get":
985
+ case "wait":
966
986
  case "skill":
967
987
  case "mcp":
968
988
  case "generic":
@@ -988,6 +1008,7 @@ function toolRawResultDiff(
988
1008
  diffTruncated: raw.patchTruncated === true,
989
1009
  };
990
1010
  case "read":
1011
+ case "view_image":
991
1012
  case "delete":
992
1013
  case "glob":
993
1014
  case "grep":
@@ -1001,6 +1022,8 @@ function toolRawResultDiff(
1001
1022
  case "web_fetch":
1002
1023
  case "recall":
1003
1024
  case "memory_search":
1025
+ case "memory_get":
1026
+ case "wait":
1004
1027
  case "skill":
1005
1028
  case "mcp":
1006
1029
  case "generic":
@@ -1067,10 +1090,46 @@ function toolQuery(args: unknown): string | undefined {
1067
1090
  return stringProperty(asRecord(args), "query");
1068
1091
  }
1069
1092
 
1093
+ function memorySearchDetail(args: unknown): string {
1094
+ const parts: string[] = [];
1095
+ const query = toolQuery(args);
1096
+ if (query !== undefined) {
1097
+ parts.push(query);
1098
+ }
1099
+ const keywords = toolKeywords(args);
1100
+ if (keywords !== undefined) {
1101
+ parts.push(`[${keywords}]`);
1102
+ }
1103
+ return parts.join(" ");
1104
+ }
1105
+
1106
+ function toolKeywords(args: unknown): string | undefined {
1107
+ const limit = 120;
1108
+ const value = asRecord(args).keywords;
1109
+ if (!Array.isArray(value)) {
1110
+ return undefined;
1111
+ }
1112
+ const keywords = value.filter(
1113
+ (keyword): keyword is string => typeof keyword === "string" && keyword.length > 0,
1114
+ );
1115
+ if (keywords.length === 0) {
1116
+ return undefined;
1117
+ }
1118
+ const joined = keywords.join(", ");
1119
+ return joined.length <= limit ? joined : `${joined.slice(0, limit)}…`;
1120
+ }
1121
+
1070
1122
  function toolUrl(args: unknown): string | undefined {
1071
1123
  return stringProperty(asRecord(args), "url");
1072
1124
  }
1073
1125
 
1126
+ function toolSeconds(args: unknown): string | undefined {
1127
+ const seconds = asRecord(args).seconds;
1128
+ return typeof seconds === "number" && Number.isFinite(seconds)
1129
+ ? `${seconds}s`
1130
+ : undefined;
1131
+ }
1132
+
1074
1133
  function toolPath(args: unknown): string | undefined {
1075
1134
  const record = asRecord(args);
1076
1135
  return stringProperty(record, "file_path");