tinker-agent 1.4.0 → 1.5.1
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 +40 -1
- package/README.md +54 -0
- package/package.json +6 -1
- package/src/agent/runtime-session.ts +79 -0
- package/src/cli/config.ts +29 -0
- package/src/cli/model-profiles.ts +84 -1
- package/src/cli/public-config-contract.ts +82 -0
- package/src/cli/runner-dependencies.ts +8 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +31 -0
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +1068 -3
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/types.ts +16 -0
- package/src/tui/app.tsx +211 -93
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/assistant-markdown.tsx +27 -26
- package/src/tui/components/background-tasks.tsx +7 -2
- package/src/tui/components/file-viewer.tsx +2 -2
- package/src/tui/components/footer.tsx +5 -6
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/components/resume-session-picker.tsx +3 -1
- package/src/tui/components/timeline.tsx +9 -11
- package/src/tui/event-store.ts +22 -2
- package/src/tui/shiki-highlighter.ts +104 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/tui-projection-store.ts +33 -0
- package/src/tui/tui-session-controller.ts +14 -8
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -41,6 +41,9 @@ import { resolveSessionProfileName, type ModelProfile } from "./model-profiles";
|
|
|
41
41
|
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
42
42
|
import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
43
43
|
import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
44
|
+
import { clipboardWriterForEnvironment } from "../tui/clipboard";
|
|
45
|
+
import { initializeTuiMemory } from "./tui-memory";
|
|
46
|
+
import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
|
|
44
47
|
|
|
45
48
|
export type RunTuiOptions = {
|
|
46
49
|
readonly publicConfig: ResolvedPublicConfig;
|
|
@@ -49,10 +52,18 @@ export type RunTuiOptions = {
|
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
55
|
+
const shikiPreparation = prepareShikiHighlighter();
|
|
52
56
|
const profiles =
|
|
53
57
|
options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
|
|
54
58
|
const config = options.initialRunnerConfig;
|
|
55
59
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
60
|
+
const memory = await initializeTuiMemory({
|
|
61
|
+
config:
|
|
62
|
+
options.publicConfig.mode === "profile" ? options.publicConfig.memory : undefined,
|
|
63
|
+
env: options.env,
|
|
64
|
+
});
|
|
65
|
+
const memoryCoordinator = memory.coordinator;
|
|
66
|
+
const memoryNotice = memory.notice;
|
|
56
67
|
let controller: DefaultTuiSessionController | undefined;
|
|
57
68
|
let instance: ReturnType<typeof render> | undefined;
|
|
58
69
|
let disposeReason: SessionDisposeReason = { type: "tui_exit" };
|
|
@@ -93,6 +104,15 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
93
104
|
presentationSinks: [sink],
|
|
94
105
|
webFetchRefiner: createWebFetchRefiner(sessionConfig, options.env),
|
|
95
106
|
toolingConfig: options.publicConfig.tooling,
|
|
107
|
+
...(memoryCoordinator === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: {
|
|
110
|
+
memorySearch: memoryCoordinator.createSearchToolExecutor({
|
|
111
|
+
workspaceRoot,
|
|
112
|
+
sessionId,
|
|
113
|
+
}),
|
|
114
|
+
completedTurnHook: memoryCoordinator,
|
|
115
|
+
}),
|
|
96
116
|
};
|
|
97
117
|
if (mode === "resume") {
|
|
98
118
|
return createRuntimeSession({
|
|
@@ -238,6 +258,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
238
258
|
createFreshSession,
|
|
239
259
|
);
|
|
240
260
|
|
|
261
|
+
await shikiPreparation;
|
|
241
262
|
instance = render(
|
|
242
263
|
<App
|
|
243
264
|
sessionController={controller}
|
|
@@ -255,10 +276,19 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
255
276
|
timeoutMs: options.publicConfig.tooling.grepTimeoutMs,
|
|
256
277
|
maxBufferBytes: options.publicConfig.tooling.grepMaxBufferBytes,
|
|
257
278
|
})}
|
|
279
|
+
writeClipboard={clipboardWriterForEnvironment(options.env)}
|
|
258
280
|
onQuit={() => {
|
|
259
281
|
quitRequested = true;
|
|
260
282
|
}}
|
|
283
|
+
initialNotice={memoryNotice}
|
|
284
|
+
memoryDisabledNotice={memoryNotice}
|
|
285
|
+
listStoredMemories={
|
|
286
|
+
memoryCoordinator === undefined
|
|
287
|
+
? undefined
|
|
288
|
+
: () => memoryCoordinator.listStoredMemories()
|
|
289
|
+
}
|
|
261
290
|
/>,
|
|
291
|
+
{ incrementalRendering: true },
|
|
262
292
|
);
|
|
263
293
|
await instance.waitUntilExit();
|
|
264
294
|
} catch (error) {
|
|
@@ -267,6 +297,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
267
297
|
} finally {
|
|
268
298
|
instance?.unmount();
|
|
269
299
|
restoreStdin();
|
|
300
|
+
memoryCoordinator?.dispose();
|
|
270
301
|
if (controller !== undefined) {
|
|
271
302
|
try {
|
|
272
303
|
await controller.dispose(disposeReason);
|
|
@@ -2,7 +2,7 @@ export const swapOnlyPolicyV1 = Object.freeze({
|
|
|
2
2
|
version: "swap-only-v1",
|
|
3
3
|
minimumObservationBytes: 8 * 1_024,
|
|
4
4
|
protectedRecentTurnCount: 8,
|
|
5
|
-
targetInputRatio: 0.
|
|
5
|
+
targetInputRatio: 0.3,
|
|
6
6
|
} as const);
|
|
7
7
|
|
|
8
8
|
export type SwapOnlyPolicyV1 = typeof swapOnlyPolicyV1;
|
|
@@ -10,7 +10,7 @@ export type SwapOnlyPolicyV1 = typeof swapOnlyPolicyV1;
|
|
|
10
10
|
export const recallFirstRetirementPolicyV1 = Object.freeze({
|
|
11
11
|
version: "recall-first-retirement-v1",
|
|
12
12
|
protectedRecentTurnCount: 8,
|
|
13
|
-
targetInputRatio: 0.
|
|
13
|
+
targetInputRatio: 0.3,
|
|
14
14
|
} as const);
|
|
15
15
|
|
|
16
16
|
export type RecallFirstRetirementPolicyV1 = typeof recallFirstRetirementPolicyV1;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { SessionId, TurnId } from "../ids/runtime-id";
|
|
2
|
+
|
|
3
|
+
export const MEMORY_SEARCH_TOOL_NAME = "MemorySearch" as const;
|
|
4
|
+
export const MEMORY_SCHEMA_VERSION = 1 as const;
|
|
5
|
+
export const MAX_MEMORIES_PER_TURN = 4;
|
|
6
|
+
export const MAX_MEMORY_TEXT_BYTES = 512;
|
|
7
|
+
export const MAX_MEMORY_QUERY_BYTES = 1_024;
|
|
8
|
+
export const MEMORY_SEARCH_LIMIT = 5;
|
|
9
|
+
|
|
10
|
+
export type MemoryEmbeddingKind = "openai-compatible";
|
|
11
|
+
|
|
12
|
+
export type MemoryEmbeddingConfig = {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly kind: MemoryEmbeddingKind;
|
|
15
|
+
readonly model: string;
|
|
16
|
+
readonly apiBase: string;
|
|
17
|
+
readonly apiKey: string;
|
|
18
|
+
readonly dimensions: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MemoryEmbeddingIdentity = Pick<
|
|
22
|
+
MemoryEmbeddingConfig,
|
|
23
|
+
"name" | "kind" | "model" | "dimensions"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export type MemoryPaths = {
|
|
27
|
+
readonly directory: string;
|
|
28
|
+
readonly database: string;
|
|
29
|
+
readonly log: string;
|
|
30
|
+
readonly extractedLog: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type MemoryWriteCandidate = {
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly embedding: Float32Array;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemoryWriteBatch = {
|
|
39
|
+
readonly workspaceRoot: string;
|
|
40
|
+
readonly sessionId: SessionId;
|
|
41
|
+
readonly turnId: TurnId;
|
|
42
|
+
readonly candidates: readonly MemoryWriteCandidate[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type MemoryWriteResult = {
|
|
46
|
+
readonly written: number;
|
|
47
|
+
readonly duplicate: number;
|
|
48
|
+
readonly inserted: readonly MemoryInsertedRecord[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type MemoryInsertedRecord = {
|
|
52
|
+
readonly memoryId: string;
|
|
53
|
+
readonly text: string;
|
|
54
|
+
readonly createdAt: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type MemorySearchMatch = {
|
|
58
|
+
readonly memoryId: string;
|
|
59
|
+
readonly text: string;
|
|
60
|
+
readonly score: number;
|
|
61
|
+
readonly sourceWorkspace: string;
|
|
62
|
+
readonly createdAt: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type StoredMemorySummary = {
|
|
66
|
+
readonly memoryId: string;
|
|
67
|
+
readonly text: string;
|
|
68
|
+
readonly sourceWorkspace: string;
|
|
69
|
+
readonly createdAt: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type MemoryExtractionRejectedCounts = {
|
|
73
|
+
readonly duplicate: number;
|
|
74
|
+
readonly secret: number;
|
|
75
|
+
readonly invalid: number;
|
|
76
|
+
readonly embedding: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type MemoryExtractionDiagnostic = {
|
|
80
|
+
readonly at: string;
|
|
81
|
+
readonly kind: "extraction";
|
|
82
|
+
readonly outcome: "ok" | "failed" | "skipped";
|
|
83
|
+
readonly reason: string | null;
|
|
84
|
+
readonly workspace: string;
|
|
85
|
+
readonly turnId: string;
|
|
86
|
+
readonly inputTokens: number;
|
|
87
|
+
readonly returned: number;
|
|
88
|
+
readonly written: number;
|
|
89
|
+
readonly rejected: MemoryExtractionRejectedCounts;
|
|
90
|
+
readonly ms: number;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type MemorySearchDiagnostic = {
|
|
94
|
+
readonly at: string;
|
|
95
|
+
readonly kind: "search";
|
|
96
|
+
readonly outcome: "ok" | "failed" | "skipped";
|
|
97
|
+
readonly reason: string | null;
|
|
98
|
+
readonly workspace: string;
|
|
99
|
+
readonly sessionId: string;
|
|
100
|
+
readonly queryBytes: number;
|
|
101
|
+
readonly returned: number;
|
|
102
|
+
readonly scores: readonly number[];
|
|
103
|
+
readonly ms: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type MemoryInitDiagnostic = {
|
|
107
|
+
readonly at: string;
|
|
108
|
+
readonly kind: "init";
|
|
109
|
+
readonly outcome: "failed";
|
|
110
|
+
readonly reason: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type MemoryDiagnostic =
|
|
114
|
+
| MemoryExtractionDiagnostic
|
|
115
|
+
| MemorySearchDiagnostic
|
|
116
|
+
| MemoryInitDiagnostic;
|
|
117
|
+
|
|
118
|
+
export class MemoryError extends Error {
|
|
119
|
+
constructor(
|
|
120
|
+
readonly code: string,
|
|
121
|
+
message: string,
|
|
122
|
+
options?: ErrorOptions,
|
|
123
|
+
) {
|
|
124
|
+
super(message, options);
|
|
125
|
+
this.name = "MemoryError";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function memoryErrorCode(error: unknown, fallback: string): string {
|
|
130
|
+
return error instanceof MemoryError ? error.code : fallback;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function boundedMemoryError(error: unknown): string {
|
|
134
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
135
|
+
const singleLine = raw.replaceAll(/\s+/g, " ").trim() || "unknown memory error";
|
|
136
|
+
return truncateUtf8(singleLine, 400);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
140
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
let end = Math.min(value.length, maxBytes);
|
|
144
|
+
while (end > 0 && Buffer.byteLength(`${value.slice(0, end)}…`, "utf8") > maxBytes) {
|
|
145
|
+
end -= 1;
|
|
146
|
+
}
|
|
147
|
+
return `${value.slice(0, end)}…`;
|
|
148
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type { MemoryEmbeddingConfig } from "./contracts";
|
|
3
|
+
import { MemoryError } from "./contracts";
|
|
4
|
+
|
|
5
|
+
const EMBEDDING_TIMEOUT_MS = 60_000;
|
|
6
|
+
const EMBEDDING_MAX_RETRIES = 2;
|
|
7
|
+
|
|
8
|
+
export interface MemoryEmbeddingClient {
|
|
9
|
+
embed(
|
|
10
|
+
inputs: readonly string[],
|
|
11
|
+
signal: AbortSignal,
|
|
12
|
+
): Promise<readonly (readonly number[])[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class OpenAICompatibleEmbeddingClient implements MemoryEmbeddingClient {
|
|
16
|
+
private readonly client: OpenAI;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly config: MemoryEmbeddingConfig,
|
|
20
|
+
options: { readonly fetch?: typeof fetch } = {},
|
|
21
|
+
) {
|
|
22
|
+
this.client = new OpenAI({
|
|
23
|
+
apiKey: config.apiKey,
|
|
24
|
+
baseURL: config.apiBase,
|
|
25
|
+
timeout: EMBEDDING_TIMEOUT_MS,
|
|
26
|
+
maxRetries: EMBEDDING_MAX_RETRIES,
|
|
27
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async embed(
|
|
32
|
+
inputs: readonly string[],
|
|
33
|
+
signal: AbortSignal,
|
|
34
|
+
): Promise<readonly (readonly number[])[]> {
|
|
35
|
+
if (inputs.length === 0) {
|
|
36
|
+
throw new MemoryError(
|
|
37
|
+
"memory_embedding_input_invalid",
|
|
38
|
+
"Embedding input must not be empty.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await this.client.embeddings.create(
|
|
46
|
+
{
|
|
47
|
+
model: this.config.model,
|
|
48
|
+
input: [...inputs],
|
|
49
|
+
encoding_format: "float",
|
|
50
|
+
},
|
|
51
|
+
{ signal },
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
throw new MemoryError(
|
|
58
|
+
"memory_embedding_request_failed",
|
|
59
|
+
"Embedding provider request failed.",
|
|
60
|
+
{ cause: error },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
|
|
65
|
+
if (!Array.isArray(response.data)) {
|
|
66
|
+
throw new MemoryError(
|
|
67
|
+
"memory_embedding_response_invalid",
|
|
68
|
+
"Embedding response did not contain a data array.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (response.data.length !== inputs.length) {
|
|
72
|
+
throw new MemoryError(
|
|
73
|
+
"memory_embedding_response_invalid",
|
|
74
|
+
`Embedding response returned ${response.data.length} vectors for ${inputs.length} inputs.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const vectors: Array<readonly number[] | undefined> = Array.from({
|
|
79
|
+
length: inputs.length,
|
|
80
|
+
});
|
|
81
|
+
for (const item of response.data) {
|
|
82
|
+
if (
|
|
83
|
+
!Number.isSafeInteger(item.index) ||
|
|
84
|
+
item.index < 0 ||
|
|
85
|
+
item.index >= inputs.length ||
|
|
86
|
+
vectors[item.index] !== undefined ||
|
|
87
|
+
!Array.isArray(item.embedding) ||
|
|
88
|
+
item.embedding.some((value) => typeof value !== "number")
|
|
89
|
+
) {
|
|
90
|
+
throw new MemoryError(
|
|
91
|
+
"memory_embedding_response_invalid",
|
|
92
|
+
"Embedding response indices or vectors are invalid.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
vectors[item.index] = Object.freeze([...item.embedding]);
|
|
96
|
+
}
|
|
97
|
+
if (vectors.some((vector) => vector === undefined)) {
|
|
98
|
+
throw new MemoryError(
|
|
99
|
+
"memory_embedding_response_invalid",
|
|
100
|
+
"Embedding response did not map every input index.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return Object.freeze(vectors as readonly (readonly number[])[]);
|
|
104
|
+
}
|
|
105
|
+
}
|