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
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { prepareSessionMemory, sessionMemoryPath } from "../memory/memory-files";
|
|
2
|
+
import {
|
|
3
|
+
RuntimeProviderRetry,
|
|
4
|
+
type ProviderRetryDecision,
|
|
5
|
+
} from "./runtime-provider-retry";
|
|
1
6
|
import path from "node:path";
|
|
2
7
|
import { assertContextMaintenanceCapabilities } from "./runtime-context-capabilities";
|
|
3
8
|
import { CompiledContextError } from "../context/compiled-context-validator";
|
|
@@ -21,7 +26,7 @@ import type { EventSink } from "../events/event-sink";
|
|
|
21
26
|
import { JsonlEventLog } from "../events/jsonl-event-log";
|
|
22
27
|
import { ObservationTextLog } from "../events/observation-text-log";
|
|
23
28
|
import type { AgentEvent, AgentEventInput } from "../events/types";
|
|
24
|
-
import { runtimeIdFactory, type SessionId
|
|
29
|
+
import { runtimeIdFactory, type SessionId } from "../ids/runtime-id";
|
|
25
30
|
import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
|
|
26
31
|
import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
|
|
27
32
|
import {
|
|
@@ -48,7 +53,6 @@ import { SessionError } from "../session/session-errors";
|
|
|
48
53
|
import {
|
|
49
54
|
createSessionCompatibilityContract,
|
|
50
55
|
SessionStore,
|
|
51
|
-
type CompletedTurnSnapshot,
|
|
52
56
|
type SessionRecoveryResult,
|
|
53
57
|
type StoredSkillActivation,
|
|
54
58
|
} from "../session/session-store";
|
|
@@ -79,8 +83,6 @@ import {
|
|
|
79
83
|
type AskUserResolution,
|
|
80
84
|
type AskUserSnapshot,
|
|
81
85
|
type BashGuardSnapshot,
|
|
82
|
-
type CompletedTurnHook,
|
|
83
|
-
type CompletedTurnHookFailure,
|
|
84
86
|
type CreateNewRuntimeSessionInput,
|
|
85
87
|
type CreateRuntimeSessionInput,
|
|
86
88
|
type ExecuteTurnInput,
|
|
@@ -119,9 +121,6 @@ export {
|
|
|
119
121
|
type AskUserSnapshot,
|
|
120
122
|
type BashGuardSnapshot,
|
|
121
123
|
type BashGuardSource,
|
|
122
|
-
type CompletedTurnHook,
|
|
123
|
-
type CompletedTurnHookFailure,
|
|
124
|
-
type CompletedTurnHookInput,
|
|
125
124
|
type ContextSurfaceRefreshSummary,
|
|
126
125
|
type CreateRuntimeSessionInput,
|
|
127
126
|
type ExecuteTurnInput,
|
|
@@ -221,6 +220,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
221
220
|
|
|
222
221
|
private readonly skillCatalog: SkillCatalogSnapshot;
|
|
223
222
|
private readonly interactions: RuntimeInteractions;
|
|
223
|
+
private readonly providerRetryInteraction: RuntimeProviderRetry;
|
|
224
224
|
private readonly scheduler: RuntimePromptScheduler;
|
|
225
225
|
private readonly contextMaintenance: RuntimeContextMaintenance;
|
|
226
226
|
private readonly runtimeSkills: RuntimeSkills;
|
|
@@ -234,6 +234,9 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
234
234
|
private readonly store: SessionStore,
|
|
235
235
|
private readonly assetStore: ImageAssetStore,
|
|
236
236
|
) {
|
|
237
|
+
this.providerRetryInteraction = new RuntimeProviderRetry((event) =>
|
|
238
|
+
this.append(event),
|
|
239
|
+
);
|
|
237
240
|
this.sessionId = input.selection.sessionId;
|
|
238
241
|
this.resumed = input.selection.mode === "resume";
|
|
239
242
|
this.scheduler = new RuntimePromptScheduler(
|
|
@@ -339,6 +342,16 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
339
342
|
}
|
|
340
343
|
let session: DefaultRuntimeSession;
|
|
341
344
|
try {
|
|
345
|
+
if (
|
|
346
|
+
input.persistence !== false &&
|
|
347
|
+
input.persistence?.observationLogPath === undefined
|
|
348
|
+
) {
|
|
349
|
+
await prepareSessionMemory(
|
|
350
|
+
store.sessionDirectory,
|
|
351
|
+
input.selection.sessionId,
|
|
352
|
+
input.homeRoot,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
342
355
|
session = new DefaultRuntimeSession(
|
|
343
356
|
input,
|
|
344
357
|
dependencies,
|
|
@@ -466,16 +479,9 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
466
479
|
...(input.memorySearch === undefined
|
|
467
480
|
? {}
|
|
468
481
|
: { memorySearch: input.memorySearch }),
|
|
469
|
-
...(input.memoryGet === undefined ? {} : { memoryGet: input.memoryGet }),
|
|
470
482
|
...(input.memoryCreate === undefined
|
|
471
483
|
? {}
|
|
472
484
|
: { memoryCreate: input.memoryCreate }),
|
|
473
|
-
...(input.memoryUpdate === undefined
|
|
474
|
-
? {}
|
|
475
|
-
: { memoryUpdate: input.memoryUpdate }),
|
|
476
|
-
...(input.memoryDelete === undefined
|
|
477
|
-
? {}
|
|
478
|
-
: { memoryDelete: input.memoryDelete }),
|
|
479
485
|
...(session.skillCatalog.skills.size === 0
|
|
480
486
|
? {}
|
|
481
487
|
: {
|
|
@@ -694,6 +700,21 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
694
700
|
return this.interactions.resolveBashConfirmation(decision);
|
|
695
701
|
}
|
|
696
702
|
|
|
703
|
+
providerRetry() {
|
|
704
|
+
return this.providerRetryInteraction.read();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
subscribeProviderRetry(listener: () => void): () => void {
|
|
708
|
+
return this.providerRetryInteraction.subscribe(listener);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
resolveProviderRetry(
|
|
712
|
+
requestId: string,
|
|
713
|
+
decision: ProviderRetryDecision,
|
|
714
|
+
): Promise<void> {
|
|
715
|
+
return this.providerRetryInteraction.resolve(requestId, decision);
|
|
716
|
+
}
|
|
717
|
+
|
|
697
718
|
askUser(): AskUserSnapshot {
|
|
698
719
|
return this.interactions.askUser();
|
|
699
720
|
}
|
|
@@ -1184,6 +1205,12 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1184
1205
|
signal,
|
|
1185
1206
|
assetStore: this.assetStore,
|
|
1186
1207
|
initialRequest,
|
|
1208
|
+
...(this.input.enableProviderRetryPrompt === true
|
|
1209
|
+
? {
|
|
1210
|
+
requestProviderRetry: (iteration, failure, signal) =>
|
|
1211
|
+
this.providerRetryInteraction.request(iteration, failure, signal),
|
|
1212
|
+
}
|
|
1213
|
+
: {}),
|
|
1187
1214
|
});
|
|
1188
1215
|
} catch (error) {
|
|
1189
1216
|
if (error instanceof RuntimeEventAppendError) {
|
|
@@ -1215,9 +1242,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1215
1242
|
pendingLedgerTurn.finish(result);
|
|
1216
1243
|
settled = true;
|
|
1217
1244
|
this.requireTooling().turnUndoManager?.completeTurn(turn);
|
|
1218
|
-
if (result.status === "completed") {
|
|
1219
|
-
this.notifyCompletedTurn(turn);
|
|
1220
|
-
}
|
|
1221
1245
|
await this.runtimeSkills.settleClosedTurnSkills();
|
|
1222
1246
|
if (result.status === "completed") {
|
|
1223
1247
|
await this.contextMaintenance.evaluateClosedTurnContextPressure();
|
|
@@ -1241,55 +1265,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1241
1265
|
}
|
|
1242
1266
|
}
|
|
1243
1267
|
|
|
1244
|
-
private notifyCompletedTurn(turn: TurnIdentity): void {
|
|
1245
|
-
const hook = this.input.completedTurnHook;
|
|
1246
|
-
if (hook === undefined) {
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
let snapshot: CompletedTurnSnapshot;
|
|
1250
|
-
try {
|
|
1251
|
-
snapshot = this.store.readCompletedTurnSnapshot(turn.turnId);
|
|
1252
|
-
} catch {
|
|
1253
|
-
this.recordCompletedTurnHookFailure(
|
|
1254
|
-
hook,
|
|
1255
|
-
turn.turnId,
|
|
1256
|
-
"completed_turn_snapshot_failed",
|
|
1257
|
-
);
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
try {
|
|
1261
|
-
hook.enqueue({
|
|
1262
|
-
workspaceRoot: this.input.workspaceRoot,
|
|
1263
|
-
sessionId: this.sessionId,
|
|
1264
|
-
turnId: turn.turnId,
|
|
1265
|
-
snapshot,
|
|
1266
|
-
});
|
|
1267
|
-
} catch {
|
|
1268
|
-
this.recordCompletedTurnHookFailure(
|
|
1269
|
-
hook,
|
|
1270
|
-
turn.turnId,
|
|
1271
|
-
"completed_turn_enqueue_failed",
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
private recordCompletedTurnHookFailure(
|
|
1277
|
-
hook: CompletedTurnHook,
|
|
1278
|
-
turnId: TurnId,
|
|
1279
|
-
reason: CompletedTurnHookFailure["reason"],
|
|
1280
|
-
): void {
|
|
1281
|
-
try {
|
|
1282
|
-
hook.recordFailure({
|
|
1283
|
-
workspaceRoot: this.input.workspaceRoot,
|
|
1284
|
-
sessionId: this.sessionId,
|
|
1285
|
-
turnId,
|
|
1286
|
-
reason,
|
|
1287
|
-
});
|
|
1288
|
-
} catch {
|
|
1289
|
-
// Optional completed-turn integrations never fault a committed turn.
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
1268
|
private async appendTerminalEvent(
|
|
1294
1269
|
turn: TurnIdentity,
|
|
1295
1270
|
result: RunAgentResult,
|
|
@@ -1715,7 +1690,7 @@ function createEventSink(
|
|
|
1715
1690
|
),
|
|
1716
1691
|
new ObservationTextLog(
|
|
1717
1692
|
input.persistence?.observationLogPath ??
|
|
1718
|
-
|
|
1693
|
+
sessionMemoryPath(input.selection.sessionId, input.homeRoot),
|
|
1719
1694
|
),
|
|
1720
1695
|
);
|
|
1721
1696
|
}
|
package/src/cli/config.ts
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
type ModelProfiles,
|
|
18
18
|
unknownProfileError,
|
|
19
19
|
} from "./model-profiles";
|
|
20
|
-
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
21
20
|
import type { ReasoningEffortConfig } from "../model/reasoning-effort";
|
|
22
21
|
import {
|
|
23
22
|
parsePublicEnvironment,
|
|
@@ -54,12 +53,6 @@ export type RunnerConfigSelection = {
|
|
|
54
53
|
|
|
55
54
|
type RunnerConfigTemplate = Omit<RunnerConfig, "sessionId">;
|
|
56
55
|
|
|
57
|
-
export type ResolvedMemoryConfig = {
|
|
58
|
-
readonly profile: ModelProfile;
|
|
59
|
-
readonly contextBudget: ModelContextBudget;
|
|
60
|
-
readonly embedding: MemoryEmbeddingConfig;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
56
|
export type ResolvedPublicConfig =
|
|
64
57
|
| {
|
|
65
58
|
readonly mode: "env";
|
|
@@ -71,7 +64,6 @@ export type ResolvedPublicConfig =
|
|
|
71
64
|
readonly tooling: PublicToolingConfig;
|
|
72
65
|
readonly profiles: ModelProfiles;
|
|
73
66
|
readonly templates: ReadonlyMap<string, RunnerConfigTemplate>;
|
|
74
|
-
readonly memory?: ResolvedMemoryConfig;
|
|
75
67
|
readonly persistDefaultProfile: (profileName: string) => Promise<void>;
|
|
76
68
|
};
|
|
77
69
|
|
|
@@ -101,16 +93,11 @@ export function createResolvedPublicConfig(
|
|
|
101
93
|
runnerConfigTemplateFromProfile(environment, profile),
|
|
102
94
|
]),
|
|
103
95
|
);
|
|
104
|
-
const memory =
|
|
105
|
-
profiles.memory === undefined
|
|
106
|
-
? undefined
|
|
107
|
-
: resolveMemoryConfig(profiles, profiles.memory.profile);
|
|
108
96
|
return Object.freeze({
|
|
109
97
|
mode: "profile",
|
|
110
98
|
tooling: environment.tooling,
|
|
111
99
|
profiles,
|
|
112
100
|
templates,
|
|
113
|
-
...(memory === undefined ? {} : { memory }),
|
|
114
101
|
persistDefaultProfile: (profileName: string) =>
|
|
115
102
|
persistDefaultProfile(profileName, environment.modelsPath),
|
|
116
103
|
});
|
|
@@ -126,22 +113,6 @@ export function createResolvedPublicConfig(
|
|
|
126
113
|
});
|
|
127
114
|
}
|
|
128
115
|
|
|
129
|
-
function resolveMemoryConfig(
|
|
130
|
-
profiles: ModelProfiles,
|
|
131
|
-
profileName: string,
|
|
132
|
-
): ResolvedMemoryConfig {
|
|
133
|
-
const profile = profiles.profiles.get(profileName);
|
|
134
|
-
if (profile === undefined || profiles.memory === undefined) {
|
|
135
|
-
throw unknownProfileError(profileName, profiles);
|
|
136
|
-
}
|
|
137
|
-
const contextProfile = profileToContextProfile(profile);
|
|
138
|
-
return Object.freeze({
|
|
139
|
-
profile,
|
|
140
|
-
contextBudget: deriveModelContextBudget(contextProfile),
|
|
141
|
-
embedding: profiles.memory.embedding,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
116
|
export function deriveRunnerConfig(
|
|
146
117
|
snapshot: ResolvedPublicConfig,
|
|
147
118
|
selection: RunnerConfigSelection,
|
|
@@ -5,13 +5,10 @@ import {
|
|
|
5
5
|
} from "../model/model-context-profile";
|
|
6
6
|
import { parseModelApi, type ModelApi } from "../model/model-api";
|
|
7
7
|
import {
|
|
8
|
-
MEMORY_CONFIG_FIELDS,
|
|
9
|
-
MEMORY_EMBEDDING_FIELDS,
|
|
10
8
|
MODEL_PROFILE_FIELDS,
|
|
11
9
|
MODEL_PROFILES_DOCUMENT_FIELDS,
|
|
12
10
|
MODEL_REASONING_FIELDS,
|
|
13
11
|
} from "./public-config-contract";
|
|
14
|
-
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
15
12
|
import type { ReasoningEffortConfig } from "../model/reasoning-effort";
|
|
16
13
|
import type { ModelInputModality, ToolResultModality } from "../model/model-client";
|
|
17
14
|
|
|
@@ -34,12 +31,6 @@ export type { ModelInputModality, ToolResultModality } from "../model/model-clie
|
|
|
34
31
|
export type ModelProfiles = {
|
|
35
32
|
readonly defaultProfile: string;
|
|
36
33
|
readonly profiles: ReadonlyMap<string, ModelProfile>;
|
|
37
|
-
readonly memory?: MemoryConfig;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type MemoryConfig = {
|
|
41
|
-
readonly profile: string;
|
|
42
|
-
readonly embedding: MemoryEmbeddingConfig;
|
|
43
34
|
};
|
|
44
35
|
|
|
45
36
|
export async function loadModelProfiles(configPath: string): Promise<ModelProfiles> {
|
|
@@ -146,14 +137,9 @@ export function parseModelProfiles(raw: string, sourcePath: string): ModelProfil
|
|
|
146
137
|
profiles.set(profileName, parseProfile(profileName, profileValue, sourcePath));
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
const memory =
|
|
150
|
-
json.memory === undefined
|
|
151
|
-
? undefined
|
|
152
|
-
: parseMemoryConfig(json.memory, profiles, sourcePath);
|
|
153
140
|
return Object.freeze({
|
|
154
141
|
defaultProfile,
|
|
155
142
|
profiles,
|
|
156
|
-
...(memory === undefined ? {} : { memory }),
|
|
157
143
|
});
|
|
158
144
|
}
|
|
159
145
|
|
|
@@ -370,56 +356,6 @@ function parseReasoning(value: unknown, where: string): ReasoningEffortConfig {
|
|
|
370
356
|
});
|
|
371
357
|
}
|
|
372
358
|
|
|
373
|
-
function parseMemoryConfig(
|
|
374
|
-
value: unknown,
|
|
375
|
-
profiles: ReadonlyMap<string, ModelProfile>,
|
|
376
|
-
sourcePath: string,
|
|
377
|
-
): MemoryConfig {
|
|
378
|
-
const where = `Model profiles ${sourcePath}: "memory"`;
|
|
379
|
-
if (!isRecord(value)) {
|
|
380
|
-
throw new Error(`${where} must be an object.`);
|
|
381
|
-
}
|
|
382
|
-
assertKnownKeys(
|
|
383
|
-
value,
|
|
384
|
-
MEMORY_CONFIG_FIELDS.map((field) => field.name),
|
|
385
|
-
where,
|
|
386
|
-
);
|
|
387
|
-
const profile = requireString(value.profile, `${where}.profile`);
|
|
388
|
-
if (!profiles.has(profile)) {
|
|
389
|
-
throw unknownProfileNamesError(profile, [...profiles.keys()]);
|
|
390
|
-
}
|
|
391
|
-
const embedding = parseMemoryEmbedding(value.embedding, `${where}.embedding`);
|
|
392
|
-
return Object.freeze({ profile, embedding });
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function parseMemoryEmbedding(value: unknown, where: string): MemoryEmbeddingConfig {
|
|
396
|
-
if (!isRecord(value)) {
|
|
397
|
-
throw new Error(`${where} must be an object.`);
|
|
398
|
-
}
|
|
399
|
-
assertKnownKeys(
|
|
400
|
-
value,
|
|
401
|
-
MEMORY_EMBEDDING_FIELDS.map((field) => field.name),
|
|
402
|
-
where,
|
|
403
|
-
);
|
|
404
|
-
const name = requireString(value.name, `${where}.name`);
|
|
405
|
-
if (value.kind !== "openai-compatible") {
|
|
406
|
-
throw new Error(`${where}.kind must be "openai-compatible".`);
|
|
407
|
-
}
|
|
408
|
-
const model = requireString(value.model, `${where}.model`);
|
|
409
|
-
const apiBase = requireString(value.apiBase, `${where}.apiBase`);
|
|
410
|
-
requireHttpUrl(apiBase, `${where}.apiBase`);
|
|
411
|
-
const apiKey = requireString(value.apiKey, `${where}.apiKey`);
|
|
412
|
-
const dimensions = requirePositiveInteger(value.dimensions, `${where}.dimensions`);
|
|
413
|
-
return Object.freeze({
|
|
414
|
-
name,
|
|
415
|
-
kind: "openai-compatible",
|
|
416
|
-
model,
|
|
417
|
-
apiBase,
|
|
418
|
-
apiKey,
|
|
419
|
-
dimensions,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
359
|
function parseInputModalities(
|
|
424
360
|
value: unknown,
|
|
425
361
|
name: string,
|
|
@@ -538,22 +474,6 @@ function requirePositiveInteger(value: unknown, name: string): number {
|
|
|
538
474
|
return value;
|
|
539
475
|
}
|
|
540
476
|
|
|
541
|
-
function requireHttpUrl(value: string, name: string): void {
|
|
542
|
-
let parsed: URL;
|
|
543
|
-
try {
|
|
544
|
-
parsed = new URL(value);
|
|
545
|
-
} catch {
|
|
546
|
-
throw new Error(`${name} must be a valid HTTP(S) URL.`);
|
|
547
|
-
}
|
|
548
|
-
if (
|
|
549
|
-
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
550
|
-
parsed.username !== "" ||
|
|
551
|
-
parsed.password !== ""
|
|
552
|
-
) {
|
|
553
|
-
throw new Error(`${name} must be a valid HTTP(S) URL.`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
477
|
function parseBoolean(value: unknown, name: string): boolean {
|
|
558
478
|
if (typeof value === "boolean") {
|
|
559
479
|
return value;
|
|
@@ -379,91 +379,10 @@ export const MODEL_REASONING_FIELDS = Object.freeze([
|
|
|
379
379
|
}),
|
|
380
380
|
]);
|
|
381
381
|
|
|
382
|
-
export type MemoryConfigField = {
|
|
383
|
-
readonly name: "profile" | "embedding";
|
|
384
|
-
readonly valueKind: "non-empty-string" | "embedding-profile";
|
|
385
|
-
readonly required: true;
|
|
386
|
-
readonly secret: boolean;
|
|
387
|
-
readonly description: string;
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
export const MEMORY_CONFIG_FIELDS = Object.freeze([
|
|
391
|
-
Object.freeze({
|
|
392
|
-
name: "profile",
|
|
393
|
-
valueKind: "non-empty-string",
|
|
394
|
-
required: true,
|
|
395
|
-
secret: false,
|
|
396
|
-
description:
|
|
397
|
-
"Existing model profile used for completed-turn atomic-memory extraction.",
|
|
398
|
-
}),
|
|
399
|
-
Object.freeze({
|
|
400
|
-
name: "embedding",
|
|
401
|
-
valueKind: "embedding-profile",
|
|
402
|
-
required: true,
|
|
403
|
-
secret: true,
|
|
404
|
-
description: "Single embedding profile for the global memory database.",
|
|
405
|
-
}),
|
|
406
|
-
] satisfies readonly MemoryConfigField[]);
|
|
407
|
-
|
|
408
|
-
export type MemoryEmbeddingField = {
|
|
409
|
-
readonly name: "name" | "kind" | "model" | "apiBase" | "apiKey" | "dimensions";
|
|
410
|
-
readonly valueKind: "non-empty-string" | "positive-integer" | "literal-string";
|
|
411
|
-
readonly required: true;
|
|
412
|
-
readonly secret: boolean;
|
|
413
|
-
readonly literalValue?: "openai-compatible";
|
|
414
|
-
readonly description: string;
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
export const MEMORY_EMBEDDING_FIELDS = Object.freeze([
|
|
418
|
-
Object.freeze({
|
|
419
|
-
name: "name",
|
|
420
|
-
valueKind: "non-empty-string",
|
|
421
|
-
required: true,
|
|
422
|
-
secret: false,
|
|
423
|
-
description: "Stable identity for the embedding space.",
|
|
424
|
-
}),
|
|
425
|
-
Object.freeze({
|
|
426
|
-
name: "kind",
|
|
427
|
-
valueKind: "literal-string",
|
|
428
|
-
required: true,
|
|
429
|
-
secret: false,
|
|
430
|
-
literalValue: "openai-compatible",
|
|
431
|
-
description: "Embedding transport kind.",
|
|
432
|
-
}),
|
|
433
|
-
Object.freeze({
|
|
434
|
-
name: "model",
|
|
435
|
-
valueKind: "non-empty-string",
|
|
436
|
-
required: true,
|
|
437
|
-
secret: false,
|
|
438
|
-
description: "Embedding provider model name.",
|
|
439
|
-
}),
|
|
440
|
-
Object.freeze({
|
|
441
|
-
name: "apiBase",
|
|
442
|
-
valueKind: "non-empty-string",
|
|
443
|
-
required: true,
|
|
444
|
-
secret: false,
|
|
445
|
-
description: "OpenAI-compatible API base URL.",
|
|
446
|
-
}),
|
|
447
|
-
Object.freeze({
|
|
448
|
-
name: "apiKey",
|
|
449
|
-
valueKind: "non-empty-string",
|
|
450
|
-
required: true,
|
|
451
|
-
secret: true,
|
|
452
|
-
description: "Embedding provider credential.",
|
|
453
|
-
}),
|
|
454
|
-
Object.freeze({
|
|
455
|
-
name: "dimensions",
|
|
456
|
-
valueKind: "positive-integer",
|
|
457
|
-
required: true,
|
|
458
|
-
secret: false,
|
|
459
|
-
description: "Fixed vector dimensions for the global memory database.",
|
|
460
|
-
}),
|
|
461
|
-
] satisfies readonly MemoryEmbeddingField[]);
|
|
462
|
-
|
|
463
382
|
export const MODEL_PROFILES_DOCUMENT_FIELDS = Object.freeze([
|
|
464
383
|
Object.freeze({ name: "default", valueKind: "non-empty-string" as const }),
|
|
465
384
|
Object.freeze({ name: "profiles", valueKind: "profiles-map" as const }),
|
|
466
|
-
Object.freeze({ name: "memory", valueKind: "
|
|
385
|
+
Object.freeze({ name: "memory", valueKind: "ignored-legacy-config" as const }),
|
|
467
386
|
]);
|
|
468
387
|
|
|
469
388
|
export type PublicToolingConfig = {
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -46,7 +46,7 @@ import { loadSkillCatalog } from "../skills/skill-loader";
|
|
|
46
46
|
import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
47
47
|
import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
48
48
|
import { clipboardWriterForEnvironment } from "../tui/clipboard";
|
|
49
|
-
import {
|
|
49
|
+
import { listMemoryFiles } from "../memory/memory-files";
|
|
50
50
|
import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
|
|
51
51
|
import { createReasoningEffortController } from "../model/reasoning-effort";
|
|
52
52
|
|
|
@@ -63,13 +63,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
63
63
|
options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
|
|
64
64
|
const config = options.initialRunnerConfig;
|
|
65
65
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
66
|
-
const memory = await initializeTuiMemory({
|
|
67
|
-
config:
|
|
68
|
-
options.publicConfig.mode === "profile" ? options.publicConfig.memory : undefined,
|
|
69
|
-
env: options.env,
|
|
70
|
-
});
|
|
71
|
-
const memoryCoordinator = memory.coordinator;
|
|
72
|
-
const memoryNotice = memory.notice;
|
|
73
66
|
let controller: DefaultTuiSessionController | undefined;
|
|
74
67
|
let instance: ReturnType<typeof render> | undefined;
|
|
75
68
|
let disposeReason: SessionDisposeReason = { type: "tui_exit" };
|
|
@@ -119,36 +112,12 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
119
112
|
toolingConfig: options.publicConfig.tooling,
|
|
120
113
|
enableTurnUndo: true,
|
|
121
114
|
enableAskUser: true,
|
|
115
|
+
enableProviderRetryPrompt: true,
|
|
122
116
|
bashGuard: {
|
|
123
117
|
mode: sessionConfig.bashGuardMode,
|
|
124
118
|
source: sessionConfig.bashGuardSource,
|
|
125
119
|
surface: "tui" as const,
|
|
126
120
|
},
|
|
127
|
-
...(memoryCoordinator === undefined
|
|
128
|
-
? {}
|
|
129
|
-
: {
|
|
130
|
-
memorySearch: memoryCoordinator.createSearchToolExecutor({
|
|
131
|
-
workspaceRoot,
|
|
132
|
-
sessionId,
|
|
133
|
-
}),
|
|
134
|
-
memoryGet: memoryCoordinator.createGetToolExecutor({
|
|
135
|
-
workspaceRoot,
|
|
136
|
-
sessionId,
|
|
137
|
-
}),
|
|
138
|
-
memoryCreate: memoryCoordinator.createCreateToolExecutor({
|
|
139
|
-
workspaceRoot,
|
|
140
|
-
sessionId,
|
|
141
|
-
}),
|
|
142
|
-
memoryUpdate: memoryCoordinator.createUpdateToolExecutor({
|
|
143
|
-
workspaceRoot,
|
|
144
|
-
sessionId,
|
|
145
|
-
}),
|
|
146
|
-
memoryDelete: memoryCoordinator.createDeleteToolExecutor({
|
|
147
|
-
workspaceRoot,
|
|
148
|
-
sessionId,
|
|
149
|
-
}),
|
|
150
|
-
completedTurnHook: memoryCoordinator,
|
|
151
|
-
}),
|
|
152
121
|
};
|
|
153
122
|
if (mode === "resume") {
|
|
154
123
|
return createRuntimeSession({
|
|
@@ -319,13 +288,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
319
288
|
onQuit={() => {
|
|
320
289
|
quitRequested = true;
|
|
321
290
|
}}
|
|
322
|
-
|
|
323
|
-
memoryDisabledNotice={memoryNotice}
|
|
324
|
-
listStoredMemories={
|
|
325
|
-
memoryCoordinator === undefined
|
|
326
|
-
? undefined
|
|
327
|
-
: () => memoryCoordinator.listStoredMemories()
|
|
328
|
-
}
|
|
291
|
+
listStoredMemories={() => listMemoryFiles()}
|
|
329
292
|
/>,
|
|
330
293
|
{ incrementalRendering: true },
|
|
331
294
|
);
|
|
@@ -349,7 +312,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
349
312
|
);
|
|
350
313
|
}
|
|
351
314
|
}
|
|
352
|
-
memoryCoordinator?.dispose();
|
|
353
315
|
}
|
|
354
316
|
|
|
355
317
|
if (primaryError !== undefined) {
|
|
@@ -197,6 +197,13 @@ function toolCallSummary(call: ToolCall): string {
|
|
|
197
197
|
.join("\n");
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
if (call.name === "MemorySearch" && Array.isArray(args.keywords)) {
|
|
201
|
+
return [
|
|
202
|
+
`Call ID: ${call.toolCallId}`,
|
|
203
|
+
`Keywords: ${JSON.stringify(args.keywords)}`,
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
200
207
|
const filePath = stringProperty(args, "file_path");
|
|
201
208
|
const pattern = stringProperty(args, "pattern");
|
|
202
209
|
return [
|
package/src/events/types.ts
CHANGED
|
@@ -337,6 +337,12 @@ export type AgentEventDataMap = {
|
|
|
337
337
|
"agent.iteration.started": { iterationNumber: number };
|
|
338
338
|
"model.request.started": ModelRequestAttemptData;
|
|
339
339
|
"model.request.failed": ModelRequestFailedData;
|
|
340
|
+
"model.retry.requested": ModelRequestFailedData;
|
|
341
|
+
"model.retry.resolved": {
|
|
342
|
+
attemptNumber: number;
|
|
343
|
+
decision: "retry" | "stop" | "cancelled";
|
|
344
|
+
durationMs: number;
|
|
345
|
+
};
|
|
340
346
|
"model.request.finished": ModelRequestAttemptData & {
|
|
341
347
|
output: ModelRequestOutput;
|
|
342
348
|
};
|
|
@@ -457,6 +463,8 @@ export type AgentEventInput =
|
|
|
457
463
|
| "agent.iteration.started"
|
|
458
464
|
| "model.request.started"
|
|
459
465
|
| "model.request.failed"
|
|
466
|
+
| "model.retry.requested"
|
|
467
|
+
| "model.retry.resolved"
|
|
460
468
|
| "model.request.finished"
|
|
461
469
|
| "context.usage.updated"
|
|
462
470
|
| "context.shadow.planned"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
// An OS permission prompt can leave open() pending without accepting a signal.
|
|
4
|
+
// Cancellation releases the caller; ownership of a late handle stays here.
|
|
5
|
+
export function abortableFileOpen(
|
|
6
|
+
openFile: () => Promise<FileHandle>,
|
|
7
|
+
signal?: AbortSignal,
|
|
8
|
+
onWarning?: (message: string) => void,
|
|
9
|
+
): Promise<FileHandle> {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
if (signal === undefined) return openFile();
|
|
12
|
+
|
|
13
|
+
return new Promise<FileHandle>((resolve, reject) => {
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
const onAbort = () => {
|
|
16
|
+
cancelled = true;
|
|
17
|
+
signal.removeEventListener("abort", onAbort);
|
|
18
|
+
reject(asError(signal.reason));
|
|
19
|
+
};
|
|
20
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
21
|
+
// Also handle a synchronous failure from the opener without leaking a listener.
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
.then(() => {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
return openFile();
|
|
26
|
+
})
|
|
27
|
+
.then(async (handle) => {
|
|
28
|
+
signal.removeEventListener("abort", onAbort);
|
|
29
|
+
if (cancelled) {
|
|
30
|
+
try {
|
|
31
|
+
await handle.close();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
onWarning?.(
|
|
34
|
+
`Failed to close cancelled image file: ${error instanceof Error ? error.message : String(error)}.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
resolve(handle);
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.catch((error: unknown) => {
|
|
42
|
+
signal.removeEventListener("abort", onAbort);
|
|
43
|
+
if (cancelled) {
|
|
44
|
+
// Late open failures must not become unhandled rejections.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
reject(asError(error));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asError(error: unknown): Error {
|
|
53
|
+
return error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
54
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { createUuidV7 } from "../ids/uuid-v7";
|
|
15
15
|
import { IMAGE_INPUT_POLICY } from "./image-input-policy";
|
|
16
|
+
import { abortableFileOpen } from "./abortable-file-open";
|
|
16
17
|
import { probeImageBytes } from "./image-probe";
|
|
17
18
|
import {
|
|
18
19
|
normalizeOriginalImageName,
|
|
@@ -116,9 +117,14 @@ export class ImageAssetStore {
|
|
|
116
117
|
assertContained(this.workspaceRoot, canonicalSource, "Image source realpath");
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const handle = await
|
|
120
|
+
const handle = await abortableFileOpen(
|
|
121
|
+
() => open(canonicalSource, constants.O_RDONLY | noFollowFlag()),
|
|
122
|
+
options.signal,
|
|
123
|
+
this.onWarning,
|
|
124
|
+
);
|
|
120
125
|
let bytes: Buffer;
|
|
121
126
|
try {
|
|
127
|
+
throwIfAborted(options.signal);
|
|
122
128
|
const handleStat = await handle.stat();
|
|
123
129
|
if (
|
|
124
130
|
!handleStat.isFile() ||
|