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
|
@@ -1,189 +1,93 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type ToolDefinition,
|
|
6
|
-
type ToolExecutor,
|
|
7
|
-
} from "../tools/types";
|
|
8
|
-
import {
|
|
9
|
-
MAX_MEMORY_KEYWORD_BYTES,
|
|
10
|
-
MAX_MEMORY_KEYWORDS,
|
|
11
|
-
MAX_MEMORY_QUERY_BYTES,
|
|
12
|
-
MEMORY_SEARCH_TOOL_NAME,
|
|
13
|
-
} from "./contracts";
|
|
14
|
-
|
|
15
|
-
export const MEMORY_SEARCH_TOOL_DEFINITION: ToolDefinition = Object.freeze({
|
|
16
|
-
name: MEMORY_SEARCH_TOOL_NAME,
|
|
17
|
-
description:
|
|
18
|
-
"Search memories retained from historical conversation turns across sessions and workspaces. Use this proactively when prior user preferences, project decisions, environment facts, or verified solutions may help. Put exact-match terms in `keywords`, put a concise semantic description in `query`, and provide both for hybrid recall. Each result is a historical record containing a one-line index text, a possibly truncated detailed summary, a `memoryId`, and a `sourceSessionId`. Results may be stale or incorrect, so verify current workspace facts with current tools. To read the full stored record for a result, call `MemoryGet` with its `memoryId`.",
|
|
19
|
-
parameters: {
|
|
20
|
-
type: "object",
|
|
21
|
-
additionalProperties: false,
|
|
22
|
-
properties: {
|
|
23
|
-
query: {
|
|
24
|
-
type: "string",
|
|
25
|
-
minLength: 1,
|
|
26
|
-
maxLength: MAX_MEMORY_QUERY_BYTES,
|
|
27
|
-
description:
|
|
28
|
-
"A concise semantic description of the fact, preference, decision, or solution to recall. Drives vector similarity search.",
|
|
29
|
-
},
|
|
30
|
-
keywords: {
|
|
31
|
-
type: "array",
|
|
32
|
-
maxItems: MAX_MEMORY_KEYWORDS,
|
|
33
|
-
items: {
|
|
34
|
-
type: "string",
|
|
35
|
-
minLength: 1,
|
|
36
|
-
maxLength: MAX_MEMORY_KEYWORD_BYTES,
|
|
37
|
-
},
|
|
38
|
-
description:
|
|
39
|
-
"Exact terms to match literally, such as identifiers, error strings, paths, or project names. Drives keyword search. Keywords shorter than 3 characters cannot be matched.",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
required: [],
|
|
43
|
-
},
|
|
44
|
-
});
|
|
1
|
+
import { defineToolExecutor, type ToolExecutor } from "../tools/types";
|
|
2
|
+
import { boundedMemoryError } from "./contracts";
|
|
3
|
+
import { ensureMemoryDirectory } from "./memory-files";
|
|
4
|
+
import { searchMemoryFiles, type MemorySearchInput } from "./memory-search";
|
|
45
5
|
|
|
46
6
|
export function createMemorySearchToolExecutor(options: {
|
|
47
|
-
|
|
48
|
-
query: string | null,
|
|
49
|
-
keywords: readonly string[],
|
|
50
|
-
signal: AbortSignal,
|
|
51
|
-
) => Promise<MemorySearchRawResult>;
|
|
52
|
-
readonly recordInvalidCall: (input: {
|
|
53
|
-
readonly queryBytes: number;
|
|
54
|
-
readonly keywordCount: number;
|
|
55
|
-
}) => Promise<void>;
|
|
7
|
+
homeRoot?: string;
|
|
56
8
|
}): ToolExecutor {
|
|
57
9
|
return defineToolExecutor("memory_search", {
|
|
58
|
-
definition:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
10
|
+
definition: {
|
|
11
|
+
name: "MemorySearch",
|
|
12
|
+
description:
|
|
13
|
+
"Search historical session records and saved notes across workspaces when earlier decisions, preferences, or work may help. Matches any keyword literally, ignoring case, within individual lines. Returns passages grouped by file with line numbers; > marks matching lines and … separates passages. Use Read with offset and limit to expand a passage. Continue using nextOffset with the same keywords and context.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
keywords: {
|
|
19
|
+
type: "array",
|
|
20
|
+
minItems: 1,
|
|
21
|
+
items: { type: "string", minLength: 1 },
|
|
22
|
+
description:
|
|
23
|
+
"Literal keywords; any one can match. No regular expressions or multiline keywords. A line matching multiple keywords counts once.",
|
|
24
|
+
},
|
|
25
|
+
context: {
|
|
26
|
+
type: "integer",
|
|
27
|
+
minimum: 0,
|
|
28
|
+
description:
|
|
29
|
+
"Lines before and after each matching line. Defaults to 3. Overlapping passages are merged.",
|
|
30
|
+
},
|
|
31
|
+
limit: {
|
|
32
|
+
type: "integer",
|
|
33
|
+
minimum: 1,
|
|
34
|
+
description:
|
|
35
|
+
"Maximum selected matching lines, excluding context. Defaults to 20.",
|
|
36
|
+
},
|
|
37
|
+
offset: {
|
|
38
|
+
type: "integer",
|
|
39
|
+
minimum: 0,
|
|
40
|
+
description:
|
|
41
|
+
"Matching lines to skip. Defaults to 0. Use nextOffset to continue; file changes between searches can affect pagination.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ["keywords"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
async execute(args, _call, context) {
|
|
48
|
+
context.signal.throwIfAborted();
|
|
49
|
+
try {
|
|
50
|
+
const input = parseArgs(args);
|
|
51
|
+
const directory = await ensureMemoryDirectory(options.homeRoot);
|
|
52
|
+
return await searchMemoryFiles(directory, input, context.signal);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
context.signal.throwIfAborted();
|
|
55
|
+
return { ok: false, error: boundedMemoryError(error) };
|
|
68
56
|
}
|
|
69
|
-
const result = await options.search(
|
|
70
|
-
parsed.query,
|
|
71
|
-
parsed.keywords,
|
|
72
|
-
context.signal,
|
|
73
|
-
);
|
|
74
|
-
throwIfTurnCancelled(context.signal);
|
|
75
|
-
return result;
|
|
76
57
|
},
|
|
77
58
|
});
|
|
78
59
|
}
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
readonly keywords: readonly string[];
|
|
85
|
-
}
|
|
86
|
-
| {
|
|
87
|
-
readonly ok: false;
|
|
88
|
-
readonly queryBytes: number;
|
|
89
|
-
readonly keywordCount: number;
|
|
90
|
-
readonly error: string;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function invalidMemorySearchArgs(
|
|
94
|
-
args: Record<string, unknown>,
|
|
95
|
-
error: string,
|
|
96
|
-
queryBytes?: number,
|
|
97
|
-
): ParsedMemorySearchArgs {
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
queryBytes:
|
|
101
|
-
queryBytes ??
|
|
102
|
-
(typeof args.query === "string" ? Buffer.byteLength(args.query, "utf8") : 0),
|
|
103
|
-
keywordCount: Array.isArray(args.keywords) ? args.keywords.length : 0,
|
|
104
|
-
error,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function parseMemorySearchArgs(args: unknown): ParsedMemorySearchArgs {
|
|
109
|
-
if (!isRecord(args)) {
|
|
110
|
-
return {
|
|
111
|
-
ok: false,
|
|
112
|
-
queryBytes: 0,
|
|
113
|
-
keywordCount: 0,
|
|
114
|
-
error:
|
|
115
|
-
"MemorySearch arguments must be an object containing only query and keywords.",
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
const unexpected = Object.keys(args).find(
|
|
119
|
-
(key) => key !== "query" && key !== "keywords",
|
|
120
|
-
);
|
|
121
|
-
if (unexpected !== undefined) {
|
|
122
|
-
return invalidMemorySearchArgs(
|
|
123
|
-
args,
|
|
124
|
-
`MemorySearch received unexpected field: ${unexpected}.`,
|
|
61
|
+
function parseArgs(args: unknown): MemorySearchInput {
|
|
62
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
63
|
+
throw new Error(
|
|
64
|
+
"MemorySearch requires keywords, with optional context, limit and offset.",
|
|
125
65
|
);
|
|
66
|
+
const value = args as Record<string, unknown>;
|
|
67
|
+
for (const name of Object.keys(value)) {
|
|
68
|
+
if (!["keywords", "context", "limit", "offset"].includes(name))
|
|
69
|
+
throw new Error(`MemorySearch received unexpected field: ${name}.`);
|
|
126
70
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (typeof
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
71
|
+
if (!Array.isArray(value.keywords) || value.keywords.length === 0)
|
|
72
|
+
throw new Error("MemorySearch.keywords must be a non-empty array of strings.");
|
|
73
|
+
const keywords = value.keywords.map((word: unknown) => {
|
|
74
|
+
if (typeof word !== "string" || word.trim() === "" || /[\r\n]/.test(word))
|
|
75
|
+
throw new Error("MemorySearch keywords must be non-empty single-line strings.");
|
|
76
|
+
return word.trim();
|
|
77
|
+
});
|
|
78
|
+
const integers = { context: 3, limit: 20, offset: 0 };
|
|
79
|
+
for (const name of ["context", "limit", "offset"] as const) {
|
|
80
|
+
const number = value[name];
|
|
81
|
+
if (number === undefined) continue;
|
|
82
|
+
if (
|
|
83
|
+
typeof number !== "number" ||
|
|
84
|
+
!Number.isSafeInteger(number) ||
|
|
85
|
+
number < (name === "limit" ? 1 : 0)
|
|
86
|
+
)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`MemorySearch.${name} must be a safe integer >= ${name === "limit" ? 1 : 0}.`,
|
|
140
89
|
);
|
|
141
|
-
|
|
142
|
-
query = trimmed;
|
|
90
|
+
integers[name] = number;
|
|
143
91
|
}
|
|
144
|
-
|
|
145
|
-
const keywords: string[] = [];
|
|
146
|
-
if (args.keywords !== undefined) {
|
|
147
|
-
if (!Array.isArray(args.keywords)) {
|
|
148
|
-
return invalidMemorySearchArgs(
|
|
149
|
-
args,
|
|
150
|
-
"MemorySearch.keywords must be an array of strings.",
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (args.keywords.length > MAX_MEMORY_KEYWORDS) {
|
|
154
|
-
return invalidMemorySearchArgs(
|
|
155
|
-
args,
|
|
156
|
-
`MemorySearch.keywords may contain at most ${MAX_MEMORY_KEYWORDS} entries.`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
for (const entry of args.keywords) {
|
|
160
|
-
if (typeof entry !== "string") {
|
|
161
|
-
return invalidMemorySearchArgs(
|
|
162
|
-
args,
|
|
163
|
-
"Every MemorySearch keyword must be a string.",
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const trimmed = entry.trim();
|
|
167
|
-
const bytes = Buffer.byteLength(trimmed, "utf8");
|
|
168
|
-
if (bytes < 1 || bytes > MAX_MEMORY_KEYWORD_BYTES) {
|
|
169
|
-
return invalidMemorySearchArgs(
|
|
170
|
-
args,
|
|
171
|
-
`Every MemorySearch keyword must be 1 to ${MAX_MEMORY_KEYWORD_BYTES} UTF-8 bytes after trimming.`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
keywords.push(trimmed);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (query === null && keywords.length === 0) {
|
|
179
|
-
return invalidMemorySearchArgs(
|
|
180
|
-
args,
|
|
181
|
-
"MemorySearch requires a non-empty query or at least one keyword.",
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
return { ok: true, query, keywords: Object.freeze(keywords) };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
188
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
|
+
return { keywords, ...integers };
|
|
189
93
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import type { MemoryTextSearchResult, MemorySearchLine } from "../tools/types";
|
|
6
|
+
|
|
7
|
+
export type MemorySearchInput = {
|
|
8
|
+
keywords: string[];
|
|
9
|
+
context: number;
|
|
10
|
+
limit: number;
|
|
11
|
+
offset: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function searchMemoryFiles(
|
|
15
|
+
directory: string,
|
|
16
|
+
input: MemorySearchInput,
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
): Promise<MemoryTextSearchResult> {
|
|
19
|
+
const paths = await markdownFiles(directory, signal);
|
|
20
|
+
const keywords = input.keywords.map((word) => word.toLowerCase());
|
|
21
|
+
const files: MemoryTextSearchResult["files"][number][] = [];
|
|
22
|
+
let seen = 0;
|
|
23
|
+
let returnedResults = 0;
|
|
24
|
+
let hasMore = false;
|
|
25
|
+
for (const filePath of paths) {
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
const stream = createReadStream(filePath, { encoding: "utf8", signal });
|
|
28
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
29
|
+
const before: MemorySearchLine[] = [];
|
|
30
|
+
const selected = new Map<number, MemorySearchLine>();
|
|
31
|
+
let lineNumber = 0;
|
|
32
|
+
let through = 0;
|
|
33
|
+
try {
|
|
34
|
+
for await (const text of reader) {
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
lineNumber++;
|
|
37
|
+
const folded = text.toLowerCase();
|
|
38
|
+
let matchAt = -1;
|
|
39
|
+
for (const keyword of keywords) {
|
|
40
|
+
const position = folded.indexOf(keyword);
|
|
41
|
+
if (position >= 0 && (matchAt < 0 || position < matchAt)) matchAt = position;
|
|
42
|
+
}
|
|
43
|
+
const match = matchAt >= 0;
|
|
44
|
+
const line = { lineNumber, match, text: excerpt(text, matchAt) };
|
|
45
|
+
if (match) {
|
|
46
|
+
if (seen >= input.offset && returnedResults < input.limit) {
|
|
47
|
+
returnedResults++;
|
|
48
|
+
for (const previous of before) selected.set(previous.lineNumber, previous);
|
|
49
|
+
through = lineNumber + input.context;
|
|
50
|
+
} else if (returnedResults === input.limit) {
|
|
51
|
+
hasMore = true;
|
|
52
|
+
}
|
|
53
|
+
seen++;
|
|
54
|
+
}
|
|
55
|
+
if (lineNumber <= through) selected.set(lineNumber, line);
|
|
56
|
+
if (hasMore && lineNumber >= through) break;
|
|
57
|
+
if (input.context > 0) {
|
|
58
|
+
before.push(line);
|
|
59
|
+
if (before.length > input.context) before.shift();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
reader.close();
|
|
64
|
+
stream.destroy();
|
|
65
|
+
}
|
|
66
|
+
if (selected.size > 0)
|
|
67
|
+
files.push({
|
|
68
|
+
filePath,
|
|
69
|
+
lines: [...selected.values()].sort((a, b) => a.lineNumber - b.lineNumber),
|
|
70
|
+
});
|
|
71
|
+
if (hasMore) break;
|
|
72
|
+
}
|
|
73
|
+
signal.throwIfAborted();
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
format: "text",
|
|
77
|
+
files,
|
|
78
|
+
returnedResults,
|
|
79
|
+
hasMore,
|
|
80
|
+
...(hasMore ? { nextOffset: input.offset + returnedResults } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function markdownFiles(
|
|
85
|
+
directory: string,
|
|
86
|
+
signal: AbortSignal,
|
|
87
|
+
): Promise<string[]> {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
const files: string[] = [];
|
|
90
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
91
|
+
signal.throwIfAborted();
|
|
92
|
+
const filePath = path.join(directory, entry.name);
|
|
93
|
+
if (entry.isDirectory()) files.push(...(await markdownFiles(filePath, signal)));
|
|
94
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
95
|
+
files.push(filePath);
|
|
96
|
+
}
|
|
97
|
+
return files.sort();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Keep up to 500 original code points around the earliest literal match. */
|
|
101
|
+
function excerpt(text: string, foldedMatchOffset: number): string {
|
|
102
|
+
const points = [...text];
|
|
103
|
+
if (points.length <= 500) return text;
|
|
104
|
+
let matchPoint = 0;
|
|
105
|
+
let foldedOffset = 0;
|
|
106
|
+
if (foldedMatchOffset >= 0) {
|
|
107
|
+
while (matchPoint < points.length && foldedOffset < foldedMatchOffset) {
|
|
108
|
+
foldedOffset += points[matchPoint].toLowerCase().length;
|
|
109
|
+
matchPoint++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const start = Math.max(0, matchPoint - 100);
|
|
113
|
+
const end = Math.min(points.length, start + 500);
|
|
114
|
+
return `${start > 0 ? `[... ${start} code points omitted ...]` : ""}${points.slice(start, end).join("")}${end < points.length ? `[... ${points.length - end} code points omitted ...]` : ""}`;
|
|
115
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderMemoryTextSearch } from "../memory/memory-search-output";
|
|
1
2
|
import type { ToolCall } from "../agent/types";
|
|
2
3
|
import type { ToolResultContent } from "../agent/types";
|
|
3
4
|
import {
|
|
@@ -410,6 +411,7 @@ function renderMemorySearchObservation(raw: MemorySearchRawResult): string {
|
|
|
410
411
|
if (!raw.ok) {
|
|
411
412
|
return `MemorySearch unavailable: ${raw.error}`;
|
|
412
413
|
}
|
|
414
|
+
if ("format" in raw) return renderMemoryTextSearch(raw);
|
|
413
415
|
const degradedNote =
|
|
414
416
|
raw.degraded === "vector"
|
|
415
417
|
? " vector search unavailable; keyword results only."
|
|
@@ -467,7 +469,9 @@ function renderMemoryCreateObservation(
|
|
|
467
469
|
}
|
|
468
470
|
const text = memoryMutationText(call);
|
|
469
471
|
const result = `MemoryCreate ${raw.status} memory=${raw.memoryId} created_at=${raw.createdAt}.`;
|
|
470
|
-
|
|
472
|
+
const location =
|
|
473
|
+
raw.filePath === undefined ? result : `${result}\nPath: ${raw.filePath}`;
|
|
474
|
+
return text === undefined ? location : `${location}\ntext: ${text}`;
|
|
471
475
|
}
|
|
472
476
|
|
|
473
477
|
function renderMemoryUpdateObservation(
|
|
@@ -28,29 +28,6 @@ export type SessionMediaCompatibility = {
|
|
|
28
28
|
readonly toolResultModalities: readonly ("text" | "image")[];
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export type CompletedTurnMessageSnapshot =
|
|
32
|
-
| {
|
|
33
|
-
readonly ordinal: number;
|
|
34
|
-
readonly role: "user";
|
|
35
|
-
readonly content: string;
|
|
36
|
-
}
|
|
37
|
-
| {
|
|
38
|
-
readonly ordinal: number;
|
|
39
|
-
readonly role: "assistant";
|
|
40
|
-
readonly content: string | null;
|
|
41
|
-
readonly reasoningContent?: string | null;
|
|
42
|
-
}
|
|
43
|
-
| {
|
|
44
|
-
readonly ordinal: number;
|
|
45
|
-
readonly role: "tool";
|
|
46
|
-
readonly name: string;
|
|
47
|
-
readonly content: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type CompletedTurnSnapshot = {
|
|
51
|
-
readonly messages: readonly CompletedTurnMessageSnapshot[];
|
|
52
|
-
};
|
|
53
|
-
|
|
54
31
|
export type SessionCompatibilityContract = {
|
|
55
32
|
modelName: string;
|
|
56
33
|
profileName?: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareSessionMemory } from "../memory/memory-files";
|
|
1
2
|
import { Database } from "bun:sqlite";
|
|
2
3
|
import { ScopedQueryDatabase } from "./scoped-query-database";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -88,8 +89,6 @@ import {
|
|
|
88
89
|
type CommitSurfaceRefreshOptions,
|
|
89
90
|
type CommitSwapRevisionInput,
|
|
90
91
|
type CommitSwapRevisionOptions,
|
|
91
|
-
type CompletedTurnMessageSnapshot,
|
|
92
|
-
type CompletedTurnSnapshot,
|
|
93
92
|
type CreateNewSessionStoreInput,
|
|
94
93
|
type OpenSessionStoreInput,
|
|
95
94
|
type SessionCloseReason,
|
|
@@ -132,9 +131,7 @@ import { requireItem, requireSingleChange, runTransaction } from "./session-stor
|
|
|
132
131
|
import { SessionStoreValidation } from "./session-store-validation";
|
|
133
132
|
import {
|
|
134
133
|
assertMeasuredContextAnchor,
|
|
135
|
-
enumFromSql,
|
|
136
134
|
nullableStringFromSql,
|
|
137
|
-
nullableTextFromSql,
|
|
138
135
|
numberFromSql,
|
|
139
136
|
recordFromSql,
|
|
140
137
|
stringFromSql,
|
|
@@ -886,105 +883,6 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
886
883
|
});
|
|
887
884
|
}
|
|
888
885
|
|
|
889
|
-
readCompletedTurnSnapshot(turnId: TurnId): CompletedTurnSnapshot {
|
|
890
|
-
this.requireOpen();
|
|
891
|
-
const turnRow = this.database
|
|
892
|
-
.query("SELECT status FROM turns WHERE turn_id = ?")
|
|
893
|
-
.get(turnId);
|
|
894
|
-
const status = enumFromSql(
|
|
895
|
-
recordFromSql(turnRow, "completed turn").status,
|
|
896
|
-
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
897
|
-
"turn status",
|
|
898
|
-
);
|
|
899
|
-
if (status !== "completed") {
|
|
900
|
-
throw new Error(`Turn ${turnId} is not completed.`);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const rows = this.database
|
|
904
|
-
.query(
|
|
905
|
-
`SELECT ordinal, role, content, reasoning_content,
|
|
906
|
-
reasoning_content_present, name
|
|
907
|
-
FROM messages
|
|
908
|
-
WHERE turn_id = ?
|
|
909
|
-
ORDER BY ordinal`,
|
|
910
|
-
)
|
|
911
|
-
.all(turnId);
|
|
912
|
-
if (rows.length === 0) {
|
|
913
|
-
throw new Error(`Completed turn ${turnId} has no messages.`);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
let previousOrdinal = 0;
|
|
917
|
-
const messages = rows.map((value): CompletedTurnMessageSnapshot => {
|
|
918
|
-
const row = recordFromSql(value, "completed turn message");
|
|
919
|
-
const ordinal = numberFromSql(row.ordinal, "completed turn ordinal");
|
|
920
|
-
if (ordinal < 1 || ordinal <= previousOrdinal) {
|
|
921
|
-
throw new Error("Completed turn message ordinals are invalid.");
|
|
922
|
-
}
|
|
923
|
-
previousOrdinal = ordinal;
|
|
924
|
-
const role = enumFromSql(
|
|
925
|
-
row.role,
|
|
926
|
-
["user", "assistant", "tool"] as const,
|
|
927
|
-
"completed turn message role",
|
|
928
|
-
);
|
|
929
|
-
if (role === "user") {
|
|
930
|
-
if (
|
|
931
|
-
row.reasoning_content !== null ||
|
|
932
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !==
|
|
933
|
-
0 ||
|
|
934
|
-
row.name !== null
|
|
935
|
-
) {
|
|
936
|
-
throw new Error("Completed user message fields are invalid.");
|
|
937
|
-
}
|
|
938
|
-
return Object.freeze({
|
|
939
|
-
ordinal,
|
|
940
|
-
role,
|
|
941
|
-
content: stringFromSql(row.content, "completed user content"),
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
if (role === "assistant") {
|
|
945
|
-
if (row.name !== null) {
|
|
946
|
-
throw new Error("Completed assistant message name must be null.");
|
|
947
|
-
}
|
|
948
|
-
const reasoningPresent = numberFromSql(
|
|
949
|
-
row.reasoning_content_present,
|
|
950
|
-
"reasoning_content_present",
|
|
951
|
-
);
|
|
952
|
-
if (reasoningPresent !== 0 && reasoningPresent !== 1) {
|
|
953
|
-
throw new Error("reasoning_content_present must be 0 or 1.");
|
|
954
|
-
}
|
|
955
|
-
if (reasoningPresent === 0 && row.reasoning_content !== null) {
|
|
956
|
-
throw new Error("Absent assistant reasoning content must be null.");
|
|
957
|
-
}
|
|
958
|
-
return Object.freeze({
|
|
959
|
-
ordinal,
|
|
960
|
-
role,
|
|
961
|
-
content: nullableTextFromSql(row.content, "completed assistant content"),
|
|
962
|
-
...(reasoningPresent === 0
|
|
963
|
-
? {}
|
|
964
|
-
: {
|
|
965
|
-
reasoningContent: nullableTextFromSql(
|
|
966
|
-
row.reasoning_content,
|
|
967
|
-
"completed assistant reasoning content",
|
|
968
|
-
),
|
|
969
|
-
}),
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
if (
|
|
973
|
-
row.reasoning_content !== null ||
|
|
974
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !== 0
|
|
975
|
-
) {
|
|
976
|
-
throw new Error("Completed tool message reasoning fields are invalid.");
|
|
977
|
-
}
|
|
978
|
-
return Object.freeze({
|
|
979
|
-
ordinal,
|
|
980
|
-
role,
|
|
981
|
-
name: stringFromSql(row.name, "completed tool name"),
|
|
982
|
-
content: stringFromSql(row.content, "completed tool content"),
|
|
983
|
-
});
|
|
984
|
-
});
|
|
985
|
-
return Object.freeze({ messages: Object.freeze(messages) });
|
|
986
|
-
}
|
|
987
|
-
|
|
988
886
|
loadProtocolView(): ProtocolContextView {
|
|
989
887
|
this.requireOpen();
|
|
990
888
|
const imageAttachments = loadMessageImageAttachments(this.database);
|
|
@@ -1404,6 +1302,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1404
1302
|
input.faultInjector?.("before_publish_rename");
|
|
1405
1303
|
await rename(stagingDirectory, targetDirectory);
|
|
1406
1304
|
published = true;
|
|
1305
|
+
await prepareSessionMemory(targetDirectory, input.targetSessionId, this.homeRoot);
|
|
1407
1306
|
} finally {
|
|
1408
1307
|
if (stagingDatabase !== undefined) {
|
|
1409
1308
|
try {
|
package/src/tools/bash.ts
CHANGED
|
@@ -48,7 +48,15 @@ export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
|
|
|
48
48
|
definition: {
|
|
49
49
|
name: "Bash",
|
|
50
50
|
description:
|
|
51
|
-
"Run a shell command locally.
|
|
51
|
+
"Run a shell command locally. " +
|
|
52
|
+
"If the foreground timeout expires while the command is still running, " +
|
|
53
|
+
"it continues as a background task and returns a task ID; it is not killed. " +
|
|
54
|
+
"Use TaskOutput to inspect progress, then decide whether to keep waiting or stop it with TaskStop. " +
|
|
55
|
+
"Use run_in_background=true for persistent processes such as dev servers and watch commands, " +
|
|
56
|
+
"or when you have independent work to do while a command runs. " +
|
|
57
|
+
"For finite commands whose result is needed next, such as builds, tests, and checks, " +
|
|
58
|
+
"prefer foreground execution when no independent work remains. " +
|
|
59
|
+
"Set a sufficient foreground timeout; the call returns as soon as the command finishes.",
|
|
52
60
|
parameters: {
|
|
53
61
|
type: "object",
|
|
54
62
|
additionalProperties: false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
|
|
3
|
-
export const MAX_PREVIEW_LINES =
|
|
4
|
-
export const PREVIEW_EDGE_LINES =
|
|
3
|
+
export const MAX_PREVIEW_LINES = 50;
|
|
4
|
+
export const PREVIEW_EDGE_LINES = 25;
|
|
5
5
|
export const MAX_PREVIEW_BYTES = 32 * 1024;
|
|
6
6
|
export const MAX_PREVIEW_LINE_BYTES = 8 * 1024;
|
|
7
7
|
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createMemorySearchToolExecutor } from "../memory/memory-search-tool";
|
|
2
|
+
import { createFileMemoryCreateToolExecutor } from "../memory/memory-files";
|
|
1
3
|
import { createAskUserToolExecutor } from "./ask-user";
|
|
2
4
|
import { createBashToolExecutor } from "./bash";
|
|
3
5
|
import { ShellTaskManager } from "./bash-task";
|
|
@@ -182,10 +184,7 @@ export function createDefaultTooling(options: {
|
|
|
182
184
|
skillCoordinator?: SkillActivationCoordinator;
|
|
183
185
|
toolingConfig?: PublicToolingConfig;
|
|
184
186
|
memorySearch?: ToolExecutor;
|
|
185
|
-
memoryGet?: ToolExecutor;
|
|
186
187
|
memoryCreate?: ToolExecutor;
|
|
187
|
-
memoryUpdate?: ToolExecutor;
|
|
188
|
-
memoryDelete?: ToolExecutor;
|
|
189
188
|
enableTurnUndo?: boolean;
|
|
190
189
|
imageAssetStore?: ImageAssetStore;
|
|
191
190
|
supportsViewImage?: boolean;
|
|
@@ -259,21 +258,19 @@ export function createDefaultTooling(options: {
|
|
|
259
258
|
registry.register(createContextStatusToolExecutor());
|
|
260
259
|
registry.register(createContextSwapCandidatesToolExecutor());
|
|
261
260
|
registry.register(createContextSwapToolExecutor());
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
registry.register(options.memoryDelete);
|
|
276
|
-
}
|
|
261
|
+
registry.register(
|
|
262
|
+
options.memorySearch ??
|
|
263
|
+
createMemorySearchToolExecutor({
|
|
264
|
+
homeRoot: options.homeRoot,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
registry.register(
|
|
268
|
+
options.memoryCreate ??
|
|
269
|
+
createFileMemoryCreateToolExecutor({
|
|
270
|
+
workspaceRoot: options.workspaceRoot,
|
|
271
|
+
homeRoot: options.homeRoot,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
277
274
|
if (options.skillCatalog !== undefined) {
|
|
278
275
|
if (options.skillCatalog.skills.size === 0) {
|
|
279
276
|
throw new Error("An empty Agent Skill catalog must not register tooling.");
|
package/src/tools/types.ts
CHANGED
|
@@ -427,7 +427,18 @@ export type ContextMaintenanceHandle = {
|
|
|
427
427
|
): Promise<ContextSwapRawResult>;
|
|
428
428
|
};
|
|
429
429
|
|
|
430
|
+
export type MemorySearchLine = { lineNumber: number; match: boolean; text: string };
|
|
431
|
+
export type MemoryTextSearchResult = {
|
|
432
|
+
ok: true;
|
|
433
|
+
format: "text";
|
|
434
|
+
files: readonly { filePath: string; lines: readonly MemorySearchLine[] }[];
|
|
435
|
+
returnedResults: number;
|
|
436
|
+
hasMore: boolean;
|
|
437
|
+
nextOffset?: number;
|
|
438
|
+
};
|
|
439
|
+
|
|
430
440
|
export type MemorySearchRawResult =
|
|
441
|
+
| MemoryTextSearchResult
|
|
431
442
|
| {
|
|
432
443
|
ok: true;
|
|
433
444
|
degraded: "vector" | "fts" | null;
|
|
@@ -469,6 +480,7 @@ export type MemoryCreateRawResult =
|
|
|
469
480
|
| {
|
|
470
481
|
ok: true;
|
|
471
482
|
status: "created" | "already_exists";
|
|
483
|
+
filePath?: string;
|
|
472
484
|
memoryId: string;
|
|
473
485
|
createdAt: string;
|
|
474
486
|
}
|