tinker-agent 1.0.65
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/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createModelContextProfile,
|
|
5
|
+
type ModelContextProfile,
|
|
6
|
+
} from "../model/model-context-profile";
|
|
7
|
+
|
|
8
|
+
export type ModelProfile = {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly model: string;
|
|
11
|
+
readonly apiBase: string;
|
|
12
|
+
readonly apiKey: string;
|
|
13
|
+
readonly contextWindowTokens: number;
|
|
14
|
+
readonly maxSupportedOutputTokens: number;
|
|
15
|
+
readonly includeReasoningContent: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ModelProfiles = {
|
|
19
|
+
readonly defaultProfile: string;
|
|
20
|
+
readonly profiles: ReadonlyMap<string, ModelProfile>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function modelsConfigPath(
|
|
24
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
25
|
+
): string | undefined {
|
|
26
|
+
const value = env.TINKER_MODELS;
|
|
27
|
+
if (value === undefined || value.trim() === "") {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function loadModelProfiles(
|
|
34
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
35
|
+
): Promise<ModelProfiles | undefined> {
|
|
36
|
+
const configPath = modelsConfigPath(env);
|
|
37
|
+
if (configPath === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let raw: string;
|
|
42
|
+
try {
|
|
43
|
+
raw = await readFile(configPath, "utf8");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to read model profiles at ${configPath}: ${errorMessage(error)}`,
|
|
47
|
+
{ cause: error },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parseModelProfiles(raw, configPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function persistDefaultProfile(
|
|
55
|
+
profileName: string,
|
|
56
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const configPath = modelsConfigPath(env);
|
|
59
|
+
if (configPath === undefined) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let raw: string;
|
|
64
|
+
try {
|
|
65
|
+
raw = await readFile(configPath, "utf8");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to read model profiles at ${configPath}: ${errorMessage(error)}`,
|
|
69
|
+
{ cause: error },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const json = JSON.parse(raw) as Record<string, unknown>;
|
|
74
|
+
if (json.default === profileName) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const profileValues = json.profiles;
|
|
79
|
+
if (!isRecord(profileValues) || profileValues[profileName] === undefined) {
|
|
80
|
+
const available = isRecord(profileValues) ? Object.keys(profileValues) : [];
|
|
81
|
+
throw unknownProfileNamesError(profileName, available);
|
|
82
|
+
}
|
|
83
|
+
json.default = profileName;
|
|
84
|
+
parseModelProfiles(JSON.stringify(json), configPath);
|
|
85
|
+
const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
86
|
+
try {
|
|
87
|
+
await writeFile(temporaryPath, `${JSON.stringify(json, null, 2)}\n`, {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
flag: "wx",
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
92
|
+
await rename(temporaryPath, configPath);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Failed to persist default model profile at ${configPath}: ${errorMessage(error)}`,
|
|
97
|
+
{ cause: error },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseModelProfiles(raw: string, sourcePath: string): ModelProfiles {
|
|
103
|
+
let json: unknown;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
json = JSON.parse(raw);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid JSON in model profiles ${sourcePath}: ${errorMessage(error)}`,
|
|
110
|
+
{ cause: error },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isRecord(json)) {
|
|
115
|
+
throw new Error(`Model profiles ${sourcePath} must be a JSON object.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const defaultProfile = json.default;
|
|
119
|
+
if (typeof defaultProfile !== "string" || defaultProfile.trim() === "") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Model profiles ${sourcePath} requires a non-empty string "default" field.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const profilesValue = json.profiles;
|
|
126
|
+
if (!isRecord(profilesValue)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Model profiles ${sourcePath} requires an object "profiles" field.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (profilesValue[defaultProfile] === undefined) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Model profiles ${sourcePath}: default profile "${defaultProfile}" is not defined in "profiles".`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const profiles = new Map<string, ModelProfile>();
|
|
139
|
+
|
|
140
|
+
for (const [profileName, profileValue] of Object.entries(profilesValue)) {
|
|
141
|
+
profiles.set(profileName, parseProfile(profileName, profileValue, sourcePath));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { defaultProfile, profiles };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveModelProfile(
|
|
148
|
+
profiles: ModelProfiles | undefined,
|
|
149
|
+
profileName: string | undefined,
|
|
150
|
+
): ModelProfile | undefined {
|
|
151
|
+
if (profiles === undefined) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const name = profileName ?? profiles.defaultProfile;
|
|
156
|
+
return profiles.profiles.get(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveSessionProfileName(
|
|
160
|
+
profiles: ModelProfiles,
|
|
161
|
+
session: { profileName?: string; modelName: string },
|
|
162
|
+
): string {
|
|
163
|
+
if (session.profileName !== undefined) {
|
|
164
|
+
if (!profiles.profiles.has(session.profileName)) {
|
|
165
|
+
throw unknownProfileError(session.profileName, profiles);
|
|
166
|
+
}
|
|
167
|
+
return session.profileName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const matches = [...profiles.profiles.values()].filter(
|
|
171
|
+
(profile) => profile.model === session.modelName,
|
|
172
|
+
);
|
|
173
|
+
if (matches.length === 1) {
|
|
174
|
+
return matches[0].name;
|
|
175
|
+
}
|
|
176
|
+
if (matches.length === 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Legacy session model ${JSON.stringify(session.modelName)} does not match any configured profile.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Legacy session model ${JSON.stringify(session.modelName)} matches multiple profiles: ${matches.map((profile) => profile.name).join(", ")}.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function unknownProfileError(
|
|
187
|
+
profileName: string,
|
|
188
|
+
profiles: ModelProfiles,
|
|
189
|
+
): Error {
|
|
190
|
+
return unknownProfileNamesError(profileName, [...profiles.profiles.keys()]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function unknownProfileNamesError(
|
|
194
|
+
profileName: string,
|
|
195
|
+
profileNames: readonly string[],
|
|
196
|
+
): Error {
|
|
197
|
+
const available = profileNames.join(", ");
|
|
198
|
+
return new Error(
|
|
199
|
+
`Unknown model profile ${JSON.stringify(profileName)}. Available profiles: ${available}.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function profileToContextProfile(profile: ModelProfile): ModelContextProfile {
|
|
204
|
+
return createModelContextProfile({
|
|
205
|
+
contextWindowTokens: profile.contextWindowTokens,
|
|
206
|
+
maxSupportedOutputTokens: profile.maxSupportedOutputTokens,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseProfile(
|
|
211
|
+
profileName: string,
|
|
212
|
+
value: unknown,
|
|
213
|
+
sourcePath: string,
|
|
214
|
+
): ModelProfile {
|
|
215
|
+
const where = `Model profiles ${sourcePath}: profile "${profileName}"`;
|
|
216
|
+
|
|
217
|
+
if (profileName.trim() === "") {
|
|
218
|
+
throw new Error(`${where} has an empty name.`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!isRecord(value)) {
|
|
222
|
+
throw new Error(`${where} must be an object.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const model = requireString(value.model, `${where}: "model"`);
|
|
226
|
+
const apiBase = requireString(value.apiBase, `${where}: "apiBase"`);
|
|
227
|
+
const apiKey = requireString(value.apiKey, `${where}: "apiKey"`);
|
|
228
|
+
|
|
229
|
+
const contextWindowTokens = requirePositiveInteger(
|
|
230
|
+
value.contextWindowTokens,
|
|
231
|
+
`${where}: "contextWindowTokens"`,
|
|
232
|
+
);
|
|
233
|
+
const maxSupportedOutputTokens = requirePositiveInteger(
|
|
234
|
+
value.maxSupportedOutputTokens,
|
|
235
|
+
`${where}: "maxSupportedOutputTokens"`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const includeReasoningContent =
|
|
239
|
+
value.includeReasoningContent === undefined
|
|
240
|
+
? false
|
|
241
|
+
: parseBoolean(
|
|
242
|
+
value.includeReasoningContent,
|
|
243
|
+
`${where}: "includeReasoningContent"`,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
createModelContextProfile({
|
|
247
|
+
contextWindowTokens,
|
|
248
|
+
maxSupportedOutputTokens,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
name: profileName,
|
|
253
|
+
model,
|
|
254
|
+
apiBase,
|
|
255
|
+
apiKey,
|
|
256
|
+
contextWindowTokens,
|
|
257
|
+
maxSupportedOutputTokens,
|
|
258
|
+
includeReasoningContent,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
263
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function requireString(value: unknown, name: string): string {
|
|
267
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
268
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requirePositiveInteger(value: unknown, name: string): number {
|
|
274
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
275
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseBoolean(value: unknown, name: string): boolean {
|
|
281
|
+
if (typeof value === "boolean") {
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
throw new Error(`${name} must be a boolean.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function errorMessage(error: unknown): string {
|
|
288
|
+
return error instanceof Error ? error.message : String(error);
|
|
289
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeSession,
|
|
3
|
+
type RuntimeSession,
|
|
4
|
+
type SessionDisposeReason,
|
|
5
|
+
} from "../agent/runtime-session";
|
|
6
|
+
import { StdoutEventPrinter, type WritableLike } from "../events/stdout-event-printer";
|
|
7
|
+
import type { ModelClient } from "../model/model-client";
|
|
8
|
+
import {
|
|
9
|
+
buildSystemPrompt,
|
|
10
|
+
loadProjectInstructions,
|
|
11
|
+
projectInstructionManifest,
|
|
12
|
+
} from "../instructions/project-instructions";
|
|
13
|
+
import {
|
|
14
|
+
createRunnerModelClient,
|
|
15
|
+
createWebFetchRefinerFromEnv,
|
|
16
|
+
readRunnerConfig,
|
|
17
|
+
RUNTIME_INSTRUCTIONS,
|
|
18
|
+
type RunnerConfigOverrides,
|
|
19
|
+
} from "./config";
|
|
20
|
+
import { loadModelProfiles } from "./model-profiles";
|
|
21
|
+
import { realpath } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
export type RunOneShotOptions = RunnerConfigOverrides & {
|
|
24
|
+
modelClient?: ModelClient;
|
|
25
|
+
stdout?: WritableLike;
|
|
26
|
+
stderr?: WritableLike;
|
|
27
|
+
eventLogPath?: string | false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function runOneShot(
|
|
31
|
+
userPrompt: string,
|
|
32
|
+
options: RunOneShotOptions = {},
|
|
33
|
+
): Promise<number> {
|
|
34
|
+
const stdout = options.stdout ?? process.stdout;
|
|
35
|
+
const stderr = options.stderr ?? process.stderr;
|
|
36
|
+
let session: RuntimeSession | undefined;
|
|
37
|
+
let disposeReason: SessionDisposeReason = { type: "oneshot_complete" };
|
|
38
|
+
let primaryError: unknown;
|
|
39
|
+
let exitCode = 1;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const profiles =
|
|
43
|
+
options.modelClient === undefined ? await loadModelProfiles() : undefined;
|
|
44
|
+
const config = readRunnerConfig(options, profiles);
|
|
45
|
+
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
46
|
+
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
47
|
+
const systemPrompt = buildSystemPrompt({
|
|
48
|
+
workspaceRoot,
|
|
49
|
+
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
50
|
+
projectInstructions,
|
|
51
|
+
});
|
|
52
|
+
const modelClient = createRunnerModelClient(config, options.modelClient);
|
|
53
|
+
session = await createRuntimeSession({
|
|
54
|
+
selection: { mode: "new", sessionId: config.sessionId },
|
|
55
|
+
workspaceRoot,
|
|
56
|
+
modelName: config.modelName,
|
|
57
|
+
profileName: config.profileName,
|
|
58
|
+
maxIterations: config.maxIterations,
|
|
59
|
+
includeReasoningContent: config.includeReasoningContent,
|
|
60
|
+
contextProfile: config.contextProfile,
|
|
61
|
+
contextBudget: config.contextBudget,
|
|
62
|
+
systemPrompt,
|
|
63
|
+
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
64
|
+
modelClient,
|
|
65
|
+
presentationSinks: [new StdoutEventPrinter(stdout, stderr)],
|
|
66
|
+
persistence:
|
|
67
|
+
options.eventLogPath === false ? false : { eventLogPath: options.eventLogPath },
|
|
68
|
+
webFetchRefiner: createWebFetchRefinerFromEnv(config),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await session.executeTurn({
|
|
72
|
+
userPrompt,
|
|
73
|
+
signal: new AbortController().signal,
|
|
74
|
+
});
|
|
75
|
+
if (result.status === "completed") {
|
|
76
|
+
stdout.write(`\n${result.finalText}\n`);
|
|
77
|
+
exitCode = 0;
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
primaryError = error;
|
|
81
|
+
disposeReason = { type: "runner_failed", error: errorMessage(error) };
|
|
82
|
+
stderr.write(`Runtime failed: ${errorMessage(error)}\n`);
|
|
83
|
+
} finally {
|
|
84
|
+
if (session !== undefined) {
|
|
85
|
+
try {
|
|
86
|
+
await session.dispose(disposeReason);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
exitCode = 1;
|
|
89
|
+
if (primaryError === undefined) {
|
|
90
|
+
stderr.write(`Runtime cleanup failed: ${errorMessage(error)}\n`);
|
|
91
|
+
} else {
|
|
92
|
+
const combined = new AggregateError(
|
|
93
|
+
[primaryError, error],
|
|
94
|
+
"Runtime execution and cleanup failed.",
|
|
95
|
+
);
|
|
96
|
+
stderr.write(`Runtime cleanup failed: ${errorMessage(combined)}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return exitCode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function errorMessage(error: unknown): string {
|
|
106
|
+
return error instanceof Error ? error.message : String(error);
|
|
107
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createRuntimeSession,
|
|
5
|
+
type RuntimeSession,
|
|
6
|
+
type SessionDisposeReason,
|
|
7
|
+
} from "../agent/runtime-session";
|
|
8
|
+
import type { EventSink } from "../events/event-sink";
|
|
9
|
+
import type { AgentEvent } from "../events/types";
|
|
10
|
+
import { createUuidV7 } from "../ids/uuid-v7";
|
|
11
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
12
|
+
import {
|
|
13
|
+
buildSystemPrompt,
|
|
14
|
+
loadProjectInstructions,
|
|
15
|
+
projectInstructionManifest,
|
|
16
|
+
} from "../instructions/project-instructions";
|
|
17
|
+
import { ResumeProjectionReader } from "../session/resume-projection";
|
|
18
|
+
import { SessionCatalog } from "../session/session-catalog";
|
|
19
|
+
import { App } from "../tui/app";
|
|
20
|
+
import { readCurrentGitBranch } from "../tui/git-branch";
|
|
21
|
+
import { PromptHistory } from "../tui/prompt-history";
|
|
22
|
+
import { TuiProjectionStore } from "../tui/tui-projection-store";
|
|
23
|
+
import {
|
|
24
|
+
DefaultTuiSessionController,
|
|
25
|
+
managedTuiBinding,
|
|
26
|
+
type ManagedTuiSessionBinding,
|
|
27
|
+
} from "../tui/tui-session-controller";
|
|
28
|
+
import {
|
|
29
|
+
createRunnerModelClient,
|
|
30
|
+
createWebFetchRefinerFromEnv,
|
|
31
|
+
promptHistoryPath,
|
|
32
|
+
readRunnerConfig,
|
|
33
|
+
RUNTIME_INSTRUCTIONS,
|
|
34
|
+
type RunnerConfig,
|
|
35
|
+
} from "./config";
|
|
36
|
+
import {
|
|
37
|
+
loadModelProfiles,
|
|
38
|
+
persistDefaultProfile,
|
|
39
|
+
resolveSessionProfileName,
|
|
40
|
+
type ModelProfile,
|
|
41
|
+
} from "./model-profiles";
|
|
42
|
+
|
|
43
|
+
export async function runTui(options: { profileName?: string } = {}): Promise<void> {
|
|
44
|
+
const profiles = await loadModelProfiles();
|
|
45
|
+
const config = readRunnerConfig(
|
|
46
|
+
options.profileName !== undefined ? { profileName: options.profileName } : {},
|
|
47
|
+
profiles,
|
|
48
|
+
);
|
|
49
|
+
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
50
|
+
let controller: DefaultTuiSessionController | undefined;
|
|
51
|
+
let instance: ReturnType<typeof render> | undefined;
|
|
52
|
+
let disposeReason: SessionDisposeReason = { type: "tui_exit" };
|
|
53
|
+
let primaryError: unknown;
|
|
54
|
+
let quitRequested = false;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const createSessionForConfig = async (
|
|
58
|
+
sessionConfig: RunnerConfig,
|
|
59
|
+
mode: "new" | "resume",
|
|
60
|
+
sessionId: SessionId,
|
|
61
|
+
sink: EventSink,
|
|
62
|
+
): Promise<RuntimeSession> => {
|
|
63
|
+
const modelClient = createRunnerModelClient(sessionConfig);
|
|
64
|
+
const common = {
|
|
65
|
+
workspaceRoot,
|
|
66
|
+
modelName: sessionConfig.modelName,
|
|
67
|
+
profileName: sessionConfig.profileName,
|
|
68
|
+
maxIterations: sessionConfig.maxIterations,
|
|
69
|
+
includeReasoningContent: sessionConfig.includeReasoningContent,
|
|
70
|
+
contextProfile: sessionConfig.contextProfile,
|
|
71
|
+
contextBudget: sessionConfig.contextBudget,
|
|
72
|
+
modelClient,
|
|
73
|
+
presentationSinks: [sink],
|
|
74
|
+
webFetchRefiner: createWebFetchRefinerFromEnv(sessionConfig),
|
|
75
|
+
};
|
|
76
|
+
if (mode === "resume") {
|
|
77
|
+
return createRuntimeSession({
|
|
78
|
+
...common,
|
|
79
|
+
selection: { mode, sessionId },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
84
|
+
return createRuntimeSession({
|
|
85
|
+
...common,
|
|
86
|
+
selection: { mode, sessionId },
|
|
87
|
+
systemPrompt: buildSystemPrompt({
|
|
88
|
+
workspaceRoot,
|
|
89
|
+
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
90
|
+
projectInstructions,
|
|
91
|
+
}),
|
|
92
|
+
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const projectionStore = new TuiProjectionStore({
|
|
97
|
+
sessionId: config.sessionId,
|
|
98
|
+
modelName: config.modelName,
|
|
99
|
+
workspaceRoot,
|
|
100
|
+
});
|
|
101
|
+
const initialSession = await createSessionForConfig(
|
|
102
|
+
config,
|
|
103
|
+
"new",
|
|
104
|
+
config.sessionId,
|
|
105
|
+
projectionStore,
|
|
106
|
+
);
|
|
107
|
+
const promptHistory = await PromptHistory.load(promptHistoryPath(workspaceRoot));
|
|
108
|
+
const catalog = new SessionCatalog({ workspaceRoot });
|
|
109
|
+
|
|
110
|
+
const openStoredSession = async (
|
|
111
|
+
sessionId: SessionId,
|
|
112
|
+
): Promise<ManagedTuiSessionBinding> => {
|
|
113
|
+
const deferred = new DeferredProjectionSink();
|
|
114
|
+
const summary = await catalog.get(sessionId);
|
|
115
|
+
const resumeConfig =
|
|
116
|
+
profiles === undefined
|
|
117
|
+
? { ...config, sessionId }
|
|
118
|
+
: readRunnerConfig(
|
|
119
|
+
{
|
|
120
|
+
sessionId,
|
|
121
|
+
profileName: resolveSessionProfileName(profiles, summary),
|
|
122
|
+
workspaceRoot: config.workspaceRoot,
|
|
123
|
+
maxIterations: config.maxIterations,
|
|
124
|
+
},
|
|
125
|
+
profiles,
|
|
126
|
+
);
|
|
127
|
+
const runtimeSession = await createSessionForConfig(
|
|
128
|
+
resumeConfig,
|
|
129
|
+
"resume",
|
|
130
|
+
sessionId,
|
|
131
|
+
deferred,
|
|
132
|
+
);
|
|
133
|
+
const targetProjection = new TuiProjectionStore({
|
|
134
|
+
sessionId,
|
|
135
|
+
modelName: resumeConfig.modelName,
|
|
136
|
+
workspaceRoot,
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
targetProjection.hydrate(
|
|
140
|
+
await ResumeProjectionReader.read({
|
|
141
|
+
workspaceRoot,
|
|
142
|
+
sessionId,
|
|
143
|
+
modelName: resumeConfig.modelName,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
await deferred.attach(targetProjection);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
await runtimeSession
|
|
149
|
+
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
150
|
+
.catch(() => undefined);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
return managedTuiBinding({
|
|
154
|
+
runtimeSession,
|
|
155
|
+
modelName: resumeConfig.modelName,
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
profileName: resumeConfig.profileName,
|
|
158
|
+
projectionStore: targetProjection,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const createSessionWithProfile = async (
|
|
163
|
+
profile: ModelProfile,
|
|
164
|
+
): Promise<ManagedTuiSessionBinding> => {
|
|
165
|
+
if (profiles === undefined) {
|
|
166
|
+
throw new Error("Model profiles are not configured.");
|
|
167
|
+
}
|
|
168
|
+
const profileConfig = readRunnerConfig(
|
|
169
|
+
{
|
|
170
|
+
sessionId: createUuidV7() as SessionId,
|
|
171
|
+
workspaceRoot: config.workspaceRoot,
|
|
172
|
+
profileName: profile.name,
|
|
173
|
+
maxIterations: config.maxIterations,
|
|
174
|
+
},
|
|
175
|
+
profiles,
|
|
176
|
+
);
|
|
177
|
+
const profileProjectionStore = new TuiProjectionStore({
|
|
178
|
+
sessionId: profileConfig.sessionId,
|
|
179
|
+
modelName: profileConfig.modelName,
|
|
180
|
+
workspaceRoot,
|
|
181
|
+
});
|
|
182
|
+
const runtimeSession = await createSessionForConfig(
|
|
183
|
+
profileConfig,
|
|
184
|
+
"new",
|
|
185
|
+
profileConfig.sessionId,
|
|
186
|
+
profileProjectionStore,
|
|
187
|
+
);
|
|
188
|
+
return managedTuiBinding({
|
|
189
|
+
runtimeSession,
|
|
190
|
+
modelName: profileConfig.modelName,
|
|
191
|
+
workspaceRoot,
|
|
192
|
+
profileName: profile.name,
|
|
193
|
+
projectionStore: profileProjectionStore,
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
controller = new DefaultTuiSessionController(
|
|
198
|
+
managedTuiBinding({
|
|
199
|
+
runtimeSession: initialSession,
|
|
200
|
+
modelName: config.modelName,
|
|
201
|
+
workspaceRoot,
|
|
202
|
+
profileName: config.profileName,
|
|
203
|
+
projectionStore,
|
|
204
|
+
}),
|
|
205
|
+
catalog,
|
|
206
|
+
openStoredSession,
|
|
207
|
+
createSessionWithProfile,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
instance = render(
|
|
211
|
+
<App
|
|
212
|
+
sessionController={controller}
|
|
213
|
+
readGitBranch={readCurrentGitBranch}
|
|
214
|
+
history={promptHistory}
|
|
215
|
+
profiles={profiles}
|
|
216
|
+
persistDefaultProfile={persistDefaultProfile}
|
|
217
|
+
onQuit={() => {
|
|
218
|
+
quitRequested = true;
|
|
219
|
+
}}
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
await instance.waitUntilExit();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
primaryError = error;
|
|
225
|
+
disposeReason = { type: "runner_failed", error: errorMessage(error) };
|
|
226
|
+
} finally {
|
|
227
|
+
instance?.unmount();
|
|
228
|
+
restoreStdin();
|
|
229
|
+
if (controller !== undefined) {
|
|
230
|
+
try {
|
|
231
|
+
await controller.dispose(disposeReason);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
primaryError =
|
|
234
|
+
primaryError === undefined
|
|
235
|
+
? error
|
|
236
|
+
: new AggregateError(
|
|
237
|
+
[primaryError, error],
|
|
238
|
+
"TUI runtime and cleanup failed.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (primaryError !== undefined) {
|
|
245
|
+
throw asError(primaryError);
|
|
246
|
+
}
|
|
247
|
+
if (quitRequested) {
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class DeferredProjectionSink implements EventSink {
|
|
253
|
+
readonly name = "deferred-tui-projection";
|
|
254
|
+
private target?: TuiProjectionStore;
|
|
255
|
+
private readonly buffered: AgentEvent[] = [];
|
|
256
|
+
|
|
257
|
+
async append(event: AgentEvent): Promise<void> {
|
|
258
|
+
if (this.target === undefined) {
|
|
259
|
+
this.buffered.push(event);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await this.target.append(event);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async attach(target: TuiProjectionStore): Promise<void> {
|
|
266
|
+
if (this.target !== undefined) {
|
|
267
|
+
throw new Error("Deferred projection sink is already attached.");
|
|
268
|
+
}
|
|
269
|
+
this.target = target;
|
|
270
|
+
for (const event of this.buffered) {
|
|
271
|
+
await target.append(event);
|
|
272
|
+
}
|
|
273
|
+
this.buffered.length = 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function restoreStdin(): void {
|
|
278
|
+
if (process.stdin.isTTY) {
|
|
279
|
+
process.stdin.setRawMode(false);
|
|
280
|
+
}
|
|
281
|
+
process.stdin.pause();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function errorMessage(error: unknown): string {
|
|
285
|
+
return error instanceof Error ? error.message : String(error);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function asError(error: unknown): Error {
|
|
289
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
290
|
+
}
|