tinker-agent 2.10.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +44 -67
  3. package/package.json +4 -3
  4. package/src/agent/loop.ts +50 -13
  5. package/src/agent/runtime-provider-retry.ts +115 -0
  6. package/src/agent/runtime-session-contracts.ts +13 -29
  7. package/src/agent/runtime-session.ts +42 -67
  8. package/src/cli/config.ts +0 -29
  9. package/src/cli/model-profiles.ts +0 -80
  10. package/src/cli/public-config-contract.ts +1 -82
  11. package/src/cli/tui-runner.tsx +3 -41
  12. package/src/events/observation-text-log.ts +7 -0
  13. package/src/events/types.ts +8 -0
  14. package/src/image/abortable-file-open.ts +54 -0
  15. package/src/image/image-asset-store.ts +7 -1
  16. package/src/memory/contracts.ts +0 -256
  17. package/src/memory/memory-create-tool.ts +3 -5
  18. package/src/memory/memory-files.ts +145 -0
  19. package/src/memory/memory-search-output.ts +23 -0
  20. package/src/memory/memory-search-tool.ts +79 -175
  21. package/src/memory/memory-search.ts +115 -0
  22. package/src/model/fake-model-client.ts +20 -1
  23. package/src/model/openai-model-utils.ts +45 -1
  24. package/src/model/openai-responses-mapping.ts +11 -0
  25. package/src/model/openai-responses-stream.ts +14 -0
  26. package/src/observation/observation-builder.ts +5 -1
  27. package/src/session/scoped-query-database.ts +27 -0
  28. package/src/session/session-history-access.ts +4 -3
  29. package/src/session/session-store-contracts.ts +0 -23
  30. package/src/session/session-store.ts +9 -106
  31. package/src/tools/grep.ts +12 -4
  32. package/src/tools/registry.ts +15 -18
  33. package/src/tools/types.ts +12 -0
  34. package/src/tui/app.tsx +51 -7
  35. package/src/tui/components/ask-user.tsx +15 -8
  36. package/src/tui/components/prompt-input.tsx +14 -6
  37. package/src/tui/components/timeline.tsx +17 -9
  38. package/src/tui/event-store.ts +13 -1
  39. package/src/tui/file-mention.ts +29 -5
  40. package/src/tui/tui-projection-store.ts +5 -2
  41. package/src/tui/tui-session-controller.ts +8 -0
  42. package/src/tui/workspace-file-search.ts +21 -0
  43. package/src/cli/tui-memory.ts +0 -69
  44. package/src/memory/embedding-client.ts +0 -105
  45. package/src/memory/memory-coordinator.ts +0 -1274
  46. package/src/memory/memory-delete-tool.ts +0 -88
  47. package/src/memory/memory-extractor.ts +0 -252
  48. package/src/memory/memory-get-tool.ts +0 -86
  49. package/src/memory/memory-log.ts +0 -88
  50. package/src/memory/memory-store.ts +0 -1133
  51. package/src/memory/memory-update-tool.ts +0 -142
  52. package/src/memory/vector.ts +0 -153
@@ -12,6 +12,7 @@ export type FileMentionMatch = {
12
12
  path: string;
13
13
  indices: readonly number[];
14
14
  score: number;
15
+ kind: "file" | "directory";
15
16
  };
16
17
 
17
18
  export function findFileMention(editor: LineEditorState): FileMention | undefined {
@@ -76,8 +77,13 @@ export function rankWorkspaceFiles(
76
77
  ): FileMentionMatch[] {
77
78
  if (query === "") {
78
79
  return files
79
- .map((filePath) => ({ path: filePath, indices: [], score: 0 }))
80
- .sort((left, right) => compareShallowPaths(left.path, right.path))
80
+ .map((filePath) => ({
81
+ path: filePath,
82
+ indices: [],
83
+ score: 0,
84
+ kind: fileMentionKind(filePath),
85
+ }))
86
+ .sort((left, right) => compareShallowPaths(left, right))
81
87
  .slice(0, limit);
82
88
  }
83
89
 
@@ -104,6 +110,7 @@ function fuzzyMatchPath(filePath: string, query: string): FileMentionMatch | und
104
110
  path: filePath,
105
111
  indices: basenameMatch.indices,
106
112
  score: basenameMatch.score + 200,
113
+ kind: fileMentionKind(filePath),
107
114
  };
108
115
  }
109
116
 
@@ -115,6 +122,7 @@ function fuzzyMatchPath(filePath: string, query: string): FileMentionMatch | und
115
122
  path: filePath,
116
123
  indices: fullPathMatch.indices,
117
124
  score: fullPathMatch.score,
125
+ kind: fileMentionKind(filePath),
118
126
  };
119
127
  }
120
128
 
@@ -167,6 +175,11 @@ function compareFileMatches(left: FileMentionMatch, right: FileMentionMatch): nu
167
175
  return right.score - left.score;
168
176
  }
169
177
 
178
+ const kindDifference = left.kind === right.kind ? 0 : left.kind === "file" ? -1 : 1;
179
+ if (kindDifference !== 0) {
180
+ return kindDifference;
181
+ }
182
+
170
183
  const depthDifference = pathDepth(left.path) - pathDepth(right.path);
171
184
  if (depthDifference !== 0) {
172
185
  return depthDifference;
@@ -178,9 +191,16 @@ function compareFileMatches(left: FileMentionMatch, right: FileMentionMatch): nu
178
191
  : lengthDifference;
179
192
  }
180
193
 
181
- function compareShallowPaths(left: string, right: string): number {
182
- const depthDifference = pathDepth(left) - pathDepth(right);
183
- return depthDifference === 0 ? comparePathText(left, right) : depthDifference;
194
+ function compareShallowPaths(left: FileMentionMatch, right: FileMentionMatch): number {
195
+ const kindDifference = left.kind === right.kind ? 0 : left.kind === "file" ? -1 : 1;
196
+ if (kindDifference !== 0) {
197
+ return kindDifference;
198
+ }
199
+
200
+ const depthDifference = pathDepth(left.path) - pathDepth(right.path);
201
+ return depthDifference === 0
202
+ ? comparePathText(left.path, right.path)
203
+ : depthDifference;
184
204
  }
185
205
 
186
206
  function comparePathText(left: string, right: string): number {
@@ -196,6 +216,10 @@ function comparePathText(left: string, right: string): number {
196
216
  return left < right ? -1 : left > right ? 1 : 0;
197
217
  }
198
218
 
219
+ function fileMentionKind(filePath: string): FileMentionMatch["kind"] {
220
+ return filePath.endsWith("/") || filePath.endsWith("\\") ? "directory" : "file";
221
+ }
222
+
199
223
  function pathDepth(filePath: string): number {
200
224
  return [...filePath].filter((char) => char === "/" || char === "\\").length;
201
225
  }
@@ -205,12 +205,15 @@ export class TuiProjectionStore implements EventSink, AssistantTextDeltaSink {
205
205
  return false;
206
206
  }
207
207
  this.assistantStreamAttempt = undefined;
208
- if (event.data.retryDisposition !== "scheduled" || attempt.sectionCount === 0) {
208
+ if (attempt.sectionCount === 0) {
209
209
  return false;
210
210
  }
211
211
  this.appendCommitted({
212
212
  id: `assistant-stream-retry-${attempt.iterationId}-${attempt.attemptNumber}`,
213
- text: "assistant response interrupted · retrying",
213
+ text:
214
+ event.data.retryDisposition === "scheduled"
215
+ ? "assistant response interrupted · retrying"
216
+ : "assistant response interrupted",
214
217
  status: "info",
215
218
  });
216
219
  return true;
@@ -54,6 +54,9 @@ export type TuiSessionBinding = {
54
54
  subscribeBashGuard(listener: () => void): () => void;
55
55
  setYoloMode(enabled: boolean): void;
56
56
  resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
57
+ providerRetry?: RuntimeSession["providerRetry"];
58
+ subscribeProviderRetry?: RuntimeSession["subscribeProviderRetry"];
59
+ resolveProviderRetry?: RuntimeSession["resolveProviderRetry"];
57
60
  askUser(): AskUserSnapshot;
58
61
  subscribeAskUser(listener: () => void): () => void;
59
62
  resolveAskUser(response: AskUserResolution): Promise<void>;
@@ -267,6 +270,11 @@ export function managedTuiBinding(input: {
267
270
  setYoloMode: (enabled) => input.runtimeSession.setYoloMode(enabled),
268
271
  resolveBashConfirmation: (decision) =>
269
272
  input.runtimeSession.resolveBashConfirmation(decision),
273
+ providerRetry: () => input.runtimeSession.providerRetry(),
274
+ subscribeProviderRetry: (listener) =>
275
+ input.runtimeSession.subscribeProviderRetry(listener),
276
+ resolveProviderRetry: (requestId, decision) =>
277
+ input.runtimeSession.resolveProviderRetry(requestId, decision),
270
278
  askUser: () => input.runtimeSession.askUser(),
271
279
  subscribeAskUser: (listener) => input.runtimeSession.subscribeAskUser(listener),
272
280
  resolveAskUser: (response) => input.runtimeSession.resolveAskUser(response),
@@ -93,6 +93,27 @@ export function createWorkspaceFileLister(
93
93
 
94
94
  export const listWorkspaceFiles = createWorkspaceFileLister();
95
95
 
96
+ export function deriveWorkspaceDirectories(files: readonly string[]): string[] {
97
+ const directories = new Set<string>();
98
+
99
+ for (const filePath of files) {
100
+ for (let index = 0; index < filePath.length; index += 1) {
101
+ const char = filePath[index];
102
+ if ((char === "/" || char === "\\") && index > 0) {
103
+ directories.add(filePath.slice(0, index + 1));
104
+ }
105
+ }
106
+ }
107
+
108
+ return [...directories];
109
+ }
110
+
111
+ export function listWorkspaceFilesAndDirectories(
112
+ files: readonly string[],
113
+ ): readonly string[] {
114
+ return [...files, ...deriveWorkspaceDirectories(files)];
115
+ }
116
+
96
117
  function splitPaths(stdout: string): string[] {
97
118
  return stdout
98
119
  .split("\n")
@@ -1,69 +0,0 @@
1
- import {
2
- boundedMemoryError,
3
- memoryErrorCode,
4
- type MemoryPaths,
5
- } from "../memory/contracts";
6
- import { MemoryCoordinator } from "../memory/memory-coordinator";
7
- import { MemoryLog } from "../memory/memory-log";
8
- import { resolveMemoryPaths } from "../memory/memory-store";
9
- import type { ResolvedMemoryConfig } from "./config";
10
- import { createModelClient } from "./runner-dependencies";
11
-
12
- export type TuiMemoryInitialization = {
13
- readonly coordinator?: MemoryCoordinator;
14
- readonly notice?: string;
15
- };
16
-
17
- export async function initializeTuiMemory(input: {
18
- readonly config?: ResolvedMemoryConfig;
19
- readonly env: NodeJS.ProcessEnv;
20
- readonly paths?: MemoryPaths;
21
- readonly createCoordinator?: typeof MemoryCoordinator.create;
22
- }): Promise<TuiMemoryInitialization> {
23
- if (input.config === undefined) {
24
- return Object.freeze({});
25
- }
26
-
27
- const paths = input.paths ?? resolveMemoryPaths();
28
- const log = new MemoryLog(paths.log);
29
- try {
30
- const memoryConfig = input.config;
31
- const coordinator = await (input.createCoordinator ?? MemoryCoordinator.create)({
32
- paths,
33
- embedding: memoryConfig.embedding,
34
- extractionContextBudget: memoryConfig.contextBudget,
35
- createExtractionClient: () => {
36
- const profile = memoryConfig.profile;
37
- return createModelClient(
38
- {
39
- modelName: profile.model,
40
- api: profile.api,
41
- apiKey: profile.apiKey,
42
- apiBase: profile.apiBase,
43
- ...(profile.reasoning === undefined
44
- ? {}
45
- : { reasoning: profile.reasoning }),
46
- includeReasoningContent: profile.includeReasoningContent,
47
- stream: profile.stream,
48
- contextBudget: memoryConfig.contextBudget,
49
- inputModalities: profile.inputModalities,
50
- toolResultModalities: profile.toolResultModalities,
51
- },
52
- input.env,
53
- );
54
- },
55
- });
56
- return Object.freeze({ coordinator });
57
- } catch (error) {
58
- const reason = memoryErrorCode(error, "memory_init_failed");
59
- await log.append({
60
- at: new Date().toISOString(),
61
- kind: "init",
62
- outcome: "failed",
63
- reason,
64
- });
65
- return Object.freeze({
66
- notice: `memory disabled: ${boundedMemoryError(error)}`,
67
- });
68
- }
69
- }
@@ -1,105 +0,0 @@
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
- }