@openma/deepseek-harness-acp 0.4.5 → 0.4.7
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 +146 -108
- package/assets/icon.svg +8 -0
- package/cordis.patch.yml +7 -1
- package/dist/bridge.js +1467 -285
- package/dist/index.js +162 -2214
- package/package.json +6 -4
package/dist/bridge.js
CHANGED
|
@@ -22,7 +22,6 @@ import { createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
|
|
|
22
22
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
23
23
|
import { foldSessionTitle } from "@deepseek-ai/dsh-session-title";
|
|
24
24
|
import { SANDBOX_MODES, setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
|
|
25
|
-
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
26
25
|
import * as agentModule from "@deepseek-ai/dsh-agent";
|
|
27
26
|
import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
|
|
28
27
|
function selfHarness() {
|
|
@@ -35,7 +34,6 @@ function selfHarness() {
|
|
|
35
34
|
foldSessionTitle,
|
|
36
35
|
setSandboxMode,
|
|
37
36
|
sandboxModes: SANDBOX_MODES,
|
|
38
|
-
credentialRef,
|
|
39
37
|
...installModelSelection !== void 0 ? { installModelSelection } : {},
|
|
40
38
|
mcpClient
|
|
41
39
|
};
|
|
@@ -56,11 +54,245 @@ import {
|
|
|
56
54
|
PROTOCOL_VERSION,
|
|
57
55
|
RequestError
|
|
58
56
|
} from "@agentclientprotocol/sdk";
|
|
57
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
59
58
|
|
|
60
59
|
// src/version.ts
|
|
61
60
|
import { createRequire } from "node:module";
|
|
62
61
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
63
62
|
|
|
63
|
+
// src/auth.ts
|
|
64
|
+
var LOGIN_DESCRIPTION = "Save an API key to the harness credential store shared with the dsh Web UI";
|
|
65
|
+
function isDeepseekRoute(provider) {
|
|
66
|
+
return provider === void 0 || provider === "deepseek-official" || provider === "deepseek";
|
|
67
|
+
}
|
|
68
|
+
function credentialEnvNames(provider) {
|
|
69
|
+
if (provider === void 0 || isDeepseekRoute(provider)) return ["DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL"];
|
|
70
|
+
if (provider === "anthropic") return ["ANTHROPIC_API_KEY"];
|
|
71
|
+
if (provider === "openai" || provider.startsWith("openai")) return ["OPENAI_API_KEY"];
|
|
72
|
+
return [`${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_API_KEY`];
|
|
73
|
+
}
|
|
74
|
+
function primaryCredentialName(provider) {
|
|
75
|
+
return credentialEnvNames(provider)[0] ?? "DEEPSEEK_API_KEY";
|
|
76
|
+
}
|
|
77
|
+
function providerFromAuthMethodId(methodId) {
|
|
78
|
+
if (!methodId) return void 0;
|
|
79
|
+
for (const prefix of ["api-key:", "terminal-login:"]) {
|
|
80
|
+
if (methodId.startsWith(prefix)) return methodId.slice(prefix.length);
|
|
81
|
+
}
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
function apiKeyFromAuthenticate(params) {
|
|
85
|
+
const meta = params._meta;
|
|
86
|
+
const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta["api-key"] : void 0;
|
|
87
|
+
const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
|
|
88
|
+
const key = typeof record?.apiKey === "string" ? record.apiKey.trim() : void 0;
|
|
89
|
+
const providerFromMeta = typeof record?.provider === "string" ? record.provider : void 0;
|
|
90
|
+
const provider = providerFromMeta || providerFromAuthMethodId(params.methodId);
|
|
91
|
+
return {
|
|
92
|
+
...provider && provider.length > 0 ? { provider } : {},
|
|
93
|
+
...key && key.length > 0 ? { key } : {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function gatewayFromAuthenticate(params) {
|
|
97
|
+
const meta = params._meta;
|
|
98
|
+
const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.gateway : void 0;
|
|
99
|
+
const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
|
|
100
|
+
const baseUrl = typeof record?.baseUrl === "string" ? record.baseUrl.trim() : void 0;
|
|
101
|
+
const headers = stringRecord(record?.headers);
|
|
102
|
+
const providerName = typeof record?.providerName === "string" ? record.providerName.trim() : void 0;
|
|
103
|
+
const key = apiKeyFromHeaders(headers);
|
|
104
|
+
return {
|
|
105
|
+
...baseUrl && baseUrl.length > 0 ? { baseUrl } : {},
|
|
106
|
+
...headers ? { headers } : {},
|
|
107
|
+
...providerName && providerName.length > 0 ? { providerName } : {},
|
|
108
|
+
...key && key.length > 0 ? { key } : {}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function isBrowserAuthMethod(methodId) {
|
|
112
|
+
return methodId === "browser";
|
|
113
|
+
}
|
|
114
|
+
function isGatewayAuthMethod(methodId) {
|
|
115
|
+
return methodId === "gateway";
|
|
116
|
+
}
|
|
117
|
+
function credentialBaseUrlName(provider) {
|
|
118
|
+
if (isDeepseekRoute(provider) || provider === void 0) return "DEEPSEEK_BASE_URL";
|
|
119
|
+
if (provider === "anthropic") return "ANTHROPIC_BASE_URL";
|
|
120
|
+
if (provider === "openai" || provider.startsWith("openai")) return "OPENAI_BASE_URL";
|
|
121
|
+
return `${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_BASE_URL`;
|
|
122
|
+
}
|
|
123
|
+
function advertisedAuthMethods(providers, clientCapabilities, env = process.env) {
|
|
124
|
+
const methods = [...apiKeyAuthMethods(providers)];
|
|
125
|
+
if (shouldOfferLocalAuthPage(env)) methods.push(browserAuthMethod());
|
|
126
|
+
if (clientCapabilities?.auth?._meta?.["gateway"] === true) methods.push(gatewayAuthMethod());
|
|
127
|
+
return methods;
|
|
128
|
+
}
|
|
129
|
+
function apiKeyAuthMethods(providers) {
|
|
130
|
+
if (providers.length <= 1) {
|
|
131
|
+
const provider = providers[0];
|
|
132
|
+
return [apiKeyAuthMethod(provider?.id, provider?.name, providers.length === 0)];
|
|
133
|
+
}
|
|
134
|
+
return providers.map((provider) => apiKeyAuthMethod(provider.id, provider.name, false));
|
|
135
|
+
}
|
|
136
|
+
function apiKeyAuthMethod(provider, name3, unscoped) {
|
|
137
|
+
const scoped = !unscoped && provider !== void 0;
|
|
138
|
+
return {
|
|
139
|
+
id: scoped ? `api-key:${provider}` : "api-key",
|
|
140
|
+
name: scoped ? `${name3 ?? provider} API key` : "API Key",
|
|
141
|
+
description: LOGIN_DESCRIPTION,
|
|
142
|
+
_meta: { "api-key": scoped ? { provider } : {} }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function shouldOfferLocalAuthPage(env = process.env) {
|
|
146
|
+
if (env["NO_BROWSER"]) return false;
|
|
147
|
+
if (env["NODE_ENV"] === "test" || env["CI"] === "true" || env["CI"] === "1") return false;
|
|
148
|
+
if (env["DSH_ACP_AUTH_PAGE"] === "0") return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function browserAuthMethod() {
|
|
152
|
+
return {
|
|
153
|
+
id: "browser",
|
|
154
|
+
name: "Browser",
|
|
155
|
+
description: "Open a local sign-in page to save an API key. The secret never travels over ACP."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function gatewayAuthMethod() {
|
|
159
|
+
return {
|
|
160
|
+
id: "gateway",
|
|
161
|
+
name: "Custom model gateway",
|
|
162
|
+
description: "Use a custom OpenAI-compatible gateway",
|
|
163
|
+
_meta: { gateway: { protocol: "openai", restartRequired: "false" } }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function stringRecord(value) {
|
|
167
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
168
|
+
const entries = Object.entries(value).filter(
|
|
169
|
+
(entry) => typeof entry[1] === "string" && entry[1].trim().length > 0
|
|
170
|
+
);
|
|
171
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
172
|
+
}
|
|
173
|
+
function apiKeyFromHeaders(headers) {
|
|
174
|
+
if (!headers) return void 0;
|
|
175
|
+
const authorization = headers["Authorization"] ?? headers["authorization"];
|
|
176
|
+
if (authorization) {
|
|
177
|
+
const bearer = authorization.match(/^Bearer\s+(.+)$/i);
|
|
178
|
+
const token = (bearer?.[1] ?? authorization).trim();
|
|
179
|
+
if (token.length > 0) return token;
|
|
180
|
+
}
|
|
181
|
+
const first = Object.values(headers).find((value) => value.trim().length > 0);
|
|
182
|
+
return first?.trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/auth-page.ts
|
|
186
|
+
import { createServer } from "node:http";
|
|
187
|
+
import { execFile } from "node:child_process";
|
|
188
|
+
function startLocalAuthPage(options) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const server = createServer((request, response) => {
|
|
191
|
+
void handle(request, response);
|
|
192
|
+
});
|
|
193
|
+
let settle;
|
|
194
|
+
let fail;
|
|
195
|
+
const completed = new Promise((res, rej) => {
|
|
196
|
+
settle = res;
|
|
197
|
+
fail = rej;
|
|
198
|
+
});
|
|
199
|
+
const timeoutMs = options.timeoutMs ?? 10 * 6e4;
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
server.close();
|
|
202
|
+
fail?.(new Error("local auth page timed out"));
|
|
203
|
+
}, timeoutMs);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
const handle = async (request, response) => {
|
|
206
|
+
if (request.method === "GET" || request.method === void 0) {
|
|
207
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
208
|
+
response.end(authPageHtml(options.credentialName));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (request.method === "POST") {
|
|
212
|
+
const body = await readBody(request);
|
|
213
|
+
const key = new URLSearchParams(body).get("api-key")?.trim() ?? "";
|
|
214
|
+
if (key.length === 0) {
|
|
215
|
+
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
216
|
+
response.end(authPageHtml(options.credentialName, "Enter an API key."));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
220
|
+
response.end(
|
|
221
|
+
"<!doctype html><html><body><p>Saved. You can return to the client.</p></body></html>"
|
|
222
|
+
);
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
server.close();
|
|
225
|
+
settle?.(key);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
response.writeHead(405);
|
|
229
|
+
response.end();
|
|
230
|
+
};
|
|
231
|
+
server.once("error", (error) => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
reject(error);
|
|
234
|
+
});
|
|
235
|
+
server.listen(0, "127.0.0.1", () => {
|
|
236
|
+
const address = server.address();
|
|
237
|
+
if (!address || typeof address === "string") {
|
|
238
|
+
server.close();
|
|
239
|
+
reject(new Error("local auth page failed to bind"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const url = `http://127.0.0.1:${address.port}/`;
|
|
243
|
+
resolve({
|
|
244
|
+
url,
|
|
245
|
+
close: () => {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
server.close();
|
|
248
|
+
},
|
|
249
|
+
completed
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function openLocalAuthPage(url) {
|
|
255
|
+
const platform = process.platform;
|
|
256
|
+
if (platform === "darwin") {
|
|
257
|
+
execFile("open", [url], () => void 0);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (platform === "win32") {
|
|
261
|
+
execFile("cmd", ["/c", "start", "", url], () => void 0);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
execFile("xdg-open", [url], () => void 0);
|
|
265
|
+
}
|
|
266
|
+
function authPageHtml(credentialName, error) {
|
|
267
|
+
const message = error ? `<p>${escapeHtml(error)}</p>` : "";
|
|
268
|
+
return `<!doctype html>
|
|
269
|
+
<html lang="en">
|
|
270
|
+
<head><meta charset="utf-8"><title>DeepSeek Harness sign in</title></head>
|
|
271
|
+
<body>
|
|
272
|
+
<h1>Save API key</h1>
|
|
273
|
+
<p>Stored as ${escapeHtml(credentialName)} in the harness credential store.</p>
|
|
274
|
+
${message}
|
|
275
|
+
<form method="post">
|
|
276
|
+
<label>API key <input type="password" name="api-key" autocomplete="off" required></label>
|
|
277
|
+
<button type="submit">Save</button>
|
|
278
|
+
</form>
|
|
279
|
+
</body>
|
|
280
|
+
</html>`;
|
|
281
|
+
}
|
|
282
|
+
function escapeHtml(value) {
|
|
283
|
+
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char] ?? char);
|
|
284
|
+
}
|
|
285
|
+
function readBody(request) {
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
request.on("data", (chunk) => {
|
|
289
|
+
chunks.push(Buffer.from(chunk));
|
|
290
|
+
});
|
|
291
|
+
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
292
|
+
request.on("error", reject);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
64
296
|
// src/log.ts
|
|
65
297
|
var PREFIX = "[dsh-acp]";
|
|
66
298
|
function logWarn(message) {
|
|
@@ -212,7 +444,7 @@ function extractMessageTexts(message) {
|
|
|
212
444
|
}
|
|
213
445
|
function planEntries(data) {
|
|
214
446
|
const todos = data["todos"];
|
|
215
|
-
if (!Array.isArray(todos)) return
|
|
447
|
+
if (!Array.isArray(todos)) return void 0;
|
|
216
448
|
const entries = [];
|
|
217
449
|
for (const todo of todos) {
|
|
218
450
|
if (todo === null || typeof todo !== "object") continue;
|
|
@@ -255,6 +487,27 @@ function emptyWindow() {
|
|
|
255
487
|
error: void 0
|
|
256
488
|
};
|
|
257
489
|
}
|
|
490
|
+
var TOOL_KINDS = /* @__PURE__ */ new Set([
|
|
491
|
+
"read",
|
|
492
|
+
"edit",
|
|
493
|
+
"delete",
|
|
494
|
+
"move",
|
|
495
|
+
"search",
|
|
496
|
+
"execute",
|
|
497
|
+
"think",
|
|
498
|
+
"fetch",
|
|
499
|
+
"switch_mode",
|
|
500
|
+
"other"
|
|
501
|
+
]);
|
|
502
|
+
function factsFromPresentCall(name3, rawArguments, presented) {
|
|
503
|
+
const fallback = classifyToolCall(name3, rawArguments);
|
|
504
|
+
const kind = presented.kind !== void 0 && TOOL_KINDS.has(presented.kind) ? presented.kind : fallback.kind;
|
|
505
|
+
const title = presented.title !== void 0 && presented.title.length > 0 ? presented.title : fallback.title;
|
|
506
|
+
const locations = presented.locations !== void 0 && presented.locations.length > 0 ? presented.locations.map(
|
|
507
|
+
(location) => location.line === void 0 ? { path: location.path } : { path: location.path, line: location.line }
|
|
508
|
+
) : fallback.locations;
|
|
509
|
+
return { kind, title, locations, rawInput: fallback.rawInput };
|
|
510
|
+
}
|
|
258
511
|
var SessionProjection = class {
|
|
259
512
|
/** Context capacity from the latest `request/context` event, when known. */
|
|
260
513
|
contextWindow;
|
|
@@ -269,10 +522,14 @@ var SessionProjection = class {
|
|
|
269
522
|
terminalOutput;
|
|
270
523
|
/** Session working directory, advertised on display terminals. */
|
|
271
524
|
cwd;
|
|
525
|
+
presentCall;
|
|
526
|
+
subagent;
|
|
272
527
|
constructor(contextWindow, options) {
|
|
273
528
|
this.contextWindow = contextWindow;
|
|
274
529
|
this.terminalOutput = options?.terminalOutput ?? false;
|
|
275
530
|
this.cwd = options?.cwd;
|
|
531
|
+
this.presentCall = options?.presentCall;
|
|
532
|
+
this.subagent = options?.subagent;
|
|
276
533
|
}
|
|
277
534
|
/** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
|
|
278
535
|
beginPrompt() {
|
|
@@ -307,6 +564,11 @@ var SessionProjection = class {
|
|
|
307
564
|
* Project one session event onto zero or more ACP updates.
|
|
308
565
|
*/
|
|
309
566
|
onEvent(event) {
|
|
567
|
+
const updates = this.projectEvent(event);
|
|
568
|
+
return this.subagent === void 0 ? updates : updates.map((update) => this.withSubagentAttribution(update));
|
|
569
|
+
}
|
|
570
|
+
/** Project one event before optional subagent attribution is attached. */
|
|
571
|
+
projectEvent(event) {
|
|
310
572
|
const data = event.data ?? {};
|
|
311
573
|
switch (event.type) {
|
|
312
574
|
case "assistant/chunk":
|
|
@@ -317,9 +579,34 @@ var SessionProjection = class {
|
|
|
317
579
|
return this.onToolCall(data);
|
|
318
580
|
case "tool/result":
|
|
319
581
|
return this.onToolResult(data);
|
|
582
|
+
case "subagent/start":
|
|
583
|
+
return this.onSubagentStart(data);
|
|
584
|
+
case "subagent/end":
|
|
585
|
+
return this.onSubagentEnd(data);
|
|
320
586
|
case "todo/write": {
|
|
321
587
|
const entries = planEntries(data);
|
|
322
|
-
return entries
|
|
588
|
+
return entries !== void 0 ? [{ sessionUpdate: "plan", entries }] : [];
|
|
589
|
+
}
|
|
590
|
+
case "user/message": {
|
|
591
|
+
const source = data["source"];
|
|
592
|
+
const kind = source !== null && typeof source === "object" ? asString(source["kind"]) : void 0;
|
|
593
|
+
if (kind === void 0 || kind === "user") return [];
|
|
594
|
+
const content = data["content"];
|
|
595
|
+
const text = Array.isArray(content) ? content.filter(
|
|
596
|
+
(block) => block !== null && typeof block === "object" && block["type"] === "text" && typeof block["text"] === "string"
|
|
597
|
+
).map((block) => block["text"]).join("") : "";
|
|
598
|
+
return [
|
|
599
|
+
{
|
|
600
|
+
sessionUpdate: "session_info_update",
|
|
601
|
+
_meta: {
|
|
602
|
+
dsh: {
|
|
603
|
+
event: "user/message",
|
|
604
|
+
source: kind,
|
|
605
|
+
preview: Array.from(text).slice(0, 160).join("")
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
];
|
|
323
610
|
}
|
|
324
611
|
case "turn/end":
|
|
325
612
|
return this.onTurnEnd(data);
|
|
@@ -341,31 +628,117 @@ var SessionProjection = class {
|
|
|
341
628
|
stepKey(data) {
|
|
342
629
|
return `${String(data["turn"])}:${String(data["step"])}`;
|
|
343
630
|
}
|
|
631
|
+
withSubagentAttribution(update) {
|
|
632
|
+
const value = update;
|
|
633
|
+
const meta = value["_meta"] !== null && typeof value["_meta"] === "object" ? value["_meta"] : {};
|
|
634
|
+
const dsh = meta["dsh"] !== null && typeof meta["dsh"] === "object" ? meta["dsh"] : {};
|
|
635
|
+
return {
|
|
636
|
+
...value,
|
|
637
|
+
_meta: { ...meta, dsh: { ...dsh, subagent: this.subagent } }
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
subagentMeta(data, state) {
|
|
641
|
+
const runId = asString(data["runId"]);
|
|
642
|
+
const childSessionId = asString(data["id"]);
|
|
643
|
+
const provider = asString(data["provider"]);
|
|
644
|
+
if (runId === void 0 || childSessionId === void 0 || provider === void 0) return void 0;
|
|
645
|
+
const parentToolCallId = asString(data["parentToolCallId"]);
|
|
646
|
+
return {
|
|
647
|
+
state,
|
|
648
|
+
runId,
|
|
649
|
+
childSessionId,
|
|
650
|
+
provider,
|
|
651
|
+
local: data["local"] === true,
|
|
652
|
+
...parentToolCallId !== void 0 ? { parentToolCallId } : {}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
onSubagentStart(data) {
|
|
656
|
+
const subagent = this.subagentMeta(data, "started");
|
|
657
|
+
if (subagent === void 0) return [];
|
|
658
|
+
const runId = subagent["runId"];
|
|
659
|
+
const childSessionId = subagent["childSessionId"];
|
|
660
|
+
return [{
|
|
661
|
+
sessionUpdate: "tool_call",
|
|
662
|
+
toolCallId: `subagent:${runId}`,
|
|
663
|
+
title: `Start subagent ${childSessionId}`,
|
|
664
|
+
kind: "other",
|
|
665
|
+
status: "in_progress",
|
|
666
|
+
rawInput: {
|
|
667
|
+
childSessionId,
|
|
668
|
+
provider: subagent["provider"],
|
|
669
|
+
local: subagent["local"]
|
|
670
|
+
},
|
|
671
|
+
_meta: { dsh: { subagent } }
|
|
672
|
+
}];
|
|
673
|
+
}
|
|
674
|
+
onSubagentEnd(data) {
|
|
675
|
+
const subagent = this.subagentMeta(data, "finished");
|
|
676
|
+
if (subagent === void 0) return [];
|
|
677
|
+
const stopReason = asString(data["stopReason"]) ?? "error";
|
|
678
|
+
const lastAssistantMessage = data["lastAssistantMessage"];
|
|
679
|
+
return [{
|
|
680
|
+
sessionUpdate: "tool_call_update",
|
|
681
|
+
toolCallId: `subagent:${String(subagent["runId"])}`,
|
|
682
|
+
status: stopReason === "completed" ? "completed" : "failed",
|
|
683
|
+
rawOutput: {
|
|
684
|
+
stopReason,
|
|
685
|
+
...Array.isArray(lastAssistantMessage) ? { lastAssistantMessage } : {}
|
|
686
|
+
},
|
|
687
|
+
_meta: { dsh: { subagent } }
|
|
688
|
+
}];
|
|
689
|
+
}
|
|
344
690
|
onChunk(data) {
|
|
345
691
|
const chunk = data["chunk"];
|
|
346
692
|
if (chunk === null || typeof chunk === "undefined" || typeof chunk !== "object") return [];
|
|
347
693
|
const c = chunk;
|
|
348
694
|
const text = c["text"];
|
|
349
695
|
if (typeof text !== "string" || text.length === 0) return [];
|
|
696
|
+
const messageId = this.stepKey(data);
|
|
350
697
|
if (c["type"] === "text-delta") {
|
|
351
|
-
this.streamedText.add(
|
|
352
|
-
return [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }];
|
|
698
|
+
this.streamedText.add(messageId);
|
|
699
|
+
return [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text }, messageId }];
|
|
353
700
|
}
|
|
354
701
|
if (c["type"] === "reasoning-delta") {
|
|
355
|
-
this.streamedReasoning.add(
|
|
356
|
-
return [{ sessionUpdate: "agent_thought_chunk", content: { type: "text", text } }];
|
|
702
|
+
this.streamedReasoning.add(messageId);
|
|
703
|
+
return [{ sessionUpdate: "agent_thought_chunk", content: { type: "text", text }, messageId }];
|
|
357
704
|
}
|
|
358
705
|
return [];
|
|
359
706
|
}
|
|
360
707
|
onAssistantMessage(data) {
|
|
361
708
|
const updates = [];
|
|
362
709
|
const key = this.stepKey(data);
|
|
363
|
-
const
|
|
710
|
+
const message = data["message"];
|
|
711
|
+
const { text, reasoning } = extractMessageTexts(message);
|
|
712
|
+
const model = message !== null && typeof message === "object" ? asString(
|
|
713
|
+
message["source"] !== null && typeof message["source"] === "object" ? message["source"]["model"] : void 0
|
|
714
|
+
) : void 0;
|
|
715
|
+
const completionMeta = {
|
|
716
|
+
dsh: {
|
|
717
|
+
event: "assistant_message",
|
|
718
|
+
...model !== void 0 ? { model } : {}
|
|
719
|
+
}
|
|
720
|
+
};
|
|
364
721
|
if (reasoning.length > 0 && !this.streamedReasoning.has(key)) {
|
|
365
|
-
updates.push({
|
|
722
|
+
updates.push({
|
|
723
|
+
sessionUpdate: "agent_thought_chunk",
|
|
724
|
+
content: { type: "text", text: reasoning },
|
|
725
|
+
messageId: key
|
|
726
|
+
});
|
|
366
727
|
}
|
|
367
728
|
if (text.length > 0 && !this.streamedText.has(key)) {
|
|
368
|
-
updates.push({
|
|
729
|
+
updates.push({
|
|
730
|
+
sessionUpdate: "agent_message_chunk",
|
|
731
|
+
content: { type: "text", text },
|
|
732
|
+
messageId: key,
|
|
733
|
+
_meta: completionMeta
|
|
734
|
+
});
|
|
735
|
+
} else {
|
|
736
|
+
updates.push({
|
|
737
|
+
sessionUpdate: "agent_message_chunk",
|
|
738
|
+
content: { type: "text", text: "" },
|
|
739
|
+
messageId: key,
|
|
740
|
+
_meta: completionMeta
|
|
741
|
+
});
|
|
369
742
|
}
|
|
370
743
|
this.streamedText.delete(key);
|
|
371
744
|
this.streamedReasoning.delete(key);
|
|
@@ -399,7 +772,14 @@ var SessionProjection = class {
|
|
|
399
772
|
const name3 = asString(data["name"]) ?? "tool";
|
|
400
773
|
if (callId === void 0) return [];
|
|
401
774
|
const rawArguments = typeof data["arguments"] === "string" ? data["arguments"] : "{}";
|
|
402
|
-
|
|
775
|
+
let parsedArgs = {};
|
|
776
|
+
try {
|
|
777
|
+
parsedArgs = JSON.parse(rawArguments);
|
|
778
|
+
} catch {
|
|
779
|
+
parsedArgs = { arguments: rawArguments };
|
|
780
|
+
}
|
|
781
|
+
const presented = this.presentCall?.(name3, parsedArgs);
|
|
782
|
+
const facts = presented !== void 0 ? factsFromPresentCall(name3, rawArguments, presented) : classifyToolCall(name3, rawArguments);
|
|
403
783
|
this.toolCalls.set(callId, facts);
|
|
404
784
|
const displayTerminal = this.terminalOutput && facts.kind === "execute";
|
|
405
785
|
return [
|
|
@@ -504,7 +884,7 @@ ${text.replace(/\n+$/, "")}
|
|
|
504
884
|
};
|
|
505
885
|
|
|
506
886
|
// src/bridge/history.ts
|
|
507
|
-
function
|
|
887
|
+
function userMessage(data) {
|
|
508
888
|
if (data === void 0) return void 0;
|
|
509
889
|
const source = data["source"];
|
|
510
890
|
const kind = source !== null && typeof source === "object" ? source["kind"] : void 0;
|
|
@@ -519,20 +899,28 @@ function userText(data) {
|
|
|
519
899
|
}
|
|
520
900
|
}
|
|
521
901
|
const text = parts.join("");
|
|
522
|
-
return text.length > 0 ? text :
|
|
902
|
+
return text.length > 0 ? { text } : {};
|
|
523
903
|
}
|
|
524
904
|
function buildReplay(events) {
|
|
525
905
|
const projection = new SessionProjection();
|
|
526
906
|
const updates = [];
|
|
527
907
|
let plan;
|
|
528
908
|
let title;
|
|
909
|
+
let sawUserInteraction = false;
|
|
529
910
|
for (const event of events) {
|
|
530
911
|
if (event.type === "user/message") {
|
|
531
|
-
const
|
|
532
|
-
if (
|
|
533
|
-
|
|
912
|
+
const user = userMessage(event.data);
|
|
913
|
+
if (user !== void 0) {
|
|
914
|
+
projection.beginPrompt();
|
|
915
|
+
sawUserInteraction = true;
|
|
916
|
+
if (user.text !== void 0) {
|
|
917
|
+
updates.push({
|
|
918
|
+
sessionUpdate: "user_message_chunk",
|
|
919
|
+
content: { type: "text", text: user.text }
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
continue;
|
|
534
923
|
}
|
|
535
|
-
continue;
|
|
536
924
|
}
|
|
537
925
|
if (event.type === "session/title") {
|
|
538
926
|
const t = event.data?.["title"];
|
|
@@ -549,25 +937,132 @@ function buildReplay(events) {
|
|
|
549
937
|
}
|
|
550
938
|
}
|
|
551
939
|
if (plan !== void 0) updates.push(plan);
|
|
940
|
+
const usage = sawUserInteraction ? projection.promptUsage() : void 0;
|
|
941
|
+
if (usage !== void 0) {
|
|
942
|
+
updates.push({
|
|
943
|
+
sessionUpdate: "session_info_update",
|
|
944
|
+
_meta: { dsh: { event: "prompt/usage", usage } }
|
|
945
|
+
});
|
|
946
|
+
}
|
|
552
947
|
return { updates, title, contextWindow: projection.contextWindow };
|
|
553
948
|
}
|
|
554
949
|
|
|
555
950
|
// src/bridge/prompt.ts
|
|
951
|
+
function deliverPrompt(target, message, running) {
|
|
952
|
+
if (running) {
|
|
953
|
+
target.steer(message);
|
|
954
|
+
return "steer";
|
|
955
|
+
}
|
|
956
|
+
target.followup(message);
|
|
957
|
+
return "followup";
|
|
958
|
+
}
|
|
556
959
|
var UnsupportedPromptContentError = class extends Error {
|
|
557
960
|
constructor(contentType) {
|
|
558
961
|
super(`unsupported prompt content type: ${contentType}`);
|
|
559
962
|
this.contentType = contentType;
|
|
963
|
+
this.name = "UnsupportedPromptContentError";
|
|
560
964
|
}
|
|
561
965
|
contentType;
|
|
562
966
|
};
|
|
967
|
+
var PromptImageError = class extends Error {
|
|
968
|
+
constructor(message, options) {
|
|
969
|
+
super(message, options);
|
|
970
|
+
this.name = "PromptImageError";
|
|
971
|
+
}
|
|
972
|
+
};
|
|
563
973
|
function contextBlock(uri, text) {
|
|
564
974
|
return `<context ref=${JSON.stringify(uri)}>
|
|
565
975
|
${text}
|
|
566
976
|
</context>`;
|
|
567
977
|
}
|
|
568
|
-
|
|
978
|
+
var IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
979
|
+
function canonicalImageMediaType(mimeType) {
|
|
980
|
+
const lower = mimeType.trim().toLowerCase();
|
|
981
|
+
const mapped = lower === "image/jpg" ? "image/jpeg" : lower;
|
|
982
|
+
return IMAGE_MEDIA_TYPES.has(mapped) ? mapped : void 0;
|
|
983
|
+
}
|
|
984
|
+
function attachmentIngestOf(value) {
|
|
985
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
986
|
+
const candidate = value;
|
|
987
|
+
if (typeof candidate.validateImage !== "function" || typeof candidate.saveImage !== "function") {
|
|
988
|
+
return void 0;
|
|
989
|
+
}
|
|
990
|
+
const limits = candidate.imageLimits;
|
|
991
|
+
if (limits === void 0 || typeof limits.maxImagesPerMessage !== "number" || typeof limits.maxMessageImageBytes !== "number" || typeof limits.maxImageBytes !== "number") {
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
return candidate;
|
|
995
|
+
}
|
|
996
|
+
function decodeImageData(data) {
|
|
997
|
+
if (data.length === 0) throw new PromptImageError("image data is empty");
|
|
998
|
+
const decoded = Buffer.from(data, "base64");
|
|
999
|
+
if (decoded.byteLength === 0) throw new PromptImageError("image data is empty");
|
|
1000
|
+
return new Uint8Array(decoded);
|
|
1001
|
+
}
|
|
1002
|
+
function imageName(uri) {
|
|
1003
|
+
if (typeof uri !== "string" || uri.length === 0) return void 0;
|
|
1004
|
+
let leaf;
|
|
1005
|
+
try {
|
|
1006
|
+
leaf = new URL(uri).pathname.split("/").filter(Boolean).at(-1);
|
|
1007
|
+
} catch {
|
|
1008
|
+
leaf = uri.split(/[/\\]/).filter(Boolean).at(-1);
|
|
1009
|
+
}
|
|
1010
|
+
if (leaf === void 0 || leaf.length === 0) return void 0;
|
|
1011
|
+
try {
|
|
1012
|
+
return decodeURIComponent(leaf);
|
|
1013
|
+
} catch {
|
|
1014
|
+
return leaf;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function prepareImage(block) {
|
|
1018
|
+
const mediaType = canonicalImageMediaType(block.mimeType);
|
|
1019
|
+
if (mediaType === void 0) {
|
|
1020
|
+
throw new PromptImageError(`unsupported image media type: ${block.mimeType}`);
|
|
1021
|
+
}
|
|
1022
|
+
const data = decodeImageData(block.data);
|
|
1023
|
+
const name3 = imageName(block.uri);
|
|
1024
|
+
return name3 === void 0 ? { data, mediaType } : { data, mediaType, name: name3 };
|
|
1025
|
+
}
|
|
1026
|
+
function saveInput(image) {
|
|
1027
|
+
return image.name === void 0 ? { data: image.data, mediaType: image.mediaType } : { data: image.data, mediaType: image.mediaType, name: image.name };
|
|
1028
|
+
}
|
|
1029
|
+
function flushText(parts, blocks) {
|
|
1030
|
+
const text = parts.join("");
|
|
1031
|
+
parts.length = 0;
|
|
1032
|
+
if (text.length > 0) blocks.push({ type: "text", text });
|
|
1033
|
+
}
|
|
1034
|
+
async function convertPrompt(prompt, attachments) {
|
|
1035
|
+
const preparedImages = [];
|
|
1036
|
+
for (const block of prompt) {
|
|
1037
|
+
if (block.type !== "image") continue;
|
|
1038
|
+
if (attachments === void 0) throw new UnsupportedPromptContentError("image");
|
|
1039
|
+
preparedImages.push(prepareImage(block));
|
|
1040
|
+
}
|
|
1041
|
+
if (preparedImages.length > 0 && attachments !== void 0) {
|
|
1042
|
+
const { maxImagesPerMessage, maxMessageImageBytes, maxImageBytes } = attachments.imageLimits;
|
|
1043
|
+
if (preparedImages.length > maxImagesPerMessage) {
|
|
1044
|
+
throw new PromptImageError("prompt exceeds the configured image-count limit");
|
|
1045
|
+
}
|
|
1046
|
+
const totalBytes = preparedImages.reduce((sum, image) => sum + image.data.byteLength, 0);
|
|
1047
|
+
if (totalBytes > maxMessageImageBytes) {
|
|
1048
|
+
throw new PromptImageError("prompt exceeds the configured aggregate image-byte limit");
|
|
1049
|
+
}
|
|
1050
|
+
for (const image of preparedImages) {
|
|
1051
|
+
if (image.data.byteLength > maxImageBytes) {
|
|
1052
|
+
throw new PromptImageError("image exceeds the configured encoded-byte limit");
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
await attachments.validateImage(saveInput(image));
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1058
|
+
throw new PromptImageError(message, { cause: error });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
569
1062
|
const parts = [];
|
|
570
1063
|
const display = [];
|
|
1064
|
+
const blocks = [];
|
|
1065
|
+
let imageIndex = 0;
|
|
571
1066
|
for (const block of prompt) {
|
|
572
1067
|
switch (block.type) {
|
|
573
1068
|
case "text":
|
|
@@ -591,20 +1086,535 @@ ${contextBlock(resource.uri, resource.text)}
|
|
|
591
1086
|
}
|
|
592
1087
|
throw new UnsupportedPromptContentError("resource (binary)");
|
|
593
1088
|
}
|
|
1089
|
+
case "image": {
|
|
1090
|
+
if (attachments === void 0) throw new UnsupportedPromptContentError("image");
|
|
1091
|
+
const prepared = preparedImages[imageIndex];
|
|
1092
|
+
imageIndex += 1;
|
|
1093
|
+
if (prepared === void 0) throw new PromptImageError("image block was not prepared");
|
|
1094
|
+
flushText(parts, blocks);
|
|
1095
|
+
let attachment;
|
|
1096
|
+
try {
|
|
1097
|
+
attachment = await attachments.saveImage(saveInput(prepared));
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1100
|
+
throw new PromptImageError(message, { cause: error });
|
|
1101
|
+
}
|
|
1102
|
+
blocks.push({ type: "image", attachment });
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
594
1105
|
default:
|
|
595
1106
|
throw new UnsupportedPromptContentError(block.type);
|
|
596
1107
|
}
|
|
597
1108
|
}
|
|
598
|
-
|
|
1109
|
+
flushText(parts, blocks);
|
|
599
1110
|
return {
|
|
600
|
-
blocks
|
|
1111
|
+
blocks,
|
|
601
1112
|
displayText: display.join(" ").trim()
|
|
602
1113
|
};
|
|
603
1114
|
}
|
|
604
1115
|
|
|
1116
|
+
// src/bridge/rpc.ts
|
|
1117
|
+
var AcpRpc = class {
|
|
1118
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1119
|
+
writeJson;
|
|
1120
|
+
/**
|
|
1121
|
+
* Bind the mux writer. Called once from {@link muxAcpStream}.
|
|
1122
|
+
* @param writeJson - write one JSON-RPC message onto the ACP stream.
|
|
1123
|
+
*/
|
|
1124
|
+
attachWriter(writeJson) {
|
|
1125
|
+
this.writeJson = writeJson;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Register a bridge-owned request method intercepted before the ACP SDK.
|
|
1129
|
+
* @param method - JSON-RPC method name (e.g. `tui/refresh`).
|
|
1130
|
+
* @param handler - implementation.
|
|
1131
|
+
* @returns disposer.
|
|
1132
|
+
*/
|
|
1133
|
+
registerMethod(method, handler) {
|
|
1134
|
+
if (this.handlers.has(method)) {
|
|
1135
|
+
throw new Error(`acpRpc: method ${JSON.stringify(method)} is already registered`);
|
|
1136
|
+
}
|
|
1137
|
+
this.handlers.set(method, handler);
|
|
1138
|
+
return () => {
|
|
1139
|
+
this.handlers.delete(method);
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* @param method - JSON-RPC method name.
|
|
1144
|
+
* @returns whether a TUI/extension handler owns this request.
|
|
1145
|
+
*/
|
|
1146
|
+
has(method) {
|
|
1147
|
+
return this.handlers.has(method);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Dispatch one intercepted request and write the JSON-RPC response.
|
|
1151
|
+
* @param id - JSON-RPC id.
|
|
1152
|
+
* @param method - method name.
|
|
1153
|
+
* @param params - params object.
|
|
1154
|
+
*/
|
|
1155
|
+
async dispatch(id, method, params) {
|
|
1156
|
+
const handler = this.handlers.get(method);
|
|
1157
|
+
const write = this.writeJson;
|
|
1158
|
+
if (handler === void 0 || write === void 0) return;
|
|
1159
|
+
try {
|
|
1160
|
+
const result = await handler(params);
|
|
1161
|
+
write({ jsonrpc: "2.0", id, result: result ?? null });
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1164
|
+
write({ jsonrpc: "2.0", id, error: { code: -32e3, message } });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Send a JSON-RPC notification on the ACP stream (e.g. `tui/snapshot`).
|
|
1169
|
+
* @param method - notification method.
|
|
1170
|
+
* @param params - payload.
|
|
1171
|
+
*/
|
|
1172
|
+
notify(method, params) {
|
|
1173
|
+
const write = this.writeJson;
|
|
1174
|
+
if (write === void 0) return;
|
|
1175
|
+
write(params === void 0 ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params });
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
function isInterceptedRequest(message, rpc) {
|
|
1179
|
+
return "method" in message && typeof message.method === "string" && "id" in message && message.id !== void 0 && rpc.has(message.method);
|
|
1180
|
+
}
|
|
1181
|
+
function muxAcpStream(stream, rpc) {
|
|
1182
|
+
const dest = stream.writable.getWriter();
|
|
1183
|
+
const writeMessage = (value) => {
|
|
1184
|
+
void dest.write(value).catch(() => {
|
|
1185
|
+
});
|
|
1186
|
+
};
|
|
1187
|
+
rpc.attachWriter(writeMessage);
|
|
1188
|
+
const acpWritable = new WritableStream({
|
|
1189
|
+
write(chunk) {
|
|
1190
|
+
return dest.write(chunk);
|
|
1191
|
+
},
|
|
1192
|
+
close() {
|
|
1193
|
+
return dest.close();
|
|
1194
|
+
},
|
|
1195
|
+
abort(reason) {
|
|
1196
|
+
return dest.abort(reason);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
const { readable, writable } = new TransformStream({
|
|
1200
|
+
transform(message, controller) {
|
|
1201
|
+
if (isInterceptedRequest(message, rpc)) {
|
|
1202
|
+
void rpc.dispatch(message.id, message.method, message.params);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
controller.enqueue(message);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
void stream.readable.pipeTo(writable).catch(() => {
|
|
1209
|
+
});
|
|
1210
|
+
return { readable, writable: acpWritable };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/bridge/tui-client.ts
|
|
1214
|
+
function installTuiClientPlane(ctx, rpc, opts) {
|
|
1215
|
+
const { findAgent, advertisement } = opts;
|
|
1216
|
+
ctx.effect(
|
|
1217
|
+
() => rpc.registerMethod("tui/inspect-sync", (params) => {
|
|
1218
|
+
const inspect = optionalInspect(ctx);
|
|
1219
|
+
if (inspect === void 0) {
|
|
1220
|
+
throw new Error("this agent has no Cordis inspect registry");
|
|
1221
|
+
}
|
|
1222
|
+
const providers = readProviders(params);
|
|
1223
|
+
inspect.syncClientManifest(providers);
|
|
1224
|
+
advertisement.advertised = true;
|
|
1225
|
+
return { ok: true };
|
|
1226
|
+
}),
|
|
1227
|
+
"acp-bridge: tui/inspect-sync"
|
|
1228
|
+
);
|
|
1229
|
+
ctx.effect(
|
|
1230
|
+
() => rpc.registerMethod("tui/inspect-resolve", (params) => {
|
|
1231
|
+
const inspect = optionalInspect(ctx);
|
|
1232
|
+
if (inspect === void 0) {
|
|
1233
|
+
throw new Error("this agent has no Cordis inspect registry");
|
|
1234
|
+
}
|
|
1235
|
+
const body = asObject(params);
|
|
1236
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1237
|
+
const requestId = readString(body, "requestId");
|
|
1238
|
+
return inspect.resolveClientQuery(agent, requestId, body["resolution"]);
|
|
1239
|
+
}),
|
|
1240
|
+
"acp-bridge: tui/inspect-resolve"
|
|
1241
|
+
);
|
|
1242
|
+
ctx.effect(
|
|
1243
|
+
() => rpc.registerMethod("tui/run-host-half", async (params) => {
|
|
1244
|
+
const runner = optionalRunner(ctx);
|
|
1245
|
+
if (runner === void 0) {
|
|
1246
|
+
throw new Error("this agent has no dynamic Cordis runner");
|
|
1247
|
+
}
|
|
1248
|
+
const body = asObject(params);
|
|
1249
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1250
|
+
const requestId = body["requestId"];
|
|
1251
|
+
return await runner.runHostHalf(
|
|
1252
|
+
agent,
|
|
1253
|
+
readString(body, "pluginId"),
|
|
1254
|
+
readString(body, "packageId"),
|
|
1255
|
+
readString(body, "mode"),
|
|
1256
|
+
typeof requestId === "string" ? requestId : null,
|
|
1257
|
+
body["approveFutureVersions"] === true
|
|
1258
|
+
);
|
|
1259
|
+
}),
|
|
1260
|
+
"acp-bridge: tui/run-host-half"
|
|
1261
|
+
);
|
|
1262
|
+
ctx.effect(
|
|
1263
|
+
() => rpc.registerMethod("tui/get-client-code", (params) => {
|
|
1264
|
+
const runner = optionalRunner(ctx);
|
|
1265
|
+
if (runner === void 0) {
|
|
1266
|
+
throw new Error("this agent has no dynamic Cordis runner");
|
|
1267
|
+
}
|
|
1268
|
+
const body = asObject(params);
|
|
1269
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1270
|
+
return runner.getClientCode(
|
|
1271
|
+
agent,
|
|
1272
|
+
readString(body, "pluginId"),
|
|
1273
|
+
readString(body, "pluginRunId")
|
|
1274
|
+
);
|
|
1275
|
+
}),
|
|
1276
|
+
"acp-bridge: tui/get-client-code"
|
|
1277
|
+
);
|
|
1278
|
+
ctx.effect(
|
|
1279
|
+
() => rpc.registerMethod("tui/resolve-request-run", async (params) => {
|
|
1280
|
+
const runner = optionalRunner(ctx);
|
|
1281
|
+
if (runner === void 0) {
|
|
1282
|
+
throw new Error("this agent has no dynamic Cordis runner");
|
|
1283
|
+
}
|
|
1284
|
+
const body = asObject(params);
|
|
1285
|
+
return await runner.resolveRequestRun(
|
|
1286
|
+
readString(body, "requestId"),
|
|
1287
|
+
body["resolution"]
|
|
1288
|
+
);
|
|
1289
|
+
}),
|
|
1290
|
+
"acp-bridge: tui/resolve-request-run"
|
|
1291
|
+
);
|
|
1292
|
+
ctx.effect(
|
|
1293
|
+
() => rpc.registerMethod("tui/invoke", async (params) => {
|
|
1294
|
+
const runner = optionalRunner(ctx);
|
|
1295
|
+
if (runner === void 0) {
|
|
1296
|
+
throw new Error("this agent has no dynamic Cordis runner");
|
|
1297
|
+
}
|
|
1298
|
+
const body = asObject(params);
|
|
1299
|
+
requireAgent(findAgent, readString(body, "agentId"));
|
|
1300
|
+
return await runner.invoke(
|
|
1301
|
+
readString(body, "pluginId"),
|
|
1302
|
+
readString(body, "pluginRunId"),
|
|
1303
|
+
readString(body, "method"),
|
|
1304
|
+
body["args"] ?? null
|
|
1305
|
+
);
|
|
1306
|
+
}),
|
|
1307
|
+
"acp-bridge: tui/invoke"
|
|
1308
|
+
);
|
|
1309
|
+
ctx.effect(
|
|
1310
|
+
() => rpc.registerMethod("tui/list-plugins", (params) => {
|
|
1311
|
+
const runner = requireRunnerMethod(ctx, "listPlugins");
|
|
1312
|
+
const body = asObject(params);
|
|
1313
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1314
|
+
return runner.listPlugins(agent);
|
|
1315
|
+
}),
|
|
1316
|
+
"acp-bridge: tui/list-plugins"
|
|
1317
|
+
);
|
|
1318
|
+
ctx.effect(
|
|
1319
|
+
() => rpc.registerMethod("tui/stop-plugin", async (params) => {
|
|
1320
|
+
const runner = requireRunnerMethod(ctx, "stopFromPanel");
|
|
1321
|
+
const body = asObject(params);
|
|
1322
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1323
|
+
return await runner.stopFromPanel(agent, readString(body, "pluginId"));
|
|
1324
|
+
}),
|
|
1325
|
+
"acp-bridge: tui/stop-plugin"
|
|
1326
|
+
);
|
|
1327
|
+
ctx.effect(
|
|
1328
|
+
() => rpc.registerMethod("tui/start-plugin", async (params) => {
|
|
1329
|
+
const runner = requireRunnerMethod(ctx, "listPlugins");
|
|
1330
|
+
const body = asObject(params);
|
|
1331
|
+
const agentId = readString(body, "agentId");
|
|
1332
|
+
const agent = requireAgent(findAgent, agentId);
|
|
1333
|
+
const pluginId = readString(body, "pluginId");
|
|
1334
|
+
const plugin = runner.listPlugins(agent).find((row) => row.pluginId === pluginId);
|
|
1335
|
+
if (plugin === void 0) throw new Error(`dynamic plugin "${pluginId}" does not exist`);
|
|
1336
|
+
const packageId = plugin.currentPackageId;
|
|
1337
|
+
if (packageId === void 0) {
|
|
1338
|
+
throw new Error(`dynamic plugin "${pluginId}" has no current package to restore`);
|
|
1339
|
+
}
|
|
1340
|
+
const pkg = plugin.packages.find((row) => row.packageId === packageId);
|
|
1341
|
+
if (pkg === void 0) {
|
|
1342
|
+
throw new Error(`dynamic plugin "${pluginId}" current package "${packageId}" does not exist`);
|
|
1343
|
+
}
|
|
1344
|
+
const started = asObject(await runner.runHostHalf(
|
|
1345
|
+
agent,
|
|
1346
|
+
pluginId,
|
|
1347
|
+
packageId,
|
|
1348
|
+
"run",
|
|
1349
|
+
null,
|
|
1350
|
+
false
|
|
1351
|
+
));
|
|
1352
|
+
if (started["ok"] !== true) return started;
|
|
1353
|
+
const pluginRunId = readString(started, "pluginRunId");
|
|
1354
|
+
const startedHere = started["startedHere"] !== false;
|
|
1355
|
+
if (pkg.hasClientHalf) {
|
|
1356
|
+
rpc.notify("tui/user-run", {
|
|
1357
|
+
agentId,
|
|
1358
|
+
pluginId,
|
|
1359
|
+
packageId,
|
|
1360
|
+
pluginRunId,
|
|
1361
|
+
startedHere,
|
|
1362
|
+
mode: "run",
|
|
1363
|
+
hasClientHalf: true
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
return { ok: true, status: "starting", pluginId, packageId, pluginRunId };
|
|
1367
|
+
}),
|
|
1368
|
+
"acp-bridge: tui/start-plugin"
|
|
1369
|
+
);
|
|
1370
|
+
ctx.effect(
|
|
1371
|
+
() => rpc.registerMethod("tui/settle-user-run", async (params) => {
|
|
1372
|
+
const runner = requireRunnerMethod(ctx, "settleUserRun");
|
|
1373
|
+
const body = asObject(params);
|
|
1374
|
+
const agent = requireAgent(findAgent, readString(body, "agentId"));
|
|
1375
|
+
return await runner.settleUserRun(
|
|
1376
|
+
agent,
|
|
1377
|
+
readString(body, "pluginId"),
|
|
1378
|
+
body["resolution"]
|
|
1379
|
+
);
|
|
1380
|
+
}),
|
|
1381
|
+
"acp-bridge: tui/settle-user-run"
|
|
1382
|
+
);
|
|
1383
|
+
ctx.on("cordis/inspect-query", (request) => {
|
|
1384
|
+
if (!advertisement.advertised) return;
|
|
1385
|
+
rpc.notify("tui/inspect-query", request);
|
|
1386
|
+
});
|
|
1387
|
+
ctx.on("cordis/inspect-query-resolved", (resolved) => {
|
|
1388
|
+
if (!advertisement.advertised) return;
|
|
1389
|
+
rpc.notify("tui/inspect-query-resolved", resolved);
|
|
1390
|
+
});
|
|
1391
|
+
ctx.on("cordis/request-run", (request) => {
|
|
1392
|
+
if (!advertisement.advertised) return;
|
|
1393
|
+
rpc.notify("tui/request-run", request);
|
|
1394
|
+
});
|
|
1395
|
+
ctx.on("cordis/request-run-resolved", (resolved) => {
|
|
1396
|
+
if (!advertisement.advertised) return;
|
|
1397
|
+
rpc.notify("tui/request-run-resolved", resolved);
|
|
1398
|
+
});
|
|
1399
|
+
ctx.on("cordis/dynamic-retract", (retracted) => {
|
|
1400
|
+
if (!advertisement.advertised) return;
|
|
1401
|
+
rpc.notify("tui/dynamic-retract", retracted);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
function optionalInspect(ctx) {
|
|
1405
|
+
try {
|
|
1406
|
+
const value = ctx.get("cordisInspect");
|
|
1407
|
+
return value !== void 0 && typeof value.syncClientManifest === "function" ? value : void 0;
|
|
1408
|
+
} catch {
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function optionalRunner(ctx) {
|
|
1413
|
+
try {
|
|
1414
|
+
const value = ctx.get("dynamicCordisRunner");
|
|
1415
|
+
return value !== void 0 && typeof value.runHostHalf === "function" ? value : void 0;
|
|
1416
|
+
} catch {
|
|
1417
|
+
return void 0;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function requireRunnerMethod(ctx, method) {
|
|
1421
|
+
const runner = optionalRunner(ctx);
|
|
1422
|
+
if (runner === void 0 || typeof runner[method] !== "function") {
|
|
1423
|
+
throw new Error(`this agent has no dynamic Cordis runner method ${String(method)}`);
|
|
1424
|
+
}
|
|
1425
|
+
return runner;
|
|
1426
|
+
}
|
|
1427
|
+
function requireAgent(findAgent, agentId) {
|
|
1428
|
+
const agent = findAgent(agentId);
|
|
1429
|
+
if (agent === void 0) throw new Error(`unknown agent: ${agentId}`);
|
|
1430
|
+
return agent;
|
|
1431
|
+
}
|
|
1432
|
+
function asObject(value) {
|
|
1433
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1434
|
+
throw new Error("params must be an object");
|
|
1435
|
+
}
|
|
1436
|
+
return value;
|
|
1437
|
+
}
|
|
1438
|
+
function readString(body, field) {
|
|
1439
|
+
const value = body[field];
|
|
1440
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1441
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
1442
|
+
}
|
|
1443
|
+
return value;
|
|
1444
|
+
}
|
|
1445
|
+
function readProviders(params) {
|
|
1446
|
+
const body = asObject(params);
|
|
1447
|
+
const providers = body["providers"];
|
|
1448
|
+
if (!Array.isArray(providers)) throw new Error("providers must be an array");
|
|
1449
|
+
return providers.map((provider, index) => {
|
|
1450
|
+
if (provider === null || typeof provider !== "object" || Array.isArray(provider)) {
|
|
1451
|
+
throw new Error(`providers[${index}] must be an object`);
|
|
1452
|
+
}
|
|
1453
|
+
const row = provider;
|
|
1454
|
+
if (typeof row["id"] !== "string" || row["id"].trim() === "") {
|
|
1455
|
+
throw new Error(`providers[${index}].id must be a non-empty string`);
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof row["description"] !== "string" || row["description"].trim() === "") {
|
|
1458
|
+
throw new Error(`providers[${index}].description must be a non-empty string`);
|
|
1459
|
+
}
|
|
1460
|
+
if (!Array.isArray(row["methods"])) {
|
|
1461
|
+
throw new Error(`providers[${index}].methods must be an array`);
|
|
1462
|
+
}
|
|
1463
|
+
return provider;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/bridge/user-questions.ts
|
|
1468
|
+
import {
|
|
1469
|
+
UserQuestionError
|
|
1470
|
+
} from "@deepseek-ai/dsh-user-questions";
|
|
1471
|
+
var META_KEY = "dsh.userQuestions";
|
|
1472
|
+
function fieldName(index) {
|
|
1473
|
+
return `question_${index}`;
|
|
1474
|
+
}
|
|
1475
|
+
function customFieldName(index) {
|
|
1476
|
+
return `${fieldName(index)}_custom`;
|
|
1477
|
+
}
|
|
1478
|
+
function optionValue(index) {
|
|
1479
|
+
return `option_${index}`;
|
|
1480
|
+
}
|
|
1481
|
+
function customValue(index) {
|
|
1482
|
+
return `custom_${index}`;
|
|
1483
|
+
}
|
|
1484
|
+
function descriptionOf(question) {
|
|
1485
|
+
const parts = [question.header === void 0 ? void 0 : question.question, question.detail].filter((part) => part !== void 0 && part.length > 0);
|
|
1486
|
+
return parts.length === 0 ? void 0 : parts.join("\n\n");
|
|
1487
|
+
}
|
|
1488
|
+
function propertyOf(question, index) {
|
|
1489
|
+
const title = question.header ?? question.question;
|
|
1490
|
+
const description = descriptionOf(question);
|
|
1491
|
+
const options = question.options ?? [];
|
|
1492
|
+
if (options.length === 0) {
|
|
1493
|
+
return {
|
|
1494
|
+
type: "string",
|
|
1495
|
+
title,
|
|
1496
|
+
...description === void 0 ? {} : { description }
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
const choices = [
|
|
1500
|
+
...options.map((option, optionIndex) => ({
|
|
1501
|
+
const: optionValue(optionIndex),
|
|
1502
|
+
title: option.label,
|
|
1503
|
+
...option.description === void 0 ? {} : { description: option.description }
|
|
1504
|
+
})),
|
|
1505
|
+
{ const: customValue(index), title: "Other" }
|
|
1506
|
+
];
|
|
1507
|
+
if (question.multiSelect === true) {
|
|
1508
|
+
return {
|
|
1509
|
+
type: "array",
|
|
1510
|
+
title,
|
|
1511
|
+
...description === void 0 ? {} : { description },
|
|
1512
|
+
minItems: 1,
|
|
1513
|
+
items: { anyOf: choices }
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
return {
|
|
1517
|
+
type: "string",
|
|
1518
|
+
title,
|
|
1519
|
+
...description === void 0 ? {} : { description },
|
|
1520
|
+
oneOf: choices
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function questionsToElicitation(questions, sessionId) {
|
|
1524
|
+
const properties = {};
|
|
1525
|
+
const required = [];
|
|
1526
|
+
questions.forEach((question, index) => {
|
|
1527
|
+
const field = fieldName(index);
|
|
1528
|
+
properties[field] = propertyOf(question, index);
|
|
1529
|
+
if ((question.options?.length ?? 0) > 0) {
|
|
1530
|
+
properties[customFieldName(index)] = {
|
|
1531
|
+
type: "string",
|
|
1532
|
+
title: "Other",
|
|
1533
|
+
description: "Type a custom answer."
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
return {
|
|
1538
|
+
mode: "form",
|
|
1539
|
+
sessionId,
|
|
1540
|
+
message: "The agent needs your input.",
|
|
1541
|
+
requestedSchema: {
|
|
1542
|
+
type: "object",
|
|
1543
|
+
properties,
|
|
1544
|
+
required
|
|
1545
|
+
},
|
|
1546
|
+
_meta: {
|
|
1547
|
+
[META_KEY]: {
|
|
1548
|
+
version: 1,
|
|
1549
|
+
questions
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function answerFromElicitation(questions, response) {
|
|
1555
|
+
if (response.action !== "accept") return void 0;
|
|
1556
|
+
const content = response.content ?? {};
|
|
1557
|
+
return {
|
|
1558
|
+
answers: questions.map((question, index) => {
|
|
1559
|
+
const options = question.options ?? [];
|
|
1560
|
+
const raw = content[fieldName(index)];
|
|
1561
|
+
if (options.length === 0) {
|
|
1562
|
+
const custom2 = typeof raw === "string" ? raw.trim() : "";
|
|
1563
|
+
return {
|
|
1564
|
+
id: question.id,
|
|
1565
|
+
selected: [],
|
|
1566
|
+
...custom2.length === 0 ? {} : { custom: custom2 }
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const values = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
1570
|
+
const selected = values.flatMap((value) => {
|
|
1571
|
+
const match = /^option_(\d+)$/.exec(value);
|
|
1572
|
+
if (match === null) return [];
|
|
1573
|
+
const option = options[Number(match[1])];
|
|
1574
|
+
return option === void 0 ? [] : [option.label];
|
|
1575
|
+
});
|
|
1576
|
+
const customRaw = content[customFieldName(index)];
|
|
1577
|
+
const custom = typeof customRaw === "string" ? customRaw.trim() : "";
|
|
1578
|
+
return {
|
|
1579
|
+
id: question.id,
|
|
1580
|
+
selected: custom.length > 0 && question.multiSelect !== true ? [] : selected,
|
|
1581
|
+
...custom.length === 0 ? {} : { custom }
|
|
1582
|
+
};
|
|
1583
|
+
})
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
async function askUserQuestionsOverAcp(request, sessionId, create) {
|
|
1587
|
+
const response = await create(questionsToElicitation(request.questions, sessionId));
|
|
1588
|
+
const answer = answerFromElicitation(request.questions, response);
|
|
1589
|
+
if (answer === void 0) {
|
|
1590
|
+
throw new UserQuestionError("the user cancelled ask_user_question", "ASK_CANCELLED");
|
|
1591
|
+
}
|
|
1592
|
+
return answer;
|
|
1593
|
+
}
|
|
1594
|
+
function installAcpUserQuestionProvider(service, options) {
|
|
1595
|
+
return service.registerProvider({
|
|
1596
|
+
async ask(request) {
|
|
1597
|
+
if (!options.formSupported()) {
|
|
1598
|
+
throw new UserQuestionError(
|
|
1599
|
+
"the ACP client does not support form elicitation",
|
|
1600
|
+
"CLIENT_UNSUPPORTED"
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const sessionId = options.sessionIdForRequest(request);
|
|
1604
|
+
if (sessionId === void 0) {
|
|
1605
|
+
throw new UserQuestionError(
|
|
1606
|
+
"ACP user interaction requires a live root session",
|
|
1607
|
+
"ASK_MISSING_AGENT"
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
return askUserQuestionsOverAcp(request, sessionId, options.create);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
605
1615
|
// src/bridge/index.ts
|
|
606
1616
|
var name = "acp-bridge";
|
|
607
|
-
var inject = ["agents"];
|
|
1617
|
+
var inject = ["agents", "credentials", "llm", "agentDefaultModel", "sessionPersistence", "approval", "permissionPresets", "commands", "agentPresets", "skills", "subagents", "userQuestions"];
|
|
608
1618
|
function invalidParams(detail) {
|
|
609
1619
|
return RequestError.invalidParams(void 0, detail);
|
|
610
1620
|
}
|
|
@@ -614,9 +1624,6 @@ function internalError(detail) {
|
|
|
614
1624
|
function authRequired(detail) {
|
|
615
1625
|
return RequestError.authRequired(void 0, detail);
|
|
616
1626
|
}
|
|
617
|
-
function envCredentialPresent() {
|
|
618
|
-
return Boolean(process.env["DEEPSEEK_API_KEY"] || process.env["DEEPSEEK_BASE_URL"]);
|
|
619
|
-
}
|
|
620
1627
|
var MODE_LABELS = {
|
|
621
1628
|
"read-only": { name: "Read-only", description: "Bash and file mutations are denied by the sandbox" },
|
|
622
1629
|
"workspace-write": {
|
|
@@ -630,13 +1637,40 @@ var MODE_LABELS = {
|
|
|
630
1637
|
};
|
|
631
1638
|
async function apply(ctx, config = {}) {
|
|
632
1639
|
const agents = ctx.agents;
|
|
1640
|
+
const credentials = ctx.credentials;
|
|
1641
|
+
const llm = ctx.llm;
|
|
1642
|
+
const agentDefaultModel = ctx.agentDefaultModel;
|
|
1643
|
+
const sessionPersistence = ctx.sessionPersistence;
|
|
1644
|
+
const approval = ctx.approval;
|
|
1645
|
+
const permissionPresets = ctx.permissionPresets;
|
|
1646
|
+
const commandRuntime = ctx.commands;
|
|
1647
|
+
const agentPresets = ctx.agentPresets;
|
|
1648
|
+
const skillRegistry = ctx.skills;
|
|
1649
|
+
const subagents = ctx.subagents;
|
|
1650
|
+
const userQuestions = ctx.userQuestions;
|
|
633
1651
|
const harness = config.harness ?? (await Promise.resolve().then(() => (init_self_harness(), self_harness_exports))).selfHarness();
|
|
634
1652
|
const { createUserMessage: createUserMessage2, errorChain: errorChain2, sessionId: SessionId2, foldSessionTitle: foldSessionTitle2, setSandboxMode: setSandboxMode2 } = harness;
|
|
635
1653
|
const SANDBOX_MODES2 = harness.sandboxModes;
|
|
636
1654
|
const sessions = /* @__PURE__ */ new Map();
|
|
1655
|
+
const subagentByChild = /* @__PURE__ */ new Map();
|
|
1656
|
+
const subagentByRun = /* @__PURE__ */ new Map();
|
|
1657
|
+
const watchedSubagentParents = /* @__PURE__ */ new WeakSet();
|
|
637
1658
|
let closed = false;
|
|
638
1659
|
let conn;
|
|
639
1660
|
let clientTerminalOutput = false;
|
|
1661
|
+
let clientSubagentTranscript = false;
|
|
1662
|
+
let clientElicitationForm = false;
|
|
1663
|
+
const tuiClient = { advertised: false };
|
|
1664
|
+
const rpc = new AcpRpc();
|
|
1665
|
+
const findAgent = (agentId) => {
|
|
1666
|
+
const fromRegistry = agents.get(agentId);
|
|
1667
|
+
if (fromRegistry !== void 0) return fromRegistry;
|
|
1668
|
+
for (const record of sessions.values()) {
|
|
1669
|
+
if (record.agent.id === agentId) return record.agent;
|
|
1670
|
+
}
|
|
1671
|
+
return void 0;
|
|
1672
|
+
};
|
|
1673
|
+
installTuiClientPlane(ctx, rpc, { findAgent, advertisement: tuiClient });
|
|
640
1674
|
const modelCandidates = () => {
|
|
641
1675
|
const seen = /* @__PURE__ */ new Set();
|
|
642
1676
|
if (config.model !== void 0) seen.add(config.model);
|
|
@@ -644,10 +1678,8 @@ async function apply(ctx, config = {}) {
|
|
|
644
1678
|
return [...seen];
|
|
645
1679
|
};
|
|
646
1680
|
const defaultSelection = () => {
|
|
647
|
-
const service = ctx.get("agentDefaultModel");
|
|
648
|
-
if (service === void 0) return {};
|
|
649
1681
|
try {
|
|
650
|
-
return
|
|
1682
|
+
return agentDefaultModel.currentSelection();
|
|
651
1683
|
} catch (error) {
|
|
652
1684
|
logDebug(`agentDefaultModel.currentSelection failed: ${String(error)}`);
|
|
653
1685
|
return {};
|
|
@@ -664,28 +1696,25 @@ async function apply(ctx, config = {}) {
|
|
|
664
1696
|
const effortCache = /* @__PURE__ */ new Map();
|
|
665
1697
|
const discoverModels = async () => {
|
|
666
1698
|
if (liveCatalog !== void 0) return liveCatalog;
|
|
667
|
-
const llm = ctx.get("llm");
|
|
668
1699
|
const found = [];
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1700
|
+
try {
|
|
1701
|
+
const providers = llm.listProviders();
|
|
1702
|
+
const multi = providers.length > 1;
|
|
1703
|
+
for (const provider of providers) {
|
|
1704
|
+
const models = await llm.listModels(provider.id).catch((error) => {
|
|
1705
|
+
logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
|
|
1706
|
+
return [];
|
|
1707
|
+
});
|
|
1708
|
+
for (const model of models) {
|
|
1709
|
+
found.push({
|
|
1710
|
+
provider: provider.id,
|
|
1711
|
+
model: model.id,
|
|
1712
|
+
label: multi ? `${model.name} (${provider.name})` : model.name
|
|
677
1713
|
});
|
|
678
|
-
for (const model of models) {
|
|
679
|
-
found.push({
|
|
680
|
-
provider: provider.id,
|
|
681
|
-
model: model.id,
|
|
682
|
-
label: multi ? `${model.name} (${provider.name})` : model.name
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
1714
|
}
|
|
686
|
-
} catch (error) {
|
|
687
|
-
logDebug(`model discovery failed: ${String(error)}`);
|
|
688
1715
|
}
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
logDebug(`model discovery failed: ${String(error)}`);
|
|
689
1718
|
}
|
|
690
1719
|
liveCatalog = found;
|
|
691
1720
|
return found;
|
|
@@ -721,10 +1750,9 @@ async function apply(ctx, config = {}) {
|
|
|
721
1750
|
if (provider === void 0 || model === void 0) return void 0;
|
|
722
1751
|
const key = `${provider}::${model}`;
|
|
723
1752
|
if (effortCache.has(key)) return effortCache.get(key);
|
|
724
|
-
const llm = ctx.get("llm");
|
|
725
1753
|
let catalog;
|
|
726
1754
|
try {
|
|
727
|
-
const reasoning = (await llm
|
|
1755
|
+
const reasoning = (await llm.resolveModelInfo(provider, model)).reasoning;
|
|
728
1756
|
if (reasoning !== void 0 && reasoning.efforts.length > 0) {
|
|
729
1757
|
catalog = {
|
|
730
1758
|
efforts: reasoning.efforts.map((effort) => ({
|
|
@@ -827,27 +1855,38 @@ async function apply(ctx, config = {}) {
|
|
|
827
1855
|
}
|
|
828
1856
|
setApprovalPolicy(record, record.approvals);
|
|
829
1857
|
};
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1858
|
+
const listProviderRoutes = async () => {
|
|
1859
|
+
try {
|
|
1860
|
+
const raw2 = llm.listProviders();
|
|
1861
|
+
if (!Array.isArray(raw2)) return [];
|
|
1862
|
+
return raw2.flatMap((entry) => {
|
|
1863
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1864
|
+
const id = entry.id;
|
|
1865
|
+
if (typeof id !== "string" || id.length === 0) return [];
|
|
1866
|
+
const name3 = entry.name;
|
|
1867
|
+
return [{ id, ...typeof name3 === "string" && name3.length > 0 ? { name: name3 } : {} }];
|
|
1868
|
+
});
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
logDebug(`listProviders failed: ${String(error)}`);
|
|
1871
|
+
return [];
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
const credentialPresent = async (provider) => {
|
|
1875
|
+
const names = credentialEnvNames(provider).filter((name3) => !name3.endsWith("_BASE_URL"));
|
|
1876
|
+
for (const name3 of names) {
|
|
1877
|
+
try {
|
|
1878
|
+
if (await credentials.resolve(credentialRef(name3)) !== void 0) return true;
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
logWarn(`credential lookup failed: ${String(error)}`);
|
|
848
1881
|
}
|
|
849
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
850
1882
|
}
|
|
1883
|
+
return false;
|
|
1884
|
+
};
|
|
1885
|
+
const requireCredential = async (provider) => {
|
|
1886
|
+
if (await credentialPresent(provider)) return;
|
|
1887
|
+
throw authRequired(
|
|
1888
|
+
'no credential found for this provider: call authenticate with `_meta["api-key"].apiKey` or `_meta.gateway`, use the browser method, run `dsh-acp login`, or save a key in the dsh Web UI (Settings \u2192 Models)'
|
|
1889
|
+
);
|
|
851
1890
|
};
|
|
852
1891
|
const agentOptionsFor = (model, provider) => ({
|
|
853
1892
|
...provider !== void 0 ? { provider } : config.provider !== void 0 ? { provider: config.provider } : {},
|
|
@@ -866,7 +1905,6 @@ async function apply(ctx, config = {}) {
|
|
|
866
1905
|
const requireOrRestoreSession = async (sessionId) => {
|
|
867
1906
|
const record = sessions.get(sessionId);
|
|
868
1907
|
if (record !== void 0) return record;
|
|
869
|
-
if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
|
|
870
1908
|
logWarn(`restoring session ${sessionId} from the persisted log`);
|
|
871
1909
|
try {
|
|
872
1910
|
return await restoreSession(sessionId, { replay: false });
|
|
@@ -906,7 +1944,7 @@ async function apply(ctx, config = {}) {
|
|
|
906
1944
|
}
|
|
907
1945
|
const presets = presetsService();
|
|
908
1946
|
const storedPreset = presetFromLog(storedHeader, events);
|
|
909
|
-
const presetId =
|
|
1947
|
+
const presetId = (await presets.resolve(storedPreset)).id;
|
|
910
1948
|
const handle = await agents.resume({
|
|
911
1949
|
resumeSessionId: SessionId2(sessionId),
|
|
912
1950
|
agentOptions: agentOptionsFor(config.model),
|
|
@@ -929,7 +1967,7 @@ async function apply(ctx, config = {}) {
|
|
|
929
1967
|
if (presetId !== void 0) record.preset = presetId;
|
|
930
1968
|
{
|
|
931
1969
|
const permission = permissionService();
|
|
932
|
-
const storedMode = permission
|
|
1970
|
+
const storedMode = permission.current(events);
|
|
933
1971
|
if (storedMode !== void 0) record.modeId = storedMode;
|
|
934
1972
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
935
1973
|
const event = events[index];
|
|
@@ -958,13 +1996,78 @@ async function apply(ctx, config = {}) {
|
|
|
958
1996
|
record.inflight = void 0;
|
|
959
1997
|
inflight.resolve(reason);
|
|
960
1998
|
};
|
|
961
|
-
const
|
|
1999
|
+
const watchSubagentParent = (parent) => {
|
|
2000
|
+
if (watchedSubagentParents.has(parent)) return;
|
|
2001
|
+
watchedSubagentParents.add(parent);
|
|
2002
|
+
parent.ctx.on("subagent/start", (info) => {
|
|
2003
|
+
const parentLink = subagentByChild.get(String(parent.id));
|
|
2004
|
+
const rootSessionId = parentLink?.rootSessionId ?? String(parent.session.id);
|
|
2005
|
+
const record = sessions.get(rootSessionId);
|
|
2006
|
+
if (record === void 0) return;
|
|
2007
|
+
const runId = String(info.runId);
|
|
2008
|
+
const childSessionId = String(info.id);
|
|
2009
|
+
const toolCallId = `subagent:${runId}`;
|
|
2010
|
+
const payload = {
|
|
2011
|
+
runId,
|
|
2012
|
+
provider: info.provider,
|
|
2013
|
+
id: childSessionId,
|
|
2014
|
+
local: info.local,
|
|
2015
|
+
...parentLink !== void 0 ? { parentToolCallId: parentLink.toolCallId } : {}
|
|
2016
|
+
};
|
|
2017
|
+
for (const update of record.projection.onEvent({ type: "subagent/start", data: payload })) {
|
|
2018
|
+
notify(rootSessionId, update);
|
|
2019
|
+
}
|
|
2020
|
+
const child = agents.get(info.id);
|
|
2021
|
+
const link = {
|
|
2022
|
+
rootSessionId,
|
|
2023
|
+
childSessionId,
|
|
2024
|
+
runId,
|
|
2025
|
+
provider: info.provider,
|
|
2026
|
+
toolCallId,
|
|
2027
|
+
projection: new SessionProjection(void 0, {
|
|
2028
|
+
terminalOutput: clientTerminalOutput,
|
|
2029
|
+
...child?.session.header.cwd !== void 0 ? { cwd: child.session.header.cwd } : {},
|
|
2030
|
+
subagent: { childSessionId, parentToolCallId: toolCallId, provider: info.provider }
|
|
2031
|
+
})
|
|
2032
|
+
};
|
|
2033
|
+
subagentByChild.set(childSessionId, link);
|
|
2034
|
+
subagentByRun.set(runId, link);
|
|
2035
|
+
if (child !== void 0) watchSubagentParent(child);
|
|
2036
|
+
});
|
|
2037
|
+
parent.ctx.on("subagent/end", (info) => {
|
|
2038
|
+
const runId = String(info.runId);
|
|
2039
|
+
const link = subagentByRun.get(runId);
|
|
2040
|
+
if (link === void 0) return;
|
|
2041
|
+
const record = sessions.get(link.rootSessionId);
|
|
2042
|
+
if (record !== void 0) {
|
|
2043
|
+
const parentLink = subagentByChild.get(String(parent.id));
|
|
2044
|
+
const payload = {
|
|
2045
|
+
runId,
|
|
2046
|
+
provider: info.provider,
|
|
2047
|
+
id: String(info.id),
|
|
2048
|
+
local: info.local,
|
|
2049
|
+
stopReason: info.stopReason,
|
|
2050
|
+
...info.lastAssistantMessage !== void 0 ? { lastAssistantMessage: info.lastAssistantMessage } : {},
|
|
2051
|
+
...parentLink !== void 0 ? { parentToolCallId: parentLink.toolCallId } : {}
|
|
2052
|
+
};
|
|
2053
|
+
for (const update of record.projection.onEvent({ type: "subagent/end", data: payload })) {
|
|
2054
|
+
notify(link.rootSessionId, update);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
subagentByRun.delete(runId);
|
|
2058
|
+
if (subagentByChild.get(link.childSessionId)?.runId === runId) {
|
|
2059
|
+
subagentByChild.delete(link.childSessionId);
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
};
|
|
2063
|
+
const presetsService = () => agentPresets;
|
|
962
2064
|
const presetSetup = (presets, presetId) => {
|
|
963
|
-
if (
|
|
2065
|
+
if (presetId === void 0) return void 0;
|
|
964
2066
|
return async (agentCtx) => {
|
|
2067
|
+
const started = performance.now();
|
|
965
2068
|
logDebug(`preset setup: mounting "${presetId}"`);
|
|
966
2069
|
await presets.mount(agentCtx, presetId);
|
|
967
|
-
logDebug(`preset setup: mounted "${presetId}"`);
|
|
2070
|
+
logDebug(`preset setup: mounted "${presetId}" in ${(performance.now() - started).toFixed(1)}ms`);
|
|
968
2071
|
};
|
|
969
2072
|
};
|
|
970
2073
|
const presetFromLog = (header, events) => {
|
|
@@ -979,7 +2082,7 @@ async function apply(ctx, config = {}) {
|
|
|
979
2082
|
const presetLabel = (id) => id.charAt(0).toUpperCase() + id.slice(1);
|
|
980
2083
|
const modeState = (record) => {
|
|
981
2084
|
const permission = permissionService();
|
|
982
|
-
if (permission
|
|
2085
|
+
if (permission.names.length > 0) {
|
|
983
2086
|
return {
|
|
984
2087
|
currentModeId: record.modeId,
|
|
985
2088
|
availableModes: permission.names.map((name3) => {
|
|
@@ -1001,7 +2104,6 @@ async function apply(ctx, config = {}) {
|
|
|
1001
2104
|
throw invalidParams("cannot switch presets while a prompt is running");
|
|
1002
2105
|
}
|
|
1003
2106
|
const presets = presetsService();
|
|
1004
|
-
if (presets === void 0) throw invalidParams(`unknown mode: ${presetId}`);
|
|
1005
2107
|
let resolved;
|
|
1006
2108
|
try {
|
|
1007
2109
|
resolved = (await presets.resolve(presetId)).id;
|
|
@@ -1009,29 +2111,44 @@ async function apply(ctx, config = {}) {
|
|
|
1009
2111
|
throw invalidParams(`unknown preset: ${presetId} (${errorChain2(error)})`);
|
|
1010
2112
|
}
|
|
1011
2113
|
if (resolved === record.preset) return;
|
|
1012
|
-
|
|
1013
|
-
record.agent.session.append("agent-preset/selected", { agentPreset: resolved });
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
logWarn(`recording preset selection failed: ${String(error)}`);
|
|
1016
|
-
}
|
|
2114
|
+
const previous = record.preset;
|
|
1017
2115
|
const sessionId = record.agent.session.id;
|
|
2116
|
+
const resumeWith = (id) => agents.resume({
|
|
2117
|
+
resumeSessionId: sessionId,
|
|
2118
|
+
agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
|
|
2119
|
+
...presetSetup(presets, id) !== void 0 ? { setup: presetSetup(presets, id) } : {}
|
|
2120
|
+
});
|
|
1018
2121
|
await record.dispose().catch((error) => {
|
|
1019
2122
|
logWarn(`dispose during preset switch failed: ${String(error)}`);
|
|
1020
2123
|
});
|
|
1021
2124
|
let handle;
|
|
1022
2125
|
try {
|
|
1023
|
-
handle = await
|
|
1024
|
-
resumeSessionId: sessionId,
|
|
1025
|
-
agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
|
|
1026
|
-
setup: presetSetup(presets, resolved)
|
|
1027
|
-
});
|
|
2126
|
+
handle = await resumeWith(resolved);
|
|
1028
2127
|
} catch (error) {
|
|
1029
|
-
|
|
1030
|
-
|
|
2128
|
+
try {
|
|
2129
|
+
handle = await resumeWith(previous);
|
|
2130
|
+
} catch (restoreError) {
|
|
2131
|
+
sessions.delete(acpSessionId);
|
|
2132
|
+
throw internalError(
|
|
2133
|
+
`preset switch failed: ${errorChain2(error)}; restore also failed: ${errorChain2(restoreError)}`
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
record.agent = handle.agent;
|
|
2137
|
+
record.dispose = () => handle.dispose();
|
|
2138
|
+
delete record.selection;
|
|
2139
|
+
ensureSelection(record);
|
|
2140
|
+
setApprovalPolicy(record, record.approvals);
|
|
2141
|
+
publishCommands(acpSessionId, record);
|
|
2142
|
+
throw invalidParams(`preset "${resolved}" failed to mount: ${errorChain2(error)}`);
|
|
1031
2143
|
}
|
|
1032
2144
|
record.agent = handle.agent;
|
|
1033
2145
|
record.dispose = () => handle.dispose();
|
|
1034
2146
|
record.preset = resolved;
|
|
2147
|
+
try {
|
|
2148
|
+
handle.agent.session.append("agent-preset/selected", { agentPreset: resolved });
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
logWarn(`recording preset selection failed: ${String(error)}`);
|
|
2151
|
+
}
|
|
1035
2152
|
delete record.selection;
|
|
1036
2153
|
ensureSelection(record);
|
|
1037
2154
|
setApprovalPolicy(record, record.approvals);
|
|
@@ -1039,16 +2156,16 @@ async function apply(ctx, config = {}) {
|
|
|
1039
2156
|
};
|
|
1040
2157
|
const setApprovalPolicy = (record, policy) => {
|
|
1041
2158
|
try {
|
|
1042
|
-
|
|
2159
|
+
approval.setPolicy(record.agent, policy);
|
|
1043
2160
|
} catch (error) {
|
|
1044
2161
|
logWarn(`approval policy switch failed: ${String(error)}`);
|
|
1045
2162
|
}
|
|
1046
2163
|
record.approvals = policy;
|
|
1047
2164
|
};
|
|
1048
|
-
const permissionService = () =>
|
|
2165
|
+
const permissionService = () => permissionPresets;
|
|
1049
2166
|
const applyMode = (record, sessionId, modeId) => {
|
|
1050
2167
|
const permission = permissionService();
|
|
1051
|
-
if (permission
|
|
2168
|
+
if (permission.names.includes(modeId)) {
|
|
1052
2169
|
let bundledApproval = record.approvals;
|
|
1053
2170
|
permission.apply(record.agent.session, modeId, (policy) => {
|
|
1054
2171
|
bundledApproval = policy;
|
|
@@ -1090,7 +2207,7 @@ async function apply(ctx, config = {}) {
|
|
|
1090
2207
|
const result = [];
|
|
1091
2208
|
{
|
|
1092
2209
|
const permission = permissionService();
|
|
1093
|
-
const levels = permission
|
|
2210
|
+
const levels = permission.names.length > 0 ? permission.names.map((name3) => {
|
|
1094
2211
|
const spec = permission.resolve(name3);
|
|
1095
2212
|
return { value: name3, name: spec.name, description: spec.description };
|
|
1096
2213
|
}) : SANDBOX_MODES2.map((mode) => {
|
|
@@ -1106,6 +2223,19 @@ async function apply(ctx, config = {}) {
|
|
|
1106
2223
|
options: levels
|
|
1107
2224
|
});
|
|
1108
2225
|
}
|
|
2226
|
+
if (commandRuntime.list(record.agent).some((command) => command.name === "plan")) {
|
|
2227
|
+
const active = [...record.agent.session.events].reverse().map((event) => event).find((event) => event.type === "plan/mode")?.data?.["active"] === true;
|
|
2228
|
+
result.push({
|
|
2229
|
+
type: "select",
|
|
2230
|
+
id: "collaboration_mode",
|
|
2231
|
+
name: "Collaboration mode",
|
|
2232
|
+
currentValue: active ? "plan" : "default",
|
|
2233
|
+
options: [
|
|
2234
|
+
{ value: "default", name: "Default" },
|
|
2235
|
+
{ value: "plan", name: "Plan" }
|
|
2236
|
+
]
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
1109
2239
|
const models = await modelChoices(record);
|
|
1110
2240
|
if (models.length >= 2) {
|
|
1111
2241
|
result.push({
|
|
@@ -1145,14 +2275,14 @@ async function apply(ctx, config = {}) {
|
|
|
1145
2275
|
});
|
|
1146
2276
|
}
|
|
1147
2277
|
const presets = presetsService();
|
|
1148
|
-
if (
|
|
2278
|
+
if (record.preset !== void 0) {
|
|
1149
2279
|
try {
|
|
1150
2280
|
const roster = await presets.list();
|
|
1151
2281
|
if (roster.length >= 2) {
|
|
1152
2282
|
result.push({
|
|
1153
2283
|
type: "select",
|
|
1154
|
-
id: "
|
|
1155
|
-
name: "
|
|
2284
|
+
id: "agent",
|
|
2285
|
+
name: "Agent",
|
|
1156
2286
|
currentValue: record.preset,
|
|
1157
2287
|
options: roster.map((preset) => ({
|
|
1158
2288
|
value: preset.id,
|
|
@@ -1169,12 +2299,6 @@ async function apply(ctx, config = {}) {
|
|
|
1169
2299
|
};
|
|
1170
2300
|
const BUILTIN_COMMANDS = [
|
|
1171
2301
|
{ name: "status", description: "Show adapter, model, mode, and token status" },
|
|
1172
|
-
{
|
|
1173
|
-
name: "login",
|
|
1174
|
-
description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
|
|
1175
|
-
input: { hint: "<api-key>" }
|
|
1176
|
-
},
|
|
1177
|
-
{ name: "logout", description: "Remove the API key stored in the harness credential store" },
|
|
1178
2302
|
{
|
|
1179
2303
|
name: "model",
|
|
1180
2304
|
description: "Select the model for this conversation",
|
|
@@ -1184,42 +2308,47 @@ async function apply(ctx, config = {}) {
|
|
|
1184
2308
|
const availableCommandsFor = async (record) => {
|
|
1185
2309
|
const list = [...BUILTIN_COMMANDS];
|
|
1186
2310
|
const seen = new Set(list.map((command) => command.name));
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
2311
|
+
try {
|
|
2312
|
+
for (const descriptor of commandRuntime.list(record.agent)) {
|
|
2313
|
+
if (seen.has(descriptor.name)) continue;
|
|
2314
|
+
seen.add(descriptor.name);
|
|
2315
|
+
list.push({
|
|
2316
|
+
name: descriptor.name,
|
|
2317
|
+
description: descriptor.description,
|
|
2318
|
+
...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {},
|
|
2319
|
+
...descriptor.name === "plan" ? {
|
|
2320
|
+
_meta: {
|
|
2321
|
+
commandAction: {
|
|
2322
|
+
kind: "setConfigOption",
|
|
2323
|
+
configId: "collaboration_mode",
|
|
2324
|
+
value: "plan",
|
|
2325
|
+
resetValue: "default",
|
|
2326
|
+
presentation: "state"
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
} : {}
|
|
2330
|
+
});
|
|
1201
2331
|
}
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
logWarn(`command listing failed: ${String(error)}`);
|
|
1202
2334
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
2335
|
+
try {
|
|
2336
|
+
const skills = await skillRegistry.list({
|
|
2337
|
+
...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
|
|
2338
|
+
scope: record.agent
|
|
2339
|
+
});
|
|
2340
|
+
for (const skill of skills) {
|
|
2341
|
+
if (skill.invocation.userInvocable !== true) continue;
|
|
2342
|
+
if (seen.has(skill.name)) continue;
|
|
2343
|
+
seen.add(skill.name);
|
|
2344
|
+
list.push({
|
|
2345
|
+
name: skill.name,
|
|
2346
|
+
description: skill.description,
|
|
2347
|
+
input: { hint: "instructions for the skill" }
|
|
1209
2348
|
});
|
|
1210
|
-
for (const skill of skills) {
|
|
1211
|
-
if (skill.invocation.userInvocable !== true) continue;
|
|
1212
|
-
if (seen.has(skill.name)) continue;
|
|
1213
|
-
seen.add(skill.name);
|
|
1214
|
-
list.push({
|
|
1215
|
-
name: skill.name,
|
|
1216
|
-
description: skill.description,
|
|
1217
|
-
input: { hint: "instructions for the skill" }
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
} catch (error) {
|
|
1221
|
-
logDebug(`skill listing failed: ${String(error)}`);
|
|
1222
2349
|
}
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
logDebug(`skill listing failed: ${String(error)}`);
|
|
1223
2352
|
}
|
|
1224
2353
|
return list;
|
|
1225
2354
|
};
|
|
@@ -1246,7 +2375,7 @@ async function apply(ctx, config = {}) {
|
|
|
1246
2375
|
const events = record.agent.session.events;
|
|
1247
2376
|
if (events === void 0) return;
|
|
1248
2377
|
const permission = permissionService();
|
|
1249
|
-
const storedMode = permission
|
|
2378
|
+
const storedMode = permission.current(events);
|
|
1250
2379
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1251
2380
|
const event = events[index];
|
|
1252
2381
|
if (event?.type === "approval/policy") {
|
|
@@ -1263,69 +2392,35 @@ async function apply(ctx, config = {}) {
|
|
|
1263
2392
|
});
|
|
1264
2393
|
}
|
|
1265
2394
|
};
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const viaEnv = process.env["DEEPSEEK_API_KEY"] !== void 0 ? "DEEPSEEK_API_KEY" : "DEEPSEEK_BASE_URL";
|
|
1276
|
-
return `process environment (${viaEnv})`;
|
|
1277
|
-
}
|
|
1278
|
-
const cs = credentialStore();
|
|
1279
|
-
if (cs === void 0) return "not configured";
|
|
1280
|
-
try {
|
|
1281
|
-
const info = await cs.store.describe(cs.ref);
|
|
1282
|
-
if (!info.configured) return "not configured";
|
|
1283
|
-
return info.source ?? "credential store";
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
logWarn(`credential describe failed: ${String(error)}`);
|
|
1286
|
-
return "unknown";
|
|
2395
|
+
const describeCredential = async (provider) => {
|
|
2396
|
+
const names = credentialEnvNames(provider).filter((candidate) => !candidate.endsWith("_BASE_URL"));
|
|
2397
|
+
for (const name3 of names) {
|
|
2398
|
+
try {
|
|
2399
|
+
const info = await credentials.describe(credentialRef(name3));
|
|
2400
|
+
if (info.configured) return info.source ?? "credential store";
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
logWarn(`credential describe failed: ${String(error)}`);
|
|
2403
|
+
}
|
|
1287
2404
|
}
|
|
2405
|
+
return "not configured";
|
|
1288
2406
|
};
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
const cs = credentialStore();
|
|
1292
|
-
if (cs === void 0) {
|
|
1293
|
-
return "This composition has no writable credential store; export DEEPSEEK_API_KEY in the environment that launches the agent instead.";
|
|
1294
|
-
}
|
|
1295
|
-
if (key.length === 0) {
|
|
1296
|
-
return [
|
|
1297
|
-
"Usage: `/login <api-key>` \u2014 stores the key in the harness credential store",
|
|
1298
|
-
"(`~/.dsh/.credentials.yaml`, the same file the dsh Web UI writes; mode 600).",
|
|
1299
|
-
"",
|
|
1300
|
-
`Current credential source: ${await describeCredential()}.`,
|
|
1301
|
-
"",
|
|
1302
|
-
"Note: text entered here also lands in your client's chat history. If you",
|
|
1303
|
-
"prefer to keep the key out of it, save the key in the dsh Web UI instead."
|
|
1304
|
-
].join("\n");
|
|
1305
|
-
}
|
|
1306
|
-
try {
|
|
1307
|
-
await cs.store.set(cs.ref, key);
|
|
1308
|
-
} catch (error) {
|
|
1309
|
-
return [
|
|
1310
|
-
`Could not store the key: ${error instanceof Error ? error.message : String(error)}`,
|
|
1311
|
-
"",
|
|
1312
|
-
"A read-only source (usually the process environment) is currently supplying",
|
|
1313
|
-
"this credential, so a stored value would be shadowed. Unset DEEPSEEK_API_KEY",
|
|
1314
|
-
"in the launching environment, or keep using it as the credential."
|
|
1315
|
-
].join("\n");
|
|
1316
|
-
}
|
|
1317
|
-
return `Saved DEEPSEEK_API_KEY (${maskKey(key)}) to the harness credential store \u2014 source now: ${await describeCredential()}. The dsh Web UI sees the same key.`;
|
|
2407
|
+
const saveCredential = async (provider, key) => {
|
|
2408
|
+
await credentials.set(credentialRef(primaryCredentialName(provider)), key);
|
|
1318
2409
|
};
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
2410
|
+
const saveGateway = async (provider, key, baseUrl) => {
|
|
2411
|
+
await saveCredential(provider, key);
|
|
2412
|
+
await credentials.set(credentialRef(credentialBaseUrlName(provider)), baseUrl);
|
|
2413
|
+
};
|
|
2414
|
+
const logoutStored = async () => {
|
|
2415
|
+
const routes = await listProviderRoutes();
|
|
2416
|
+
const providers = routes.length > 0 ? routes.map((route) => route.id) : [config.provider];
|
|
2417
|
+
for (const provider of providers) {
|
|
2418
|
+
try {
|
|
2419
|
+
await credentials.unset(credentialRef(primaryCredentialName(provider)));
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
logWarn(`credential unset failed: ${String(error)}`);
|
|
2422
|
+
}
|
|
1326
2423
|
}
|
|
1327
|
-
const remaining = await describeCredential();
|
|
1328
|
-
return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
|
|
1329
2424
|
};
|
|
1330
2425
|
const modelCommandText = async (record, acpSessionId, query) => {
|
|
1331
2426
|
const discovered = await discoverModels();
|
|
@@ -1382,7 +2477,7 @@ async function apply(ctx, config = {}) {
|
|
|
1382
2477
|
`| Provider | ${route.provider ?? "(composition default)"} |`,
|
|
1383
2478
|
`| Model | ${route.model ?? "(composition default)"} |`,
|
|
1384
2479
|
`| Reasoning | ${effort ?? "(adapter default)"} |`,
|
|
1385
|
-
`| Credential | ${await describeCredential()} |`,
|
|
2480
|
+
`| Credential | ${await describeCredential(route.provider)} |`,
|
|
1386
2481
|
...record.preset !== void 0 ? [`| Preset | ${record.preset} |`] : [],
|
|
1387
2482
|
`| Permission mode | ${record.modeId} |`,
|
|
1388
2483
|
`| Approvals | ${record.approvals} |`,
|
|
@@ -1407,16 +2502,27 @@ async function apply(ctx, config = {}) {
|
|
|
1407
2502
|
inflight: void 0
|
|
1408
2503
|
};
|
|
1409
2504
|
sessions.set(sessionId, record);
|
|
2505
|
+
watchSubagentParent(agent);
|
|
1410
2506
|
return record;
|
|
1411
2507
|
};
|
|
1412
2508
|
ctx.on("session/event", (session, event) => {
|
|
1413
2509
|
const sessionId = String(session.header.id);
|
|
1414
2510
|
const record = sessions.get(sessionId);
|
|
1415
|
-
if (record === void 0 || record.agent.session !== session)
|
|
2511
|
+
if (record === void 0 || record.agent.session !== session) {
|
|
2512
|
+
const child = subagentByChild.get(sessionId);
|
|
2513
|
+
if (child !== void 0 && clientSubagentTranscript) {
|
|
2514
|
+
for (const update of child.projection.onEvent(event)) {
|
|
2515
|
+
notify(child.rootSessionId, update);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
1416
2520
|
try {
|
|
1417
|
-
|
|
2521
|
+
const harnessEvent = event;
|
|
2522
|
+
for (const update of record.projection.onEvent(harnessEvent)) {
|
|
1418
2523
|
notify(sessionId, update);
|
|
1419
2524
|
}
|
|
2525
|
+
if (harnessEvent.type === "plan/mode") publishConfigOptions(sessionId, record);
|
|
1420
2526
|
} finally {
|
|
1421
2527
|
const inflight = record.inflight;
|
|
1422
2528
|
if (inflight !== void 0 && event.type === "turn/end" && inflight.turn === event.data.turn) {
|
|
@@ -1453,11 +2559,7 @@ async function apply(ctx, config = {}) {
|
|
|
1453
2559
|
}).then(({ outcome }) => {
|
|
1454
2560
|
if (outcome.outcome === "cancelled") return "cancelled";
|
|
1455
2561
|
if (outcome.optionId === "allow-always") {
|
|
1456
|
-
|
|
1457
|
-
ctx.get("approval")?.setPolicy?.(record.agent, "never");
|
|
1458
|
-
} catch (error) {
|
|
1459
|
-
logWarn(`approval policy switch failed: ${String(error)}`);
|
|
1460
|
-
}
|
|
2562
|
+
setApprovalPolicy(record, "never");
|
|
1461
2563
|
return "allowed-once";
|
|
1462
2564
|
}
|
|
1463
2565
|
return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
|
|
@@ -1466,56 +2568,103 @@ async function apply(ctx, config = {}) {
|
|
|
1466
2568
|
const makeAgent = (connection) => {
|
|
1467
2569
|
conn = connection;
|
|
1468
2570
|
return {
|
|
1469
|
-
initialize(params) {
|
|
1470
|
-
const hasPersistence = ctx.get("sessionPersistence") !== void 0;
|
|
2571
|
+
async initialize(params) {
|
|
1471
2572
|
const requested = params.protocolVersion;
|
|
1472
2573
|
const capsMeta = params.clientCapabilities?.["_meta"];
|
|
1473
2574
|
clientTerminalOutput = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["terminal_output"] === true : false;
|
|
1474
|
-
|
|
2575
|
+
clientSubagentTranscript = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["subagent-transcript"] === true : false;
|
|
2576
|
+
clientElicitationForm = params.clientCapabilities?.elicitation?.form !== void 0 && params.clientCapabilities.elicitation.form !== null;
|
|
2577
|
+
if (capsMeta !== null && typeof capsMeta === "object" && capsMeta["tuiInspect"] === true) {
|
|
2578
|
+
tuiClient.advertised = true;
|
|
2579
|
+
}
|
|
2580
|
+
const providers = await listProviderRoutes();
|
|
2581
|
+
return {
|
|
1475
2582
|
protocolVersion: typeof requested === "number" && requested >= 1 && requested < PROTOCOL_VERSION ? requested : PROTOCOL_VERSION,
|
|
1476
2583
|
agentInfo: { name: "dsh-acp", title: "DeepSeek Harness", version: VERSION },
|
|
1477
2584
|
agentCapabilities: {
|
|
1478
|
-
loadSession:
|
|
1479
|
-
promptCapabilities: {
|
|
2585
|
+
loadSession: true,
|
|
2586
|
+
promptCapabilities: {
|
|
2587
|
+
image: attachmentIngestOf(ctx.get("attachments")) !== void 0,
|
|
2588
|
+
audio: false,
|
|
2589
|
+
embeddedContext: true
|
|
2590
|
+
},
|
|
1480
2591
|
// Stdio servers always work; streamable HTTP maps onto
|
|
1481
2592
|
// mcp-client's second transport. Legacy SSE does not.
|
|
1482
2593
|
mcpCapabilities: { http: true, sse: false },
|
|
1483
|
-
|
|
2594
|
+
auth: { logout: {} },
|
|
2595
|
+
sessionCapabilities: { list: {} }
|
|
1484
2596
|
},
|
|
1485
|
-
authMethods:
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
{
|
|
1491
|
-
id: "terminal-login",
|
|
1492
|
-
name: "Log in with a DeepSeek API key",
|
|
1493
|
-
description: "Interactive terminal setup \u2014 saves the key to the harness credential store shared with the dsh Web UI",
|
|
1494
|
-
_meta: { "terminal-auth": { args: ["login"], env: {} } }
|
|
1495
|
-
}
|
|
1496
|
-
]
|
|
1497
|
-
});
|
|
2597
|
+
authMethods: advertisedAuthMethods(
|
|
2598
|
+
providers,
|
|
2599
|
+
params.clientCapabilities
|
|
2600
|
+
)
|
|
2601
|
+
};
|
|
1498
2602
|
},
|
|
1499
|
-
async authenticate(
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
2603
|
+
async authenticate(params) {
|
|
2604
|
+
const gateway = gatewayFromAuthenticate(params);
|
|
2605
|
+
if (isGatewayAuthMethod(params.methodId) || gateway.baseUrl) {
|
|
2606
|
+
if (!gateway.baseUrl || !gateway.key) {
|
|
2607
|
+
throw authRequired(
|
|
2608
|
+
"authenticate gateway requires `_meta.gateway.baseUrl` and an Authorization header"
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
await saveGateway(
|
|
2612
|
+
gateway.providerName ?? config.provider,
|
|
2613
|
+
gateway.key,
|
|
2614
|
+
gateway.baseUrl
|
|
1503
2615
|
);
|
|
2616
|
+
return;
|
|
1504
2617
|
}
|
|
2618
|
+
const submitted = apiKeyFromAuthenticate(params);
|
|
2619
|
+
const provider = submitted.provider ?? providerFromAuthMethodId(params.methodId) ?? config.provider;
|
|
2620
|
+
if (submitted.key) {
|
|
2621
|
+
await saveCredential(provider, submitted.key);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (await credentialPresent(provider)) return;
|
|
2625
|
+
if (isBrowserAuthMethod(params.methodId) && shouldOfferLocalAuthPage()) {
|
|
2626
|
+
const page = await startLocalAuthPage({
|
|
2627
|
+
credentialName: primaryCredentialName(provider)
|
|
2628
|
+
});
|
|
2629
|
+
openLocalAuthPage(page.url);
|
|
2630
|
+
try {
|
|
2631
|
+
const key = await page.completed;
|
|
2632
|
+
await saveCredential(provider, key);
|
|
2633
|
+
return;
|
|
2634
|
+
} finally {
|
|
2635
|
+
page.close();
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
await requireCredential(provider);
|
|
2639
|
+
},
|
|
2640
|
+
async logout(_params) {
|
|
2641
|
+
await logoutStored();
|
|
1505
2642
|
},
|
|
1506
2643
|
async newSession(params) {
|
|
1507
2644
|
assertOpen();
|
|
2645
|
+
const started = performance.now();
|
|
2646
|
+
let checkpoint = started;
|
|
2647
|
+
const mark = (name3) => {
|
|
2648
|
+
const now = performance.now();
|
|
2649
|
+
logDebug(`session/new ${name3}: ${(now - checkpoint).toFixed(1)}ms (+${(now - started).toFixed(1)}ms)`);
|
|
2650
|
+
checkpoint = now;
|
|
2651
|
+
};
|
|
2652
|
+
await requireCredential(config.provider);
|
|
2653
|
+
mark("credential");
|
|
1508
2654
|
validateCwd(params.cwd);
|
|
1509
2655
|
syncMcpServers(params.mcpServers, params.cwd);
|
|
2656
|
+
mark("request setup");
|
|
1510
2657
|
const sessionId = SessionId2(randomUUID());
|
|
1511
2658
|
const presets = presetsService();
|
|
1512
|
-
const presetId =
|
|
2659
|
+
const presetId = (await presets.resolve(void 0)).id;
|
|
2660
|
+
mark("preset resolve");
|
|
1513
2661
|
const handle = await agents.create({
|
|
1514
2662
|
sessionId,
|
|
1515
2663
|
meta: { cwd: params.cwd, ...presetId !== void 0 ? { agentPreset: presetId } : {} },
|
|
1516
2664
|
agentOptions: agentOptionsFor(config.model),
|
|
1517
2665
|
...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
|
|
1518
2666
|
});
|
|
2667
|
+
mark("agent create");
|
|
1519
2668
|
if (closed) {
|
|
1520
2669
|
await handle.dispose();
|
|
1521
2670
|
throw internalError("connection closed during session/new");
|
|
@@ -1531,6 +2680,7 @@ async function apply(ctx, config = {}) {
|
|
|
1531
2680
|
ensureSelection(record);
|
|
1532
2681
|
queueMicrotask(() => publishCommands(String(sessionId)));
|
|
1533
2682
|
const options = await configOptions(record);
|
|
2683
|
+
mark("response surface");
|
|
1534
2684
|
return {
|
|
1535
2685
|
sessionId: String(sessionId),
|
|
1536
2686
|
modes: modeState(record),
|
|
@@ -1539,6 +2689,7 @@ async function apply(ctx, config = {}) {
|
|
|
1539
2689
|
},
|
|
1540
2690
|
async loadSession(params) {
|
|
1541
2691
|
assertOpen();
|
|
2692
|
+
await requireCredential(config.provider);
|
|
1542
2693
|
validateCwd(params.cwd);
|
|
1543
2694
|
syncMcpServers(params.mcpServers, params.cwd);
|
|
1544
2695
|
requirePersistence();
|
|
@@ -1577,17 +2728,36 @@ async function apply(ctx, config = {}) {
|
|
|
1577
2728
|
async prompt(params) {
|
|
1578
2729
|
assertOpen();
|
|
1579
2730
|
const record = await requireOrRestoreSession(params.sessionId);
|
|
1580
|
-
if (record.inflight !== void 0) {
|
|
1581
|
-
throw invalidParams("a prompt is already in flight for this session");
|
|
1582
|
-
}
|
|
1583
2731
|
let converted;
|
|
1584
2732
|
try {
|
|
1585
|
-
converted = convertPrompt(
|
|
2733
|
+
converted = await convertPrompt(
|
|
2734
|
+
params.prompt,
|
|
2735
|
+
attachmentIngestOf(ctx.get("attachments"))
|
|
2736
|
+
);
|
|
1586
2737
|
} catch (error) {
|
|
1587
|
-
if (error instanceof UnsupportedPromptContentError
|
|
2738
|
+
if (error instanceof UnsupportedPromptContentError || error instanceof PromptImageError) {
|
|
2739
|
+
throw invalidParams(error.message);
|
|
2740
|
+
}
|
|
1588
2741
|
throw error;
|
|
1589
2742
|
}
|
|
1590
2743
|
if (converted.blocks.length === 0) throw invalidParams("empty prompt");
|
|
2744
|
+
if (record.inflight !== void 0) {
|
|
2745
|
+
await requireCredential(record.provider ?? config.provider);
|
|
2746
|
+
if (agents.get(record.agent.id) !== record.agent) {
|
|
2747
|
+
throw internalError("prompt was not steered: the agent was disposed outside the bridge");
|
|
2748
|
+
}
|
|
2749
|
+
const message2 = createUserMessage2({
|
|
2750
|
+
content: converted.blocks,
|
|
2751
|
+
source: { kind: "user" }
|
|
2752
|
+
});
|
|
2753
|
+
try {
|
|
2754
|
+
deliverPrompt(record.agent, message2, true);
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2757
|
+
throw internalError(`prompt was not steered: ${detail}`);
|
|
2758
|
+
}
|
|
2759
|
+
return { stopReason: "end_turn" };
|
|
2760
|
+
}
|
|
1591
2761
|
const trimmed = converted.displayText.trim();
|
|
1592
2762
|
const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/);
|
|
1593
2763
|
const respond = (text) => {
|
|
@@ -1598,10 +2768,6 @@ async function apply(ctx, config = {}) {
|
|
|
1598
2768
|
return { stopReason: "end_turn" };
|
|
1599
2769
|
};
|
|
1600
2770
|
if (commandMatch?.[1] === "status") return respond(await statusText(record));
|
|
1601
|
-
if (commandMatch?.[1] === "login") {
|
|
1602
|
-
return respond(await loginText(trimmed.slice(commandMatch[0].length)));
|
|
1603
|
-
}
|
|
1604
|
-
if (commandMatch?.[1] === "logout") return respond(await logoutText());
|
|
1605
2771
|
if (commandMatch?.[1] === "model") {
|
|
1606
2772
|
return respond(
|
|
1607
2773
|
await modelCommandText(
|
|
@@ -1612,54 +2778,49 @@ async function apply(ctx, config = {}) {
|
|
|
1612
2778
|
);
|
|
1613
2779
|
}
|
|
1614
2780
|
if (commandMatch !== null && commandMatch[1] !== void 0) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
publishConfigOptions(params.sessionId, record);
|
|
1633
|
-
return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
|
|
1634
|
-
}
|
|
2781
|
+
let execution;
|
|
2782
|
+
try {
|
|
2783
|
+
execution = await commandRuntime.execute(
|
|
2784
|
+
record.agent,
|
|
2785
|
+
trimmed,
|
|
2786
|
+
new AbortController().signal
|
|
2787
|
+
);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
|
|
2790
|
+
}
|
|
2791
|
+
if (execution !== void 0) {
|
|
2792
|
+
const { result } = execution;
|
|
2793
|
+
const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
|
|
2794
|
+
syncPermissionState(record, params.sessionId);
|
|
2795
|
+
publishCommands(params.sessionId, record);
|
|
2796
|
+
publishConfigOptions(params.sessionId, record);
|
|
2797
|
+
return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
|
|
1635
2798
|
}
|
|
1636
2799
|
}
|
|
1637
|
-
|
|
1638
|
-
return respond(
|
|
1639
|
-
[
|
|
1640
|
-
"No DeepSeek credential is configured, so this prompt was not sent to the model.",
|
|
1641
|
-
"",
|
|
1642
|
-
"Configure one of:",
|
|
1643
|
-
"- `/login <api-key>` \u2014 store a key in the harness credential store right here,",
|
|
1644
|
-
"- the dsh Web UI (`dsh web`, Settings \u2192 Models),",
|
|
1645
|
-
"- `DEEPSEEK_API_KEY` in the environment that launches this agent."
|
|
1646
|
-
].join("\n")
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
2800
|
+
await requireCredential(record.provider ?? config.provider);
|
|
1649
2801
|
if (agents.get(record.agent.id) !== record.agent) {
|
|
1650
2802
|
throw internalError("prompt was not queued: the agent was disposed outside the bridge");
|
|
1651
2803
|
}
|
|
1652
2804
|
record.cancelled = false;
|
|
1653
|
-
record.projection.beginPrompt();
|
|
1654
2805
|
const message = createUserMessage2({
|
|
1655
2806
|
content: converted.blocks,
|
|
1656
2807
|
source: { kind: "user" }
|
|
1657
2808
|
});
|
|
2809
|
+
if (record.inflight !== void 0) {
|
|
2810
|
+
try {
|
|
2811
|
+
deliverPrompt(record.agent, message, true);
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2814
|
+
throw internalError(`prompt was not steered: ${detail}`);
|
|
2815
|
+
}
|
|
2816
|
+
return { stopReason: "end_turn" };
|
|
2817
|
+
}
|
|
2818
|
+
record.projection.beginPrompt();
|
|
1658
2819
|
const stopReason = await new Promise((resolve, reject) => {
|
|
1659
2820
|
const inflight = { resolve, reject, messageId: message.id, turn: void 0 };
|
|
1660
2821
|
record.inflight = inflight;
|
|
1661
2822
|
try {
|
|
1662
|
-
record.agent
|
|
2823
|
+
deliverPrompt(record.agent, message, false);
|
|
1663
2824
|
} catch (error) {
|
|
1664
2825
|
record.inflight = void 0;
|
|
1665
2826
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -1697,6 +2858,7 @@ async function apply(ctx, config = {}) {
|
|
|
1697
2858
|
applyMode(record, params.sessionId, value);
|
|
1698
2859
|
break;
|
|
1699
2860
|
}
|
|
2861
|
+
case "agent":
|
|
1700
2862
|
case "preset": {
|
|
1701
2863
|
await switchPreset(record, params.sessionId, value);
|
|
1702
2864
|
break;
|
|
@@ -1719,6 +2881,23 @@ async function apply(ctx, config = {}) {
|
|
|
1719
2881
|
record.effort = value;
|
|
1720
2882
|
break;
|
|
1721
2883
|
}
|
|
2884
|
+
case "collaboration_mode": {
|
|
2885
|
+
if (value !== "default" && value !== "plan") {
|
|
2886
|
+
throw invalidParams(`unknown collaboration mode: ${value}`);
|
|
2887
|
+
}
|
|
2888
|
+
const execution = await commandRuntime.execute(
|
|
2889
|
+
record.agent,
|
|
2890
|
+
value === "plan" ? "/plan" : "/plan off",
|
|
2891
|
+
new AbortController().signal
|
|
2892
|
+
);
|
|
2893
|
+
if (execution === void 0) {
|
|
2894
|
+
throw invalidParams("plan mode is unavailable on this session");
|
|
2895
|
+
}
|
|
2896
|
+
if (execution.result.kind === "error") {
|
|
2897
|
+
throw invalidParams(execution.result.text);
|
|
2898
|
+
}
|
|
2899
|
+
break;
|
|
2900
|
+
}
|
|
1722
2901
|
case "model": {
|
|
1723
2902
|
await switchModel(record, params.sessionId, value);
|
|
1724
2903
|
break;
|
|
@@ -1749,22 +2928,15 @@ async function apply(ctx, config = {}) {
|
|
|
1749
2928
|
};
|
|
1750
2929
|
};
|
|
1751
2930
|
function requirePersistence() {
|
|
1752
|
-
|
|
1753
|
-
if (persistence === void 0) {
|
|
1754
|
-
throw internalError("session persistence is not composed; session history is unavailable");
|
|
1755
|
-
}
|
|
1756
|
-
return persistence;
|
|
1757
|
-
}
|
|
1758
|
-
function getPersistence() {
|
|
1759
|
-
return ctx.get("sessionPersistence");
|
|
2931
|
+
return sessionPersistence;
|
|
1760
2932
|
}
|
|
1761
2933
|
function validateCwd(cwd) {
|
|
1762
2934
|
if (!isAbsolute(cwd)) throw invalidParams(`cwd must be an absolute path: ${cwd}`);
|
|
1763
2935
|
}
|
|
1764
2936
|
const mcpMounts = /* @__PURE__ */ new Map();
|
|
1765
2937
|
let warnedNoMcpClient = false;
|
|
1766
|
-
const sanitizeServerName = (
|
|
1767
|
-
const cleaned =
|
|
2938
|
+
const sanitizeServerName = (raw2) => {
|
|
2939
|
+
const cleaned = raw2.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 32);
|
|
1768
2940
|
return cleaned.length > 0 ? cleaned : "server";
|
|
1769
2941
|
};
|
|
1770
2942
|
const mcpConfigFor = (server, cwd, serverName) => {
|
|
@@ -1845,29 +3017,38 @@ async function apply(ctx, config = {}) {
|
|
|
1845
3017
|
}
|
|
1846
3018
|
}
|
|
1847
3019
|
};
|
|
1848
|
-
const
|
|
3020
|
+
const raw = config.stream ?? ndJsonStream(
|
|
1849
3021
|
Writable.toWeb(process.stdout),
|
|
1850
3022
|
Readable.toWeb(process.stdin)
|
|
1851
3023
|
);
|
|
3024
|
+
const stream = muxAcpStream(raw, rpc);
|
|
1852
3025
|
conn = new AgentSideConnection(makeAgent, stream);
|
|
3026
|
+
const disposeUserQuestions = installAcpUserQuestionProvider(userQuestions, {
|
|
3027
|
+
formSupported: () => clientElicitationForm,
|
|
3028
|
+
sessionIdForRequest: (request) => {
|
|
3029
|
+
if (request.agent === void 0) return void 0;
|
|
3030
|
+
const record = ownedRecord(request.agent);
|
|
3031
|
+
return record === void 0 ? void 0 : String(record.agent.session.id);
|
|
3032
|
+
},
|
|
3033
|
+
create: (request) => conn.unstable_createElicitation(request)
|
|
3034
|
+
});
|
|
1853
3035
|
let quiescing;
|
|
1854
3036
|
const quiesce = () => {
|
|
1855
3037
|
if (quiescing !== void 0) return quiescing;
|
|
1856
3038
|
closed = true;
|
|
1857
3039
|
const records = [...sessions.values()];
|
|
1858
3040
|
sessions.clear();
|
|
3041
|
+
subagentByChild.clear();
|
|
3042
|
+
subagentByRun.clear();
|
|
1859
3043
|
for (const record of records) {
|
|
1860
3044
|
record.agent.cancel({ kind: "user" });
|
|
1861
3045
|
settlePrompt(record, "cancelled");
|
|
1862
3046
|
}
|
|
1863
3047
|
quiescing = (async () => {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
} catch (error) {
|
|
1869
|
-
logWarn(`continuable subagent teardown failed: ${String(error)}`);
|
|
1870
|
-
}
|
|
3048
|
+
try {
|
|
3049
|
+
await subagents.drainContinuableDescendants(records.map((record) => record.agent));
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
logWarn(`continuable subagent teardown failed: ${String(error)}`);
|
|
1871
3052
|
}
|
|
1872
3053
|
const disposals = await Promise.allSettled(records.map((record) => record.dispose()));
|
|
1873
3054
|
const failures = disposals.filter(
|
|
@@ -1889,6 +3070,7 @@ async function apply(ctx, config = {}) {
|
|
|
1889
3070
|
logWarn(`connection-close teardown failed: ${String(error)}`);
|
|
1890
3071
|
});
|
|
1891
3072
|
ctx.effect(() => quiesce, "acp-bridge.connection");
|
|
3073
|
+
ctx.effect(() => disposeUserQuestions, "acp-bridge.user-questions");
|
|
1892
3074
|
}
|
|
1893
3075
|
|
|
1894
3076
|
// src/bundle.ts
|