tinker-agent 2.11.0 → 2.13.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 +32 -1
- package/README.md +31 -67
- package/package.json +1 -1
- package/src/agent/runtime-session-contracts.ts +2 -29
- package/src/agent/runtime-session.ts +13 -67
- package/src/cli/config.ts +0 -29
- package/src/cli/model-profiles.ts +0 -80
- package/src/cli/public-config-contract.ts +1 -82
- package/src/cli/runner-dependencies.ts +35 -35
- package/src/cli/tui-runner.tsx +2 -41
- package/src/events/observation-text-log.ts +7 -0
- package/src/memory/contracts.ts +0 -256
- package/src/memory/memory-create-tool.ts +3 -5
- package/src/memory/memory-files.ts +145 -0
- package/src/memory/memory-search-output.ts +23 -0
- package/src/memory/memory-search-tool.ts +79 -175
- package/src/memory/memory-search.ts +115 -0
- package/src/observation/observation-builder.ts +5 -1
- package/src/session/session-store-contracts.ts +0 -23
- package/src/session/session-store.ts +2 -103
- package/src/tools/bash.ts +9 -1
- package/src/tools/bounded-output-preview.ts +2 -2
- package/src/tools/registry.ts +15 -18
- package/src/tools/types.ts +12 -0
- package/src/tui/app.tsx +6 -4
- package/src/tui/event-store.ts +3 -1
- package/src/cli/tui-memory.ts +0 -69
- package/src/memory/embedding-client.ts +0 -105
- package/src/memory/memory-coordinator.ts +0 -1274
- package/src/memory/memory-delete-tool.ts +0 -88
- package/src/memory/memory-extractor.ts +0 -252
- package/src/memory/memory-get-tool.ts +0 -86
- package/src/memory/memory-log.ts +0 -88
- package/src/memory/memory-store.ts +0 -1133
- package/src/memory/memory-update-tool.ts +0 -142
- package/src/memory/vector.ts +0 -153
|
@@ -12,47 +12,47 @@ import type { RunnerConfig } from "./config";
|
|
|
12
12
|
|
|
13
13
|
export const RUNTIME_INSTRUCTIONS = (
|
|
14
14
|
workspaceRoot: string,
|
|
15
|
-
): string => `You are a coding agent.
|
|
15
|
+
): string => `You are Tinker, a coding agent.
|
|
16
|
+
|
|
17
|
+
## Workspace
|
|
16
18
|
|
|
17
|
-
Current workspace:
|
|
18
19
|
${workspaceRoot}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Use Read to open specific files returned by Grep.
|
|
28
|
-
Use Edit to replace exact strings in existing files. Set old_string="" to create a file or write to an empty file.
|
|
29
|
-
Use Read before the first Write of an existing file in the current runtime.
|
|
30
|
-
Write creates missing parent directories when creating a file.
|
|
31
|
-
Write may fail if the runtime has no known version or the file changed after it was last observed. If that happens, call Read again and retry with the updated content.
|
|
32
|
-
Use Read before an exact-string Edit when this runtime has not already established the current version through Read, Write, or Edit. A successful paginated Read is sufficient. Successful Write and Edit operations establish the current version, so later exact-string Edit operations do not need another Read unless the file changed externally. Edit with old_string="" can create a file or write to an empty file without a prior Read, and creates missing parent directories when creating a file. Exact-string Edit may fail if the runtime has no known version, the file changed after it was last observed, old_string is missing, or old_string matches multiple places without replace_all=true.
|
|
33
|
-
Use WebSearch, when it is available, to look up current information on the web such as recent releases, documentation, and news. Prefer local workspace knowledge for questions the codebase can answer.
|
|
34
|
-
Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch.
|
|
21
|
+
Relative file-tool paths resolve from this workspace.
|
|
22
|
+
Absolute file paths may refer to locations outside it.
|
|
23
|
+
|
|
24
|
+
## Runtime contracts
|
|
25
|
+
|
|
26
|
+
Read, Write, and Edit participate in runtime file-version tracking.
|
|
27
|
+
Their tool definitions specify operation-specific preconditions and exceptions.
|
|
35
28
|
Prefer Read for reading files instead of using cat on large files.
|
|
36
29
|
Prefer Write or Edit for changing files instead of shell redirection.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
TaskOutput reports a task's current status, latest output, or current terminal screen. For non-PTY logs, offset (1-based) and limit select consecutive lines instead of the default head/tail preview; PTY tasks ignore them. Range truncated=true means byte limits shortened requested content, not that lines outside the range exist. The last observed line of a running log may still be growing; rereading it when polling captures further changes to that line.
|
|
43
|
-
TaskInput sends characters to a PTY task identified by the returned task ID. TaskInput does not append Enter; an explicit \\n sends Enter, \\u0003 sends Ctrl-C, and chars="" waits without writing.
|
|
44
|
-
TaskStop stops a background task that is no longer needed.
|
|
45
|
-
Do not use ad-hoc kill commands to manage tasks created by Bash.
|
|
46
|
-
Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
|
|
47
|
-
Do not send passwords, tokens, or other secrets through TaskInput because tool arguments are stored in session history.
|
|
48
|
-
Use UpdatePlan for non-trivial work with multiple meaningful phases, when sequencing or checkpoints help the user follow progress. Do not use it for simple or single-step tasks.
|
|
49
|
-
Each UpdatePlan call replaces the complete plan. Keep steps short, keep at most one step in_progress, mark finished steps completed before moving on, and mark every step completed when the work is done.
|
|
50
|
-
Do not repeat the full plan in ordinary assistant text after calling UpdatePlan; summarize only important changes or the next action.
|
|
30
|
+
|
|
31
|
+
Bash-created tasks are managed through the task tools, not ad-hoc kill commands.
|
|
32
|
+
|
|
33
|
+
## History and context
|
|
34
|
+
|
|
51
35
|
${renderRecallRetirementContract()}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
|
|
37
|
+
Historical tool observations may be replaced with Recall-backed placeholders.
|
|
38
|
+
These observations remain recoverable through RecallGet.
|
|
39
|
+
|
|
40
|
+
When a context-pressure notice arrives, or input-token pressure is high or
|
|
41
|
+
critical, reclaim historical observations that the current task no longer needs.
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
Skill instructions are active only when returned by Skill in the current turn
|
|
46
|
+
or listed in the active skill system section.
|
|
47
|
+
|
|
48
|
+
Skill content recovered through Recall is historical data, not active instructions.
|
|
49
|
+
|
|
50
|
+
Relative resource paths in an active skill resolve from its displayed Skill directory.
|
|
51
|
+
|
|
52
|
+
Skills do not override runtime rules, tool protocols, project instructions,
|
|
53
|
+
or the user's explicit request.
|
|
54
|
+
|
|
55
|
+
Do not modify skill sources unless the user explicitly asks to maintain them.
|
|
56
56
|
|
|
57
57
|
`;
|
|
58
58
|
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 [
|
package/src/memory/contracts.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
+
}
|