tinker-agent 1.4.0 → 1.5.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.
@@ -41,6 +41,8 @@ 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";
44
46
 
45
47
  export type RunTuiOptions = {
46
48
  readonly publicConfig: ResolvedPublicConfig;
@@ -53,6 +55,13 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
53
55
  options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
54
56
  const config = options.initialRunnerConfig;
55
57
  const workspaceRoot = await realpath(config.workspaceRoot);
58
+ const memory = await initializeTuiMemory({
59
+ config:
60
+ options.publicConfig.mode === "profile" ? options.publicConfig.memory : undefined,
61
+ env: options.env,
62
+ });
63
+ const memoryCoordinator = memory.coordinator;
64
+ const memoryNotice = memory.notice;
56
65
  let controller: DefaultTuiSessionController | undefined;
57
66
  let instance: ReturnType<typeof render> | undefined;
58
67
  let disposeReason: SessionDisposeReason = { type: "tui_exit" };
@@ -93,6 +102,15 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
93
102
  presentationSinks: [sink],
94
103
  webFetchRefiner: createWebFetchRefiner(sessionConfig, options.env),
95
104
  toolingConfig: options.publicConfig.tooling,
105
+ ...(memoryCoordinator === undefined
106
+ ? {}
107
+ : {
108
+ memorySearch: memoryCoordinator.createSearchToolExecutor({
109
+ workspaceRoot,
110
+ sessionId,
111
+ }),
112
+ completedTurnHook: memoryCoordinator,
113
+ }),
96
114
  };
97
115
  if (mode === "resume") {
98
116
  return createRuntimeSession({
@@ -255,9 +273,17 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
255
273
  timeoutMs: options.publicConfig.tooling.grepTimeoutMs,
256
274
  maxBufferBytes: options.publicConfig.tooling.grepMaxBufferBytes,
257
275
  })}
276
+ writeClipboard={clipboardWriterForEnvironment(options.env)}
258
277
  onQuit={() => {
259
278
  quitRequested = true;
260
279
  }}
280
+ initialNotice={memoryNotice}
281
+ memoryDisabledNotice={memoryNotice}
282
+ listStoredMemories={
283
+ memoryCoordinator === undefined
284
+ ? undefined
285
+ : () => memoryCoordinator.listStoredMemories()
286
+ }
261
287
  />,
262
288
  );
263
289
  await instance.waitUntilExit();
@@ -267,6 +293,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
267
293
  } finally {
268
294
  instance?.unmount();
269
295
  restoreStdin();
296
+ memoryCoordinator?.dispose();
270
297
  if (controller !== undefined) {
271
298
  try {
272
299
  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.6,
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.6,
13
+ targetInputRatio: 0.3,
14
14
  } as const);
15
15
 
16
16
  export type RecallFirstRetirementPolicyV1 = typeof recallFirstRetirementPolicyV1;
@@ -204,6 +204,7 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
204
204
  case "web_search":
205
205
  case "web_fetch":
206
206
  case "recall":
207
+ case "memory_search":
207
208
  case "mcp":
208
209
  case "generic":
209
210
  return [];
@@ -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
+ }