tinker-agent 2.12.0 → 3.0.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 +66 -1
- package/README.md +25 -4
- package/package.json +2 -1
- package/src/agent/runtime-hosted-session.ts +505 -81
- package/src/agent/runtime-session-contracts.ts +9 -0
- package/src/agent/runtime-session.ts +29 -7
- package/src/cli/command-line.ts +159 -11
- package/src/cli/connect-runner.tsx +63 -0
- package/src/cli/connect-workspace.ts +33 -0
- package/src/cli/default-service-config.ts +163 -0
- package/src/cli/default-tui-runner.ts +23 -0
- package/src/cli/interactive-runtime.ts +87 -0
- package/src/cli/local-service-start.ts +131 -0
- package/src/cli/local-tui-client.ts +234 -0
- package/src/cli/main.ts +46 -3
- package/src/cli/public-cli-contract.ts +65 -2
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +35 -35
- package/src/cli/serve-runner.ts +155 -21
- package/src/cli/serve-runtime.ts +47 -46
- package/src/cli/service-control.ts +27 -0
- package/src/cli/service-supervisor.ts +170 -0
- package/src/cli/service-upgrade.ts +106 -0
- package/src/cli/tui-runner.tsx +9 -261
- package/src/cli/update-runner.ts +4 -0
- package/src/client/local-workspace-client.ts +238 -0
- package/src/client/model-catalog.ts +15 -0
- package/src/client/remote-images.ts +80 -0
- package/src/client/remote-prompt-history.ts +22 -0
- package/src/client/remote-tasks.ts +231 -0
- package/src/client/remote-tui-view.ts +69 -0
- package/src/client/remote-workspace-client.ts +528 -0
- package/src/client/session-client.ts +109 -0
- package/src/image/image-asset-store.ts +17 -0
- package/src/remote/client.ts +27 -5
- package/src/remote/config.ts +19 -4
- package/src/remote/failures.ts +68 -0
- package/src/remote/http-server.ts +120 -3
- package/src/remote/local-service-discovery.ts +361 -0
- package/src/remote/prompt-history.ts +36 -0
- package/src/remote/protocol.ts +93 -2
- package/src/remote/resident-policy.ts +96 -0
- package/src/remote/service-store.ts +28 -6
- package/src/remote/service.ts +479 -60
- package/src/remote/session-operations.ts +66 -0
- package/src/remote/session-reads.ts +126 -0
- package/src/remote/sync-hub.ts +2 -0
- package/src/remote/tui-protocol.ts +32 -0
- package/src/remote/workspace-resolution.ts +82 -0
- package/src/session/resume-projection.ts +19 -2
- package/src/session/session-lock.ts +32 -0
- package/src/session/session-store-contracts.ts +2 -0
- package/src/session/session-store.ts +7 -4
- package/src/tools/bash.ts +9 -1
- package/src/tools/bounded-output-preview.ts +2 -2
- package/src/tui/app.tsx +67 -26
- package/src/tui/components/model-picker.tsx +5 -5
- package/src/tui/components/prompt-input.tsx +16 -1
- package/src/tui/components/resume-session-picker.tsx +6 -2
- package/src/tui/tui-projection-store.ts +25 -0
- package/src/tui/tui-session-controller.ts +12 -282
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstat, readdir, realpath, readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
localServicePaths,
|
|
7
|
+
type LocalServiceInstance,
|
|
8
|
+
} from "../remote/local-service-discovery";
|
|
9
|
+
|
|
10
|
+
/** Check every live service using this installation, including explicit non-default state directories. */
|
|
11
|
+
export async function assertServiceUpgradeSafe(packageRoot: string): Promise<void> {
|
|
12
|
+
if (process.platform === "win32") return;
|
|
13
|
+
const root = path.dirname(localServicePaths("").socket);
|
|
14
|
+
let entries: string[];
|
|
15
|
+
try {
|
|
16
|
+
const info = await lstat(root);
|
|
17
|
+
if (
|
|
18
|
+
!info.isDirectory() ||
|
|
19
|
+
info.isSymbolicLink() ||
|
|
20
|
+
info.uid !== process.getuid?.() ||
|
|
21
|
+
info.mode & 0o077
|
|
22
|
+
)
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Unsafe local service discovery directory; cannot verify upgrade safety.",
|
|
25
|
+
);
|
|
26
|
+
entries = await readdir(root);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
const canonical = await realpath(packageRoot);
|
|
32
|
+
for (const entry of entries.filter((name) =>
|
|
33
|
+
/^[a-f0-9]{32}\.sock\.json$/.test(name),
|
|
34
|
+
)) {
|
|
35
|
+
const instance = JSON.parse(
|
|
36
|
+
await readFile(path.join(root, entry), "utf8"),
|
|
37
|
+
) as LocalServiceInstance;
|
|
38
|
+
if (
|
|
39
|
+
instance.packageRoot === canonical &&
|
|
40
|
+
(await Bun.file(
|
|
41
|
+
path.join(instance.stateDirectory, "supervisor.enabled"),
|
|
42
|
+
).exists())
|
|
43
|
+
)
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Disable the supervised service before upgrading: tinker serve --config ${instance.configPath} --stop.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries.filter((name) => /^[a-f0-9]{32}\.sock$/.test(name))) {
|
|
49
|
+
const instance = await probe(path.join(root, entry));
|
|
50
|
+
if (instance && (!instance.packageRoot || instance.packageRoot === canonical))
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Stop the running service before upgrading: tinker serve --config ${instance.configPath} --stop. Upgrade does not interrupt work or replace a live runtime.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function probe(socketPath: string): Promise<LocalServiceInstance | undefined> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const socket = createConnection(socketPath);
|
|
59
|
+
const nonce = randomUUID();
|
|
60
|
+
let content = "";
|
|
61
|
+
let finished = false;
|
|
62
|
+
const done = (instance?: LocalServiceInstance, error?: Error) => {
|
|
63
|
+
if (finished) return;
|
|
64
|
+
finished = true;
|
|
65
|
+
socket.destroy();
|
|
66
|
+
if (error) reject(error);
|
|
67
|
+
else resolve(instance);
|
|
68
|
+
};
|
|
69
|
+
socket.setEncoding("utf8");
|
|
70
|
+
socket.setTimeout(1000, () =>
|
|
71
|
+
done(
|
|
72
|
+
undefined,
|
|
73
|
+
new Error(
|
|
74
|
+
"A local service is unresponsive; stop it explicitly before upgrading.",
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
socket.once("connect", () => socket.write(`${nonce}\n`));
|
|
79
|
+
socket.once("error", (error: NodeJS.ErrnoException) =>
|
|
80
|
+
["ENOENT", "ECONNREFUSED"].includes(error.code ?? "")
|
|
81
|
+
? done()
|
|
82
|
+
: done(undefined, error),
|
|
83
|
+
);
|
|
84
|
+
socket.once("end", () => done());
|
|
85
|
+
socket.on("data", (chunk: string) => {
|
|
86
|
+
content += chunk;
|
|
87
|
+
if (content.length > 16384)
|
|
88
|
+
return done(undefined, new Error("Invalid service upgrade probe response."));
|
|
89
|
+
if (!content.includes("\n")) return;
|
|
90
|
+
try {
|
|
91
|
+
const response = JSON.parse(content.trim()) as {
|
|
92
|
+
nonce: string;
|
|
93
|
+
instance: LocalServiceInstance;
|
|
94
|
+
};
|
|
95
|
+
if (response.nonce !== nonce || response.instance?.version !== 1)
|
|
96
|
+
throw new Error("Invalid service upgrade probe response.");
|
|
97
|
+
done(response.instance);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
done(
|
|
100
|
+
undefined,
|
|
101
|
+
error instanceof Error ? error : new Error("Invalid service response."),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -1,54 +1,20 @@
|
|
|
1
1
|
import { render } from "ink";
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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 type {
|
|
11
|
-
AssistantTextDeltaSink,
|
|
12
|
-
AssistantTextDeltaUpdate,
|
|
13
|
-
} from "../agent/assistant-text-delta";
|
|
14
|
-
import { createUuidV7 } from "../ids/uuid-v7";
|
|
15
|
-
import type { SessionId } from "../ids/runtime-id";
|
|
16
|
-
import {
|
|
17
|
-
buildSystemPrompt,
|
|
18
|
-
loadProjectInstructions,
|
|
19
|
-
projectInstructionManifest,
|
|
20
|
-
} from "../instructions/project-instructions";
|
|
21
|
-
import { ResumeProjectionReader } from "../session/resume-projection";
|
|
22
|
-
import { SessionCatalog } from "../session/session-catalog";
|
|
3
|
+
import type { ClientCloseReason } from "../client/session-client";
|
|
4
|
+
import { createLocalTuiClient } from "./local-tui-client";
|
|
23
5
|
import { App } from "../tui/app";
|
|
24
6
|
import { readCurrentGitBranch } from "../tui/git-branch";
|
|
25
7
|
import { PromptHistory } from "../tui/prompt-history";
|
|
26
|
-
import { TuiProjectionStore } from "../tui/tui-projection-store";
|
|
27
|
-
import {
|
|
28
|
-
DefaultTuiSessionController,
|
|
29
|
-
managedTuiBinding,
|
|
30
|
-
type ManagedTuiSessionBinding,
|
|
31
|
-
type TuiSessionBinding,
|
|
32
|
-
} from "../tui/tui-session-controller";
|
|
33
8
|
import {
|
|
34
9
|
promptHistoryPath,
|
|
35
|
-
deriveRunnerConfig,
|
|
36
10
|
type ResolvedPublicConfig,
|
|
37
11
|
type RunnerConfig,
|
|
38
12
|
} from "./config";
|
|
39
|
-
import {
|
|
40
|
-
createRunnerModelClient,
|
|
41
|
-
createWebFetchRefiner,
|
|
42
|
-
RUNTIME_INSTRUCTIONS,
|
|
43
|
-
} from "./runner-dependencies";
|
|
44
|
-
import { resolveSessionProfileName, type ModelProfile } from "./model-profiles";
|
|
45
|
-
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
46
13
|
import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
47
14
|
import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
48
15
|
import { clipboardWriterForEnvironment } from "../tui/clipboard";
|
|
49
16
|
import { listMemoryFiles } from "../memory/memory-files";
|
|
50
17
|
import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
|
|
51
|
-
import { createReasoningEffortController } from "../model/reasoning-effort";
|
|
52
18
|
|
|
53
19
|
export type RunTuiOptions = {
|
|
54
20
|
readonly publicConfig: ResolvedPublicConfig;
|
|
@@ -63,212 +29,23 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
63
29
|
options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
|
|
64
30
|
const config = options.initialRunnerConfig;
|
|
65
31
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
66
|
-
let
|
|
32
|
+
let connection: Awaited<ReturnType<typeof createLocalTuiClient>> | undefined;
|
|
67
33
|
let instance: ReturnType<typeof render> | undefined;
|
|
68
|
-
let
|
|
34
|
+
let closeReason: ClientCloseReason = { type: "client_exit" };
|
|
69
35
|
let primaryError: unknown;
|
|
70
36
|
let quitRequested = false;
|
|
71
37
|
|
|
72
38
|
try {
|
|
73
39
|
const projectSlashCommands = await loadProjectSlashCommands(workspaceRoot);
|
|
74
|
-
|
|
75
|
-
sessionConfig: RunnerConfig,
|
|
76
|
-
mode: "new" | "resume",
|
|
77
|
-
sessionId: SessionId,
|
|
78
|
-
sink: EventSink & AssistantTextDeltaSink,
|
|
79
|
-
): Promise<RuntimeSession> => {
|
|
80
|
-
const reasoningEffort = createReasoningEffortController(sessionConfig.reasoning);
|
|
81
|
-
const modelClient = createRunnerModelClient(
|
|
82
|
-
sessionConfig,
|
|
83
|
-
undefined,
|
|
84
|
-
options.env,
|
|
85
|
-
reasoningEffort,
|
|
86
|
-
);
|
|
87
|
-
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
88
|
-
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
89
|
-
const common = {
|
|
90
|
-
workspaceRoot,
|
|
91
|
-
modelName: sessionConfig.modelName,
|
|
92
|
-
profileName: sessionConfig.profileName,
|
|
93
|
-
maxIterations: sessionConfig.maxIterations,
|
|
94
|
-
includeReasoningContent: sessionConfig.includeReasoningContent,
|
|
95
|
-
contextProfile: sessionConfig.contextProfile,
|
|
96
|
-
contextBudget: sessionConfig.contextBudget,
|
|
97
|
-
modelClient,
|
|
98
|
-
systemPrompt: buildSystemPrompt({
|
|
99
|
-
workspaceRoot,
|
|
100
|
-
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
101
|
-
projectInstructions,
|
|
102
|
-
}),
|
|
103
|
-
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
104
|
-
skillCatalog,
|
|
105
|
-
presentationSinks: [sink],
|
|
106
|
-
assistantTextDeltaSink: sink,
|
|
107
|
-
webFetchRefiner: createWebFetchRefiner(
|
|
108
|
-
sessionConfig,
|
|
109
|
-
options.env,
|
|
110
|
-
reasoningEffort,
|
|
111
|
-
),
|
|
112
|
-
toolingConfig: options.publicConfig.tooling,
|
|
113
|
-
enableTurnUndo: true,
|
|
114
|
-
enableAskUser: true,
|
|
115
|
-
enableProviderRetryPrompt: true,
|
|
116
|
-
bashGuard: {
|
|
117
|
-
mode: sessionConfig.bashGuardMode,
|
|
118
|
-
source: sessionConfig.bashGuardSource,
|
|
119
|
-
surface: "tui" as const,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
if (mode === "resume") {
|
|
123
|
-
return createRuntimeSession({
|
|
124
|
-
...common,
|
|
125
|
-
selection: { mode, sessionId },
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return createRuntimeSession({
|
|
130
|
-
...common,
|
|
131
|
-
selection: { mode, sessionId },
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const projectionStore = new TuiProjectionStore({
|
|
136
|
-
sessionId: config.sessionId,
|
|
137
|
-
modelName: config.modelName,
|
|
138
|
-
workspaceRoot,
|
|
139
|
-
});
|
|
140
|
-
const initialSession = await createSessionForConfig(
|
|
141
|
-
config,
|
|
142
|
-
"new",
|
|
143
|
-
config.sessionId,
|
|
144
|
-
projectionStore,
|
|
145
|
-
);
|
|
40
|
+
connection = await createLocalTuiClient({ ...options, workspaceRoot });
|
|
146
41
|
const promptHistory = await PromptHistory.load(
|
|
147
42
|
await promptHistoryPath(workspaceRoot),
|
|
148
43
|
);
|
|
149
|
-
const catalog = new SessionCatalog({ workspaceRoot });
|
|
150
|
-
|
|
151
|
-
const openStoredSession = async (
|
|
152
|
-
sessionId: SessionId,
|
|
153
|
-
): Promise<ManagedTuiSessionBinding> => {
|
|
154
|
-
const deferred = new DeferredProjectionSink();
|
|
155
|
-
const summary = await catalog.get(sessionId);
|
|
156
|
-
const resumeConfig = deriveRunnerConfig(options.publicConfig, {
|
|
157
|
-
sessionId,
|
|
158
|
-
...(profiles === undefined
|
|
159
|
-
? {}
|
|
160
|
-
: { profileName: resolveSessionProfileName(profiles, summary) }),
|
|
161
|
-
});
|
|
162
|
-
const runtimeSession = await createSessionForConfig(
|
|
163
|
-
resumeConfig,
|
|
164
|
-
"resume",
|
|
165
|
-
sessionId,
|
|
166
|
-
deferred,
|
|
167
|
-
);
|
|
168
|
-
const targetProjection = new TuiProjectionStore({
|
|
169
|
-
sessionId,
|
|
170
|
-
modelName: resumeConfig.modelName,
|
|
171
|
-
workspaceRoot,
|
|
172
|
-
});
|
|
173
|
-
try {
|
|
174
|
-
targetProjection.hydrate(
|
|
175
|
-
await ResumeProjectionReader.read({
|
|
176
|
-
workspaceRoot,
|
|
177
|
-
sessionId,
|
|
178
|
-
modelName: resumeConfig.modelName,
|
|
179
|
-
}),
|
|
180
|
-
);
|
|
181
|
-
await deferred.attach(targetProjection);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
await runtimeSession
|
|
184
|
-
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
185
|
-
.catch(() => undefined);
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
return managedTuiBinding({
|
|
189
|
-
runtimeSession,
|
|
190
|
-
modelName: resumeConfig.modelName,
|
|
191
|
-
workspaceRoot,
|
|
192
|
-
profileName: resumeConfig.profileName,
|
|
193
|
-
projectionStore: targetProjection,
|
|
194
|
-
});
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const createNewSessionBinding = async (
|
|
198
|
-
sessionConfig: RunnerConfig,
|
|
199
|
-
): Promise<ManagedTuiSessionBinding> => {
|
|
200
|
-
const freshProjectionStore = new TuiProjectionStore({
|
|
201
|
-
sessionId: sessionConfig.sessionId,
|
|
202
|
-
modelName: sessionConfig.modelName,
|
|
203
|
-
workspaceRoot,
|
|
204
|
-
});
|
|
205
|
-
const runtimeSession = await createSessionForConfig(
|
|
206
|
-
sessionConfig,
|
|
207
|
-
"new",
|
|
208
|
-
sessionConfig.sessionId,
|
|
209
|
-
freshProjectionStore,
|
|
210
|
-
);
|
|
211
|
-
return managedTuiBinding({
|
|
212
|
-
runtimeSession,
|
|
213
|
-
modelName: sessionConfig.modelName,
|
|
214
|
-
workspaceRoot,
|
|
215
|
-
profileName: sessionConfig.profileName,
|
|
216
|
-
projectionStore: freshProjectionStore,
|
|
217
|
-
});
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const createSessionWithProfile = async (
|
|
221
|
-
profile: ModelProfile,
|
|
222
|
-
): Promise<ManagedTuiSessionBinding> => {
|
|
223
|
-
if (profiles === undefined) {
|
|
224
|
-
throw new Error("Model profiles are not configured.");
|
|
225
|
-
}
|
|
226
|
-
return createNewSessionBinding(
|
|
227
|
-
deriveRunnerConfig(options.publicConfig, {
|
|
228
|
-
sessionId: createUuidV7() as SessionId,
|
|
229
|
-
profileName: profile.name,
|
|
230
|
-
}),
|
|
231
|
-
);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const createFreshSession = async (
|
|
235
|
-
current: TuiSessionBinding,
|
|
236
|
-
): Promise<ManagedTuiSessionBinding> => {
|
|
237
|
-
const sessionId = createUuidV7() as SessionId;
|
|
238
|
-
if (profiles === undefined) {
|
|
239
|
-
return createNewSessionBinding(
|
|
240
|
-
deriveRunnerConfig(options.publicConfig, { sessionId }),
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
if (current.profileName === undefined) {
|
|
244
|
-
throw new Error("Current session does not have a model profile.");
|
|
245
|
-
}
|
|
246
|
-
return createNewSessionBinding(
|
|
247
|
-
deriveRunnerConfig(options.publicConfig, {
|
|
248
|
-
sessionId,
|
|
249
|
-
profileName: current.profileName,
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
controller = new DefaultTuiSessionController(
|
|
255
|
-
managedTuiBinding({
|
|
256
|
-
runtimeSession: initialSession,
|
|
257
|
-
modelName: config.modelName,
|
|
258
|
-
workspaceRoot,
|
|
259
|
-
profileName: config.profileName,
|
|
260
|
-
projectionStore,
|
|
261
|
-
}),
|
|
262
|
-
catalog,
|
|
263
|
-
openStoredSession,
|
|
264
|
-
createSessionWithProfile,
|
|
265
|
-
createFreshSession,
|
|
266
|
-
);
|
|
267
44
|
|
|
268
45
|
await shikiPreparation;
|
|
269
46
|
instance = render(
|
|
270
47
|
<App
|
|
271
|
-
sessionController={
|
|
48
|
+
sessionController={connection.client}
|
|
272
49
|
version={options.version}
|
|
273
50
|
readGitBranch={readCurrentGitBranch}
|
|
274
51
|
history={promptHistory}
|
|
@@ -295,13 +72,13 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
295
72
|
await instance.waitUntilExit();
|
|
296
73
|
} catch (error) {
|
|
297
74
|
primaryError = error;
|
|
298
|
-
|
|
75
|
+
closeReason = { type: "client_failed", error: errorMessage(error) };
|
|
299
76
|
} finally {
|
|
300
77
|
instance?.unmount();
|
|
301
78
|
restoreStdin();
|
|
302
|
-
if (
|
|
79
|
+
if (connection !== undefined) {
|
|
303
80
|
try {
|
|
304
|
-
await
|
|
81
|
+
await connection.close(closeReason);
|
|
305
82
|
} catch (error) {
|
|
306
83
|
primaryError =
|
|
307
84
|
primaryError === undefined
|
|
@@ -322,35 +99,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
322
99
|
}
|
|
323
100
|
}
|
|
324
101
|
|
|
325
|
-
class DeferredProjectionSink implements EventSink, AssistantTextDeltaSink {
|
|
326
|
-
readonly name = "deferred-tui-projection";
|
|
327
|
-
private target?: TuiProjectionStore;
|
|
328
|
-
private readonly buffered: AgentEvent[] = [];
|
|
329
|
-
|
|
330
|
-
async append(event: AgentEvent): Promise<void> {
|
|
331
|
-
if (this.target === undefined) {
|
|
332
|
-
this.buffered.push(event);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
await this.target.append(event);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
|
|
339
|
-
this.target?.updateAssistantTextDelta(update);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async attach(target: TuiProjectionStore): Promise<void> {
|
|
343
|
-
if (this.target !== undefined) {
|
|
344
|
-
throw new Error("Deferred projection sink is already attached.");
|
|
345
|
-
}
|
|
346
|
-
this.target = target;
|
|
347
|
-
for (const event of this.buffered) {
|
|
348
|
-
await target.append(event);
|
|
349
|
-
}
|
|
350
|
-
this.buffered.length = 0;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
102
|
function restoreStdin(): void {
|
|
355
103
|
if (process.stdin.isTTY) {
|
|
356
104
|
process.stdin.setRawMode(false);
|
package/src/cli/update-runner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertServiceUpgradeSafe } from "./service-upgrade";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { lstat, realpath } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -26,6 +27,7 @@ type NpmCommandInput = {
|
|
|
26
27
|
|
|
27
28
|
export type UpdateRunnerDependencies = {
|
|
28
29
|
readonly packageRoot: string;
|
|
30
|
+
readonly assertServiceUpgradeSafe: (packageRoot: string) => Promise<void>;
|
|
29
31
|
readonly npmCwd: string;
|
|
30
32
|
readonly runNpm: (input: NpmCommandInput) => Promise<NpmCommandResult>;
|
|
31
33
|
readonly canonicalizePath: (filePath: string) => Promise<string>;
|
|
@@ -41,6 +43,7 @@ type UpdateInput = {
|
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
const DEFAULT_DEPENDENCIES: UpdateRunnerDependencies = {
|
|
46
|
+
assertServiceUpgradeSafe,
|
|
44
47
|
packageRoot: path.resolve(fileURLToPath(new URL("../../", import.meta.url))),
|
|
45
48
|
npmCwd: tmpdir(),
|
|
46
49
|
runNpm,
|
|
@@ -116,6 +119,7 @@ export async function runUpdate(
|
|
|
116
119
|
return 0;
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
await dependencies.assertServiceUpgradeSafe(dependencies.packageRoot);
|
|
119
123
|
await writeCliOutput(input.stdout, `Updating to ${latestVersion}...\n`);
|
|
120
124
|
await runNpmChecked(
|
|
121
125
|
dependencies,
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RuntimeSession,
|
|
3
|
+
SessionDisposeReason,
|
|
4
|
+
ExecuteTurnInput,
|
|
5
|
+
} from "../agent/runtime-session";
|
|
6
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
7
|
+
import { createUuidV7 } from "../ids/uuid-v7";
|
|
8
|
+
import type { ClientModelProfile } from "./model-catalog";
|
|
9
|
+
import type { SessionCatalog, SessionSummary } from "../session/session-catalog";
|
|
10
|
+
import { readLastAssistantResponse } from "../session/session-last-response-reader";
|
|
11
|
+
import type {
|
|
12
|
+
ContextCompactionResult,
|
|
13
|
+
ContextRetirementResult,
|
|
14
|
+
} from "../context/context-manager";
|
|
15
|
+
import type { TurnUndoResult } from "../tools/turn-undo-manager";
|
|
16
|
+
import type { SessionClient, WorkspaceClient } from "./session-client";
|
|
17
|
+
|
|
18
|
+
export type OwnedSessionBinding<View> = {
|
|
19
|
+
client: SessionClient<View>;
|
|
20
|
+
runtimeSession: RuntimeSession;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class LocalWorkspaceClient<View> implements WorkspaceClient<View> {
|
|
24
|
+
private readonly listeners = new Set<() => void>();
|
|
25
|
+
private binding: OwnedSessionBinding<View>;
|
|
26
|
+
private operation?: Promise<unknown>;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
initial: OwnedSessionBinding<View>,
|
|
30
|
+
private readonly catalog: SessionCatalog,
|
|
31
|
+
private readonly openSession: (
|
|
32
|
+
sessionId: SessionId,
|
|
33
|
+
) => Promise<OwnedSessionBinding<View>>,
|
|
34
|
+
private readonly createSessionWithProfile: (
|
|
35
|
+
profile: ClientModelProfile,
|
|
36
|
+
) => Promise<OwnedSessionBinding<View>>,
|
|
37
|
+
private readonly createFreshSession: (
|
|
38
|
+
current: SessionClient<View>,
|
|
39
|
+
) => Promise<OwnedSessionBinding<View>>,
|
|
40
|
+
) {
|
|
41
|
+
this.binding = initial;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
readonly getBinding = (): SessionClient<View> => this.binding.client;
|
|
45
|
+
|
|
46
|
+
readonly subscribe = (listener: () => void): (() => void) => {
|
|
47
|
+
this.listeners.add(listener);
|
|
48
|
+
return () => {
|
|
49
|
+
this.listeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
listSessions(): Promise<readonly SessionSummary[]> {
|
|
54
|
+
return this.catalog.listAll(this.binding.client.sessionId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
compact(): Promise<ContextCompactionResult> {
|
|
58
|
+
return this.serialize(() => this.binding.runtimeSession.compactContext());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
retire(): Promise<ContextRetirementResult> {
|
|
62
|
+
return this.serialize(() => this.binding.runtimeSession.retireContext());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
undo(): Promise<TurnUndoResult> {
|
|
66
|
+
return this.serialize(() =>
|
|
67
|
+
this.binding.runtimeSession.undoLatestFileMutationTurn(),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fork(beforeCommit?: () => void): Promise<SessionId> {
|
|
72
|
+
return this.serialize(async () => {
|
|
73
|
+
const targetSessionId = createUuidV7() as SessionId;
|
|
74
|
+
await this.replaceSession(
|
|
75
|
+
"Cannot clone the session while a turn, context operation, or background task is active.",
|
|
76
|
+
async (current) => {
|
|
77
|
+
await current.runtimeSession.cloneSession(targetSessionId);
|
|
78
|
+
return this.openSession(targetSessionId);
|
|
79
|
+
},
|
|
80
|
+
beforeCommit,
|
|
81
|
+
);
|
|
82
|
+
return targetSessionId;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clear(beforeCommit?: () => void): Promise<void> {
|
|
87
|
+
return this.serialize(() =>
|
|
88
|
+
this.replaceSession(
|
|
89
|
+
"Cannot clear the session while a turn, context operation, or background task is active.",
|
|
90
|
+
(current) => this.createFreshSession(current.client),
|
|
91
|
+
beforeCommit,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
resume(sessionId: SessionId, beforeCommit?: () => void): Promise<void> {
|
|
97
|
+
return this.serialize(async () => {
|
|
98
|
+
if (sessionId === this.binding.client.sessionId) {
|
|
99
|
+
throw new Error(`Session ${sessionId} is already current.`);
|
|
100
|
+
}
|
|
101
|
+
await this.replaceSession(
|
|
102
|
+
"Cannot switch sessions while a turn or background task is active.",
|
|
103
|
+
() => this.openSession(sessionId),
|
|
104
|
+
beforeCommit,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
delete(sessionId: SessionId): Promise<void> {
|
|
110
|
+
return this.serialize(() =>
|
|
111
|
+
this.catalog.delete(sessionId, this.binding.client.sessionId),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switchModel(profile: ClientModelProfile, beforeCommit?: () => void): Promise<void> {
|
|
116
|
+
return this.serialize(() =>
|
|
117
|
+
this.replaceSession(
|
|
118
|
+
"Cannot switch models while a turn or background task is active.",
|
|
119
|
+
() => this.createSessionWithProfile(profile),
|
|
120
|
+
beforeCommit,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dispose(reason: SessionDisposeReason): Promise<void> {
|
|
126
|
+
return this.binding.runtimeSession.dispose(reason);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async replaceSession(
|
|
130
|
+
unavailableMessage: string,
|
|
131
|
+
createTarget: (
|
|
132
|
+
current: OwnedSessionBinding<View>,
|
|
133
|
+
) => Promise<OwnedSessionBinding<View>>,
|
|
134
|
+
beforeCommit?: () => void,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const current = this.binding;
|
|
137
|
+
if (!current.runtimeSession.canSwitchSession()) {
|
|
138
|
+
throw new Error(unavailableMessage);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const target = await createTarget(current);
|
|
142
|
+
try {
|
|
143
|
+
await current.runtimeSession.dispose({ type: "session_switch" });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await target.runtimeSession
|
|
146
|
+
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
147
|
+
.catch(() => undefined);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
beforeCommit?.();
|
|
151
|
+
this.binding = target;
|
|
152
|
+
for (const listener of this.listeners) {
|
|
153
|
+
listener();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private serialize<T>(operation: () => Promise<T>): Promise<T> {
|
|
158
|
+
if (this.operation !== undefined) {
|
|
159
|
+
return Promise.reject(new Error("Another session operation is already running."));
|
|
160
|
+
}
|
|
161
|
+
const pending = operation().finally(() => {
|
|
162
|
+
if (this.operation === pending) {
|
|
163
|
+
this.operation = undefined;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this.operation = pending;
|
|
167
|
+
return pending;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createLocalSessionBinding<View>(input: {
|
|
172
|
+
runtimeSession: RuntimeSession;
|
|
173
|
+
modelName: string;
|
|
174
|
+
workspaceRoot: string;
|
|
175
|
+
profileName?: string;
|
|
176
|
+
projectionStore: View;
|
|
177
|
+
}): OwnedSessionBinding<View> {
|
|
178
|
+
return {
|
|
179
|
+
runtimeSession: input.runtimeSession,
|
|
180
|
+
client: {
|
|
181
|
+
sessionId: input.runtimeSession.sessionId,
|
|
182
|
+
modelName: input.modelName,
|
|
183
|
+
workspaceRoot: input.workspaceRoot,
|
|
184
|
+
profileName: input.profileName,
|
|
185
|
+
projectionStore: input.projectionStore,
|
|
186
|
+
readLastResponse: () =>
|
|
187
|
+
readLastAssistantResponse({
|
|
188
|
+
workspaceRoot: input.workspaceRoot,
|
|
189
|
+
sessionId: input.runtimeSession.sessionId,
|
|
190
|
+
}),
|
|
191
|
+
skills: () => input.runtimeSession.skills(),
|
|
192
|
+
mcp: () => input.runtimeSession.mcp(),
|
|
193
|
+
reasoningEffort: () => input.runtimeSession.reasoningEffort(),
|
|
194
|
+
setReasoningEffort: async (effort) =>
|
|
195
|
+
input.runtimeSession.setReasoningEffort(effort),
|
|
196
|
+
resetReasoningEffort: async () => input.runtimeSession.resetReasoningEffort(),
|
|
197
|
+
supportsImageInput: () => input.runtimeSession.supportsImageInput(),
|
|
198
|
+
importImage: (sourcePath, signal, prospectiveMessageImageCount) =>
|
|
199
|
+
input.runtimeSession.importImage(
|
|
200
|
+
sourcePath,
|
|
201
|
+
signal,
|
|
202
|
+
prospectiveMessageImageCount,
|
|
203
|
+
),
|
|
204
|
+
verifyImageAssets: (assets, signal) =>
|
|
205
|
+
input.runtimeSession.verifyImageAssets(assets, signal),
|
|
206
|
+
admitTurn: (userMessage, signal) =>
|
|
207
|
+
input.runtimeSession.admitTurn({ userMessage, signal }),
|
|
208
|
+
executeTurn: (userMessage, signal) =>
|
|
209
|
+
input.runtimeSession.executeTurn({
|
|
210
|
+
userMessage,
|
|
211
|
+
signal,
|
|
212
|
+
} satisfies ExecuteTurnInput),
|
|
213
|
+
promptScheduler: () => input.runtimeSession.promptScheduler(),
|
|
214
|
+
subscribePromptScheduler: (listener) =>
|
|
215
|
+
input.runtimeSession.subscribePromptScheduler(listener),
|
|
216
|
+
queueFollowUp: async (userMessage) =>
|
|
217
|
+
input.runtimeSession.queueFollowUp(userMessage),
|
|
218
|
+
bashGuard: () => input.runtimeSession.bashGuard(),
|
|
219
|
+
subscribeBashGuard: (listener) =>
|
|
220
|
+
input.runtimeSession.subscribeBashGuard(listener),
|
|
221
|
+
setYoloMode: async (enabled) => input.runtimeSession.setYoloMode(enabled),
|
|
222
|
+
resolveBashConfirmation: (decision) =>
|
|
223
|
+
input.runtimeSession.resolveBashConfirmation(decision),
|
|
224
|
+
providerRetry: () => input.runtimeSession.providerRetry(),
|
|
225
|
+
subscribeProviderRetry: (listener) =>
|
|
226
|
+
input.runtimeSession.subscribeProviderRetry(listener),
|
|
227
|
+
resolveProviderRetry: (requestId, decision) =>
|
|
228
|
+
input.runtimeSession.resolveProviderRetry(requestId, decision),
|
|
229
|
+
askUser: () => input.runtimeSession.askUser(),
|
|
230
|
+
subscribeAskUser: (listener) => input.runtimeSession.subscribeAskUser(listener),
|
|
231
|
+
resolveAskUser: (response) => input.runtimeSession.resolveAskUser(response),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function errorMessage(error: unknown): string {
|
|
237
|
+
return error instanceof Error ? error.message : String(error);
|
|
238
|
+
}
|