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.
- package/CHANGELOG.md +47 -1
- package/README.md +44 -67
- package/package.json +4 -3
- package/src/agent/loop.ts +50 -13
- package/src/agent/runtime-provider-retry.ts +115 -0
- package/src/agent/runtime-session-contracts.ts +13 -29
- package/src/agent/runtime-session.ts +42 -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/tui-runner.tsx +3 -41
- package/src/events/observation-text-log.ts +7 -0
- package/src/events/types.ts +8 -0
- package/src/image/abortable-file-open.ts +54 -0
- package/src/image/image-asset-store.ts +7 -1
- 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/model/fake-model-client.ts +20 -1
- package/src/model/openai-model-utils.ts +45 -1
- package/src/model/openai-responses-mapping.ts +11 -0
- package/src/model/openai-responses-stream.ts +14 -0
- package/src/observation/observation-builder.ts +5 -1
- package/src/session/scoped-query-database.ts +27 -0
- package/src/session/session-history-access.ts +4 -3
- package/src/session/session-store-contracts.ts +0 -23
- package/src/session/session-store.ts +9 -106
- package/src/tools/grep.ts +12 -4
- package/src/tools/registry.ts +15 -18
- package/src/tools/types.ts +12 -0
- package/src/tui/app.tsx +51 -7
- package/src/tui/components/ask-user.tsx +15 -8
- package/src/tui/components/prompt-input.tsx +14 -6
- package/src/tui/components/timeline.tsx +17 -9
- package/src/tui/event-store.ts +13 -1
- package/src/tui/file-mention.ts +29 -5
- package/src/tui/tui-projection-store.ts +5 -2
- package/src/tui/tui-session-controller.ts +8 -0
- package/src/tui/workspace-file-search.ts +21 -0
- 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
package/src/tui/file-mention.ts
CHANGED
|
@@ -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) => ({
|
|
80
|
-
|
|
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:
|
|
182
|
-
const
|
|
183
|
-
|
|
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 (
|
|
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:
|
|
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")
|
package/src/cli/tui-memory.ts
DELETED
|
@@ -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
|
-
}
|