tinker-agent 2.11.0 → 2.12.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.
@@ -46,7 +46,7 @@ import { loadSkillCatalog } from "../skills/skill-loader";
46
46
  import { loadProjectSlashCommands } from "../tui/project-slash-commands";
47
47
  import { createWorkspaceFileLister } from "../tui/workspace-file-search";
48
48
  import { clipboardWriterForEnvironment } from "../tui/clipboard";
49
- import { initializeTuiMemory } from "./tui-memory";
49
+ import { listMemoryFiles } from "../memory/memory-files";
50
50
  import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
51
51
  import { createReasoningEffortController } from "../model/reasoning-effort";
52
52
 
@@ -63,13 +63,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
63
63
  options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
64
64
  const config = options.initialRunnerConfig;
65
65
  const workspaceRoot = await realpath(config.workspaceRoot);
66
- const memory = await initializeTuiMemory({
67
- config:
68
- options.publicConfig.mode === "profile" ? options.publicConfig.memory : undefined,
69
- env: options.env,
70
- });
71
- const memoryCoordinator = memory.coordinator;
72
- const memoryNotice = memory.notice;
73
66
  let controller: DefaultTuiSessionController | undefined;
74
67
  let instance: ReturnType<typeof render> | undefined;
75
68
  let disposeReason: SessionDisposeReason = { type: "tui_exit" };
@@ -125,31 +118,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
125
118
  source: sessionConfig.bashGuardSource,
126
119
  surface: "tui" as const,
127
120
  },
128
- ...(memoryCoordinator === undefined
129
- ? {}
130
- : {
131
- memorySearch: memoryCoordinator.createSearchToolExecutor({
132
- workspaceRoot,
133
- sessionId,
134
- }),
135
- memoryGet: memoryCoordinator.createGetToolExecutor({
136
- workspaceRoot,
137
- sessionId,
138
- }),
139
- memoryCreate: memoryCoordinator.createCreateToolExecutor({
140
- workspaceRoot,
141
- sessionId,
142
- }),
143
- memoryUpdate: memoryCoordinator.createUpdateToolExecutor({
144
- workspaceRoot,
145
- sessionId,
146
- }),
147
- memoryDelete: memoryCoordinator.createDeleteToolExecutor({
148
- workspaceRoot,
149
- sessionId,
150
- }),
151
- completedTurnHook: memoryCoordinator,
152
- }),
153
121
  };
154
122
  if (mode === "resume") {
155
123
  return createRuntimeSession({
@@ -320,13 +288,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
320
288
  onQuit={() => {
321
289
  quitRequested = true;
322
290
  }}
323
- initialNotice={memoryNotice}
324
- memoryDisabledNotice={memoryNotice}
325
- listStoredMemories={
326
- memoryCoordinator === undefined
327
- ? undefined
328
- : () => memoryCoordinator.listStoredMemories()
329
- }
291
+ listStoredMemories={() => listMemoryFiles()}
330
292
  />,
331
293
  { incrementalRendering: true },
332
294
  );
@@ -350,7 +312,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
350
312
  );
351
313
  }
352
314
  }
353
- memoryCoordinator?.dispose();
354
315
  }
355
316
 
356
317
  if (primaryError !== undefined) {
@@ -197,6 +197,13 @@ function toolCallSummary(call: ToolCall): string {
197
197
  .join("\n");
198
198
  }
199
199
 
200
+ if (call.name === "MemorySearch" && Array.isArray(args.keywords)) {
201
+ return [
202
+ `Call ID: ${call.toolCallId}`,
203
+ `Keywords: ${JSON.stringify(args.keywords)}`,
204
+ ].join("\n");
205
+ }
206
+
200
207
  const filePath = stringProperty(args, "file_path");
201
208
  const pattern = stringProperty(args, "pattern");
202
209
  return [
@@ -1,106 +1,6 @@
1
- import type { SessionId, TurnId } from "../ids/runtime-id";
2
-
3
- export const MEMORY_SEARCH_TOOL_NAME = "MemorySearch" as const;
4
- export const MEMORY_GET_TOOL_NAME = "MemoryGet" as const;
5
1
  export const MEMORY_CREATE_TOOL_NAME = "MemoryCreate" as const;
6
- export const MEMORY_UPDATE_TOOL_NAME = "MemoryUpdate" as const;
7
- export const MEMORY_DELETE_TOOL_NAME = "MemoryDelete" as const;
8
- export const MEMORY_SCHEMA_VERSION = 2 as const;
9
2
  export const MAX_MEMORY_TEXT_BYTES = 512;
10
3
  export const MAX_MEMORY_SUMMARY_BYTES = 4_096;
11
- export const MAX_SEARCH_RESULT_SUMMARY_BYTES = 1_536;
12
- export const MAX_MEMORY_QUERY_BYTES = 1_024;
13
- export const MAX_MEMORY_ID_BYTES = 64;
14
- export const MEMORY_SEARCH_LIMIT = 5;
15
- export const MEMORY_RECALL_CANDIDATE_LIMIT = 20;
16
- export const MAX_MEMORY_KEYWORDS = 8;
17
- export const MAX_MEMORY_KEYWORD_BYTES = 128;
18
- export const MEMORY_RRF_K = 60;
19
- export const MEMORY_EXTRACTION_QUEUE_CAPACITY = 64;
20
- export const MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT = 10;
21
-
22
- export type MemoryEmbeddingKind = "openai-compatible";
23
-
24
- export type MemoryEmbeddingConfig = {
25
- readonly name: string;
26
- readonly kind: MemoryEmbeddingKind;
27
- readonly model: string;
28
- readonly apiBase: string;
29
- readonly apiKey: string;
30
- readonly dimensions: number;
31
- };
32
-
33
- export type MemoryEmbeddingIdentity = Pick<
34
- MemoryEmbeddingConfig,
35
- "name" | "kind" | "model" | "dimensions"
36
- >;
37
-
38
- export type MemoryPaths = {
39
- readonly directory: string;
40
- readonly database: string;
41
- readonly log: string;
42
- readonly extractedLog: string;
43
- };
44
-
45
- export type MemoryWriteCandidate = {
46
- readonly text: string;
47
- readonly summary: string;
48
- readonly embedding: Float32Array;
49
- };
50
-
51
- export type MemoryWriteBatch = {
52
- readonly workspaceRoot: string;
53
- readonly sessionId: SessionId;
54
- readonly turnId: TurnId;
55
- readonly candidates: readonly MemoryWriteCandidate[];
56
- };
57
-
58
- export type MemoryWriteResult = {
59
- readonly written: number;
60
- readonly duplicate: number;
61
- readonly inserted: readonly MemoryInsertedRecord[];
62
- };
63
-
64
- export type MemoryInsertedRecord = {
65
- readonly memoryId: string;
66
- readonly text: string;
67
- readonly createdAt: string;
68
- };
69
-
70
- export type MemorySearchMatch = {
71
- readonly memoryId: string;
72
- readonly text: string;
73
- readonly summary: string;
74
- readonly score: number;
75
- readonly sourceWorkspace: string;
76
- readonly sourceSessionId: string;
77
- readonly createdAt: string;
78
- };
79
-
80
- export type MemoryRecallPath = "vector" | "fts";
81
-
82
- export type MemoryFtsMatch = {
83
- readonly memoryId: string;
84
- readonly text: string;
85
- readonly summary: string;
86
- readonly bm25: number;
87
- readonly sourceWorkspace: string;
88
- readonly sourceSessionId: string;
89
- readonly createdAt: string;
90
- };
91
-
92
- export type MemoryHybridMatch = {
93
- readonly memoryId: string;
94
- readonly text: string;
95
- readonly summary: string;
96
- readonly score: number;
97
- readonly via: readonly MemoryRecallPath[];
98
- readonly sourceWorkspace: string;
99
- readonly sourceSessionId: string;
100
- readonly createdAt: string;
101
- };
102
-
103
- export type MemoryRecallDegraded = "vector" | "fts";
104
4
 
105
5
  export type StoredMemorySummary = {
106
6
  readonly memoryId: string;
@@ -111,168 +11,12 @@ export type StoredMemorySummary = {
111
11
  readonly createdAt: string;
112
12
  };
113
13
 
114
- export type StoredMemoryRecord = StoredMemorySummary & {
115
- readonly sourceTurnId: string;
116
- };
117
-
118
- export type StoredMemoryMutationRecord = StoredMemoryRecord & {
119
- readonly embedding: Float32Array;
120
- };
121
-
122
- export type MemoryUpdateStoreResult =
123
- | {
124
- readonly ok: true;
125
- readonly memoryId: string;
126
- }
127
- | {
128
- readonly ok: false;
129
- readonly code: "memory_not_found";
130
- }
131
- | {
132
- readonly ok: false;
133
- readonly code: "memory_duplicate";
134
- readonly conflictMemoryId: string;
135
- };
136
-
137
- export type MemoryDeleteStoreResult =
138
- | {
139
- readonly ok: true;
140
- readonly memoryId: string;
141
- }
142
- | {
143
- readonly ok: false;
144
- readonly code: "memory_not_found";
145
- };
146
-
147
- export type MemoryExtractionRejectedCounts = {
148
- readonly duplicate: number;
149
- readonly secret: number;
150
- readonly invalid: number;
151
- readonly embedding: number;
152
- };
153
-
154
- export type MemoryExtractionDiagnostic = {
155
- readonly at: string;
156
- readonly kind: "extraction";
157
- readonly outcome: "ok" | "failed" | "skipped";
158
- readonly reason: string | null;
159
- readonly workspace: string;
160
- readonly turnId: string;
161
- readonly inputTokens: number;
162
- readonly returned: number;
163
- readonly written: number;
164
- readonly rejected: MemoryExtractionRejectedCounts;
165
- readonly ms: number;
166
- /**
167
- * Bounded single-line error detail (message plus cause chain) recorded for
168
- * failed and skipped outcomes so provider/parse failures are diagnosable
169
- * from the log alone. Absent on success.
170
- */
171
- readonly detail?: string;
172
- };
173
-
174
- export type MemorySearchDiagnostic = {
175
- readonly at: string;
176
- readonly kind: "search";
177
- readonly outcome: "ok" | "failed" | "skipped";
178
- readonly reason: string | null;
179
- readonly workspace: string;
180
- readonly sessionId: string;
181
- readonly queryBytes: number;
182
- readonly keywordCount: number;
183
- readonly returned: number;
184
- readonly vectorReturned: number;
185
- readonly ftsReturned: number;
186
- readonly degraded: MemoryRecallDegraded | null;
187
- readonly scores: readonly number[];
188
- readonly vectorScores: readonly number[];
189
- readonly ms: number;
190
- };
191
-
192
- export type MemoryInitDiagnostic = {
193
- readonly at: string;
194
- readonly kind: "init";
195
- readonly outcome: "failed";
196
- readonly reason: string;
197
- };
198
-
199
- export type MemoryGetDiagnostic = {
200
- readonly at: string;
201
- readonly kind: "get";
202
- readonly outcome: "ok" | "failed";
203
- readonly reason: string | null;
204
- readonly workspace: string;
205
- readonly sessionId: string;
206
- readonly found: boolean;
207
- readonly ms: number;
208
- };
209
-
210
- export type MemoryMutationDiagnostic = {
211
- readonly at: string;
212
- readonly kind: "create" | "update" | "delete";
213
- readonly outcome: "ok" | "failed" | "skipped";
214
- readonly reason: string | null;
215
- readonly workspace: string;
216
- readonly sessionId: string;
217
- readonly turnId: string;
218
- readonly toolCallId: string;
219
- readonly memoryId: string | null;
220
- readonly ms: number;
221
- };
222
-
223
- export type MemoryDiagnostic =
224
- | MemoryExtractionDiagnostic
225
- | MemorySearchDiagnostic
226
- | MemoryGetDiagnostic
227
- | MemoryMutationDiagnostic
228
- | MemoryInitDiagnostic;
229
-
230
- export class MemoryError extends Error {
231
- constructor(
232
- readonly code: string,
233
- message: string,
234
- options?: ErrorOptions,
235
- ) {
236
- super(message, options);
237
- this.name = "MemoryError";
238
- }
239
- }
240
-
241
- export function memoryErrorCode(error: unknown, fallback: string): string {
242
- return error instanceof MemoryError ? error.code : fallback;
243
- }
244
-
245
14
  export function boundedMemoryError(error: unknown): string {
246
15
  const raw = error instanceof Error ? error.message : String(error);
247
16
  const singleLine = raw.replaceAll(/\s+/g, " ").trim() || "unknown memory error";
248
17
  return truncateUtf8(singleLine, 400);
249
18
  }
250
19
 
251
- export function boundedMemoryErrorDetail(error: unknown): string {
252
- const parts: string[] = [];
253
- let current: unknown = error;
254
- for (
255
- let depth = 0;
256
- depth < 4 && current !== undefined && current !== null;
257
- depth += 1
258
- ) {
259
- const raw =
260
- current instanceof Error
261
- ? current.message
262
- : typeof current === "string" ||
263
- typeof current === "number" ||
264
- typeof current === "boolean"
265
- ? String(current)
266
- : "unknown non-error cause";
267
- const singleLine = raw.replaceAll(/\s+/g, " ").trim();
268
- if (singleLine !== "" && !parts.includes(singleLine)) {
269
- parts.push(singleLine);
270
- }
271
- current = current instanceof Error ? current.cause : undefined;
272
- }
273
- return truncateUtf8(parts.join(" | ") || "unknown memory error", 400);
274
- }
275
-
276
20
  export function truncateUtf8(value: string, maxBytes: number): string {
277
21
  if (Buffer.byteLength(value, "utf8") <= maxBytes) {
278
22
  return value;
@@ -14,7 +14,8 @@ import {
14
14
 
15
15
  export const MEMORY_CREATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
16
16
  name: MEMORY_CREATE_TOOL_NAME,
17
- description: "Create one global memory shared across sessions and workspaces.",
17
+ description:
18
+ "Save an explicit memory as a Markdown note shared across sessions and workspaces. Returns a file path that Read can open.",
18
19
  parameters: {
19
20
  type: "object",
20
21
  additionalProperties: false,
@@ -23,7 +24,7 @@ export const MEMORY_CREATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
23
24
  type: "string",
24
25
  minLength: 1,
25
26
  maxLength: MAX_MEMORY_TEXT_BYTES,
26
- description: "A one-line searchable index for this memory.",
27
+ description: "A one-line Markdown heading for this memory.",
27
28
  },
28
29
  summary: {
29
30
  type: "string",
@@ -43,15 +44,12 @@ export function createMemoryCreateToolExecutor(options: {
43
44
  call: ToolCall,
44
45
  signal: AbortSignal,
45
46
  ) => Promise<MemoryCreateRawResult>;
46
- readonly recordInvalidCall: (call: ToolCall) => Promise<void>;
47
47
  }): ToolExecutor {
48
48
  return defineToolExecutor("memory_create", {
49
49
  definition: MEMORY_CREATE_TOOL_DEFINITION,
50
50
  async execute(args, call, context): Promise<MemoryCreateRawResult> {
51
51
  const parsed = parseMemoryCreateArgs(args);
52
52
  if (!parsed.ok) {
53
- throwIfTurnCancelled(context.signal);
54
- await options.recordInvalidCall(call);
55
53
  throwIfTurnCancelled(context.signal);
56
54
  return { ok: false, error: parsed.error };
57
55
  }
@@ -0,0 +1,145 @@
1
+ import { defaultHomeRoot } from "../session/workspace-storage";
2
+ import path from "node:path";
3
+ import { access, mkdir, open, readdir, rename, writeFile } from "node:fs/promises";
4
+ import { createUuidV7 } from "../ids/uuid-v7";
5
+ import { throwIfTurnCancelled } from "../agent/turn-cancellation";
6
+ import type { ToolExecutor } from "../tools/types";
7
+ import { boundedMemoryError, type StoredMemorySummary } from "./contracts";
8
+ import { createMemoryCreateToolExecutor } from "./memory-create-tool";
9
+
10
+ export function memoryDirectory(homeRoot = defaultHomeRoot()): string {
11
+ return path.resolve(homeRoot, ".tinker", "memory");
12
+ }
13
+
14
+ export function sessionMemoryPath(sessionId: string, homeRoot?: string): string {
15
+ return path.join(memoryDirectory(homeRoot), "records", `${sessionId}.md`);
16
+ }
17
+
18
+ /** Old database and diagnostic files are retained outside the searchable directory. */
19
+ export async function ensureMemoryDirectory(homeRoot?: string): Promise<string> {
20
+ const directory = memoryDirectory(homeRoot);
21
+ await mkdir(directory, { recursive: true, mode: 0o700 });
22
+ const legacy = (await readdir(directory)).filter((name) =>
23
+ [
24
+ "memory.sqlite",
25
+ "memory.sqlite-wal",
26
+ "memory.sqlite-shm",
27
+ "memory-log.jsonl",
28
+ "extracted-memories.log",
29
+ ].includes(name),
30
+ );
31
+ if (legacy.length > 0) {
32
+ const archive = path.join(path.dirname(directory), "memory-legacy", createUuidV7());
33
+ await mkdir(archive, { recursive: true, mode: 0o700 });
34
+ for (const name of legacy) {
35
+ await rename(path.join(directory, name), path.join(archive, name)).catch(
36
+ (error: unknown) => {
37
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
38
+ },
39
+ );
40
+ }
41
+ }
42
+ return directory;
43
+ }
44
+
45
+ export function createFileMemoryCreateToolExecutor(input: {
46
+ workspaceRoot: string;
47
+ homeRoot?: string;
48
+ clock?: () => string;
49
+ }): ToolExecutor {
50
+ return createMemoryCreateToolExecutor({
51
+ async create(text, summary, _call, signal) {
52
+ throwIfTurnCancelled(signal);
53
+ try {
54
+ const directory = path.join(
55
+ await ensureMemoryDirectory(input.homeRoot),
56
+ "notes",
57
+ );
58
+ await mkdir(directory, { recursive: true, mode: 0o700 });
59
+ const memoryId = createUuidV7();
60
+ const createdAt = (input.clock ?? (() => new Date().toISOString()))();
61
+ const filePath = path.join(directory, `${memoryId}.md`);
62
+ const title = text.replaceAll(/\s*\r?\n\s*/g, " ");
63
+ const heading = title.startsWith("# ") ? title : `# ${title}`;
64
+ const markdown = `${heading}\n\nCreated: ${createdAt}\nWorkspace: ${input.workspaceRoot}\n${summary === "" ? "" : `\n${summary}\n`}`;
65
+ throwIfTurnCancelled(signal);
66
+ await writeFile(filePath, markdown, { flag: "wx", mode: 0o600 });
67
+ return { ok: true, status: "created", memoryId, createdAt, filePath };
68
+ } catch (error) {
69
+ throwIfTurnCancelled(signal);
70
+ return { ok: false, error: boundedMemoryError(error) };
71
+ }
72
+ },
73
+ });
74
+ }
75
+
76
+ /** The browser shows a bounded preview; Read can open the complete file. */
77
+ export async function listMemoryFiles(
78
+ homeRoot?: string,
79
+ ): Promise<readonly StoredMemorySummary[]> {
80
+ const directory = await ensureMemoryDirectory(homeRoot);
81
+ const memories: StoredMemorySummary[] = [];
82
+ for (const folder of ["notes", "records"]) {
83
+ const entries = await readdir(path.join(directory, folder), {
84
+ withFileTypes: true,
85
+ }).catch((error: unknown) => {
86
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
87
+ throw error;
88
+ });
89
+ for (const entry of entries) {
90
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
91
+ const filePath = path.join(directory, folder, entry.name);
92
+ const handle = await open(filePath, "r");
93
+ let content: string;
94
+ try {
95
+ const buffer = Buffer.alloc(4_096);
96
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
97
+ content = buffer.subarray(0, bytesRead).toString("utf8");
98
+ } finally {
99
+ await handle.close();
100
+ }
101
+ const lines = content.split("\n");
102
+ const id = entry.name.slice(0, -3);
103
+ memories.push({
104
+ memoryId: id,
105
+ text: lines[0]?.replace(/^# /, "") ?? entry.name,
106
+ summary: lines
107
+ .slice(1)
108
+ .filter(
109
+ (line) => !/^(Created|Started|Workspace|Model|Max iterations): /.test(line),
110
+ )
111
+ .join("\n")
112
+ .trim(),
113
+ sourceWorkspace: /^Workspace: (.*)$/m.exec(content)?.[1] ?? "",
114
+ sourceSessionId: folder === "records" ? id : "",
115
+ createdAt: /^(?:Created|Started): (.*)$/m.exec(content)?.[1] ?? "",
116
+ });
117
+ }
118
+ }
119
+ return memories.sort(
120
+ (a, b) =>
121
+ b.createdAt.localeCompare(a.createdAt) || b.memoryId.localeCompare(a.memoryId),
122
+ );
123
+ }
124
+
125
+ /** Move an older session-local transcript when reopening or publishing a clone. */
126
+ export async function prepareSessionMemory(
127
+ sessionDirectory: string,
128
+ sessionId: string,
129
+ homeRoot?: string,
130
+ ): Promise<void> {
131
+ const directory = await ensureMemoryDirectory(homeRoot);
132
+ await mkdir(path.join(directory, "records"), { recursive: true, mode: 0o700 });
133
+ const target = sessionMemoryPath(sessionId, homeRoot);
134
+ try {
135
+ await access(target);
136
+ return;
137
+ } catch (error) {
138
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
139
+ }
140
+ await rename(path.join(sessionDirectory, "observations.md"), target).catch(
141
+ (error: unknown) => {
142
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
143
+ },
144
+ );
145
+ }
@@ -0,0 +1,23 @@
1
+ import type { MemoryTextSearchResult } from "../tools/types";
2
+
3
+ export function renderMemoryTextSearch(result: MemoryTextSearchResult): string {
4
+ const sections = result.files.map(({ filePath, lines }) => {
5
+ const path = /[\p{Cc}\p{Cf}"\\\u2028\u2029]/u.test(filePath)
6
+ ? JSON.stringify(filePath)
7
+ : filePath;
8
+ const output = [`File: ${path}`, ""];
9
+ let previous: number | undefined;
10
+ for (const line of lines) {
11
+ if (previous !== undefined && line.lineNumber > previous + 1)
12
+ output.push("", "…", "");
13
+ output.push(`${line.match ? ">" : " "} ${line.lineNumber} | ${line.text}`);
14
+ previous = line.lineNumber;
15
+ }
16
+ return output.join("\n");
17
+ });
18
+ if (sections.length === 0) sections.push("No matches found on this page.");
19
+ if (result.hasMore)
20
+ sections.push(`More results available; nextOffset=${result.nextOffset}.`);
21
+ else sections.push("End of results.");
22
+ return sections.join("\n\n");
23
+ }