@openma/deepseek-harness-acp 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bridge.js ADDED
@@ -0,0 +1,1334 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name3 in all)
13
+ __defProp(target, name3, { get: all[name3], enumerable: true });
14
+ };
15
+
16
+ // src/bridge/self-harness.ts
17
+ var self_harness_exports = {};
18
+ __export(self_harness_exports, {
19
+ selfHarness: () => selfHarness
20
+ });
21
+ import { createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
22
+ import { SessionId } from "@deepseek-ai/dsh-session";
23
+ import { foldSessionTitle } from "@deepseek-ai/dsh-session-title";
24
+ import { SANDBOX_MODES, setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
25
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
26
+ import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
27
+ function selfHarness() {
28
+ return {
29
+ createUserMessage,
30
+ errorChain,
31
+ sessionId: SessionId,
32
+ foldSessionTitle,
33
+ setSandboxMode,
34
+ sandboxModes: SANDBOX_MODES,
35
+ credentialRef,
36
+ mcpClient
37
+ };
38
+ }
39
+ var init_self_harness = __esm({
40
+ "src/bridge/self-harness.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
45
+ // src/bridge/index.ts
46
+ import { randomUUID } from "node:crypto";
47
+ import { isAbsolute } from "node:path";
48
+ import { Readable, Writable } from "node:stream";
49
+ import {
50
+ AgentSideConnection,
51
+ ndJsonStream,
52
+ PROTOCOL_VERSION,
53
+ RequestError
54
+ } from "@agentclientprotocol/sdk";
55
+
56
+ // src/version.ts
57
+ var VERSION = "0.1.0";
58
+
59
+ // src/log.ts
60
+ var PREFIX = "[dsh-acp]";
61
+ function logWarn(message) {
62
+ process.stderr.write(`${PREFIX} warn: ${message}
63
+ `);
64
+ }
65
+ function logDebug(message) {
66
+ if (process.env["DSH_ACP_DEBUG"]) process.stderr.write(`${PREFIX} debug: ${message}
67
+ `);
68
+ }
69
+
70
+ // src/bridge/translate.ts
71
+ var MAX_TITLE_LENGTH = 80;
72
+ var MAX_RESULT_TEXT = 2e4;
73
+ function firstLine(text, limit = MAX_TITLE_LENGTH) {
74
+ const line = text.split("\n", 1)[0] ?? "";
75
+ return line.length > limit ? `${line.slice(0, limit - 1)}\u2026` : line;
76
+ }
77
+ function asString(value) {
78
+ return typeof value === "string" && value.length > 0 ? value : void 0;
79
+ }
80
+ function pathLocations(args) {
81
+ const locations = [];
82
+ for (const key of ["path", "file_path", "filePath", "file"]) {
83
+ const path = asString(args[key]);
84
+ if (path !== void 0) {
85
+ locations.push({ path });
86
+ break;
87
+ }
88
+ }
89
+ return locations;
90
+ }
91
+ function classifyToolCall(name3, rawArguments) {
92
+ let args = {};
93
+ try {
94
+ const parsed = JSON.parse(rawArguments);
95
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
96
+ args = parsed;
97
+ }
98
+ } catch {
99
+ args = { arguments: rawArguments };
100
+ }
101
+ const locations = pathLocations(args);
102
+ const path = locations[0]?.path;
103
+ const facts = (kind, title) => ({
104
+ kind,
105
+ title,
106
+ locations,
107
+ rawInput: args
108
+ });
109
+ switch (name3) {
110
+ case "bash": {
111
+ const command = asString(args["command"]);
112
+ const restart = args["restart"] === true;
113
+ return facts("execute", command !== void 0 ? firstLine(command) : restart ? "Restart bash" : "bash");
114
+ }
115
+ case "read":
116
+ return facts("read", path !== void 0 ? `Read ${path}` : "Read file");
117
+ case "write":
118
+ return facts("edit", path !== void 0 ? `Write ${path}` : "Write file");
119
+ case "edit":
120
+ return facts("edit", path !== void 0 ? `Edit ${path}` : "Edit file");
121
+ case "str_replace_editor": {
122
+ const command = asString(args["command"]) ?? "edit";
123
+ const kind = command === "view" ? "read" : "edit";
124
+ return facts(kind, path !== void 0 ? `${command} ${path}` : command);
125
+ }
126
+ case "todo_write":
127
+ return facts("think", "Update plan");
128
+ case "subagent":
129
+ case "subagent_fork": {
130
+ const task = asString(args["task"]) ?? asString(args["prompt"]);
131
+ return facts("other", task !== void 0 ? `Subagent: ${firstLine(task, 60)}` : "Subagent");
132
+ }
133
+ case "skill": {
134
+ const skillName = asString(args["name"]) ?? asString(args["skill"]);
135
+ return facts("other", skillName !== void 0 ? `Skill: ${skillName}` : "Skill");
136
+ }
137
+ default:
138
+ break;
139
+ }
140
+ const lowered = name3.toLowerCase();
141
+ if (/(^|_)(grep|glob|find|search|ls|list)($|_)/.test(lowered)) {
142
+ const query = asString(args["query"]) ?? asString(args["pattern"]);
143
+ return facts("search", query !== void 0 ? `Search for '${firstLine(query, 50)}'` : name3);
144
+ }
145
+ if (/(fetch|web|http|browse)/.test(lowered)) {
146
+ const url = asString(args["url"]) ?? asString(args["query"]);
147
+ return facts("fetch", url !== void 0 ? `Fetch ${firstLine(url, 60)}` : name3);
148
+ }
149
+ if (/(job|task|run|exec)/.test(lowered)) return facts("execute", name3);
150
+ return facts("other", name3);
151
+ }
152
+ function diffsFromToolMeta(meta) {
153
+ if (meta === null || typeof meta !== "object") return [];
154
+ const diffs = meta["diffs"];
155
+ if (!Array.isArray(diffs)) return [];
156
+ const result = [];
157
+ for (const entry of diffs) {
158
+ if (entry === null || typeof entry !== "object") continue;
159
+ const diff = entry;
160
+ const path = asString(diff["path"]);
161
+ const newText = diff["newText"];
162
+ const oldText = diff["oldText"];
163
+ if (path === void 0 || typeof newText !== "string") continue;
164
+ result.push({ path, oldText: typeof oldText === "string" ? oldText : null, newText });
165
+ }
166
+ return result;
167
+ }
168
+ function extractToolResult(data) {
169
+ let failed = data["error"] !== void 0 && data["error"] !== null;
170
+ const parts = [];
171
+ const message = data["message"];
172
+ const content = message !== null && typeof message === "object" ? message["content"] : void 0;
173
+ if (Array.isArray(content)) {
174
+ for (const block of content) {
175
+ if (block === null || typeof block !== "object") continue;
176
+ const b = block;
177
+ if (b["type"] !== "tool-result") continue;
178
+ if (b["isError"] === true) failed = true;
179
+ const inner = b["content"];
180
+ if (!Array.isArray(inner)) continue;
181
+ for (const innerBlock of inner) {
182
+ if (innerBlock === null || typeof innerBlock !== "object") continue;
183
+ const ib = innerBlock;
184
+ if (ib["type"] === "text" && typeof ib["text"] === "string") parts.push(ib["text"]);
185
+ }
186
+ }
187
+ }
188
+ let text = parts.join("");
189
+ if (text.length > MAX_RESULT_TEXT) text = `${text.slice(0, MAX_RESULT_TEXT)}
190
+ \u2026 (truncated)`;
191
+ return { failed, text, diffs: diffsFromToolMeta(data["meta"]) };
192
+ }
193
+ function extractMessageTexts(message) {
194
+ const texts = [];
195
+ const reasoning = [];
196
+ const content = message !== null && typeof message === "object" ? message["content"] : void 0;
197
+ if (Array.isArray(content)) {
198
+ for (const block of content) {
199
+ if (block === null || typeof block !== "object") continue;
200
+ const b = block;
201
+ if (b["type"] === "text" && typeof b["text"] === "string") texts.push(b["text"]);
202
+ else if (b["type"] === "reasoning" && typeof b["text"] === "string") reasoning.push(b["text"]);
203
+ else if (b["type"] === "image") texts.push("[image attachment]");
204
+ }
205
+ }
206
+ return { text: texts.join(""), reasoning: reasoning.join("") };
207
+ }
208
+ function planEntries(data) {
209
+ const todos = data["todos"];
210
+ if (!Array.isArray(todos)) return [];
211
+ const entries = [];
212
+ for (const todo of todos) {
213
+ if (todo === null || typeof todo !== "object") continue;
214
+ const t = todo;
215
+ const content = asString(t["content"]);
216
+ const status = t["status"];
217
+ if (content === void 0) continue;
218
+ entries.push({
219
+ content,
220
+ priority: "medium",
221
+ status: status === "in_progress" || status === "completed" ? status : "pending"
222
+ });
223
+ }
224
+ return entries;
225
+ }
226
+ function turnEndToStopReason(reason) {
227
+ switch (reason?.kind) {
228
+ case "completed":
229
+ return "end_turn";
230
+ case "max-tokens":
231
+ return "max_tokens";
232
+ case "blocked":
233
+ return "refusal";
234
+ case "interrupted":
235
+ return "cancelled";
236
+ case "aborted": {
237
+ const cause = reason.reason?.kind;
238
+ return cause === "user" || cause === "disposed" ? "cancelled" : "end_turn";
239
+ }
240
+ default:
241
+ return "end_turn";
242
+ }
243
+ }
244
+ function emptyWindow() {
245
+ return {
246
+ usage: { input: 0, output: 0, cachedRead: 0, cachedWrite: 0, thought: 0 },
247
+ sawUsage: false,
248
+ turnEnds: /* @__PURE__ */ new Map(),
249
+ lastTurnEnd: void 0,
250
+ error: void 0
251
+ };
252
+ }
253
+ var SessionProjection = class {
254
+ /** Context capacity from the latest `request/context` event, when known. */
255
+ contextWindow;
256
+ /** Latest `session/title` text observed on the log. */
257
+ title;
258
+ streamedText = /* @__PURE__ */ new Set();
259
+ streamedReasoning = /* @__PURE__ */ new Set();
260
+ toolCalls = /* @__PURE__ */ new Map();
261
+ window = emptyWindow();
262
+ lastContextUse;
263
+ constructor(contextWindow) {
264
+ this.contextWindow = contextWindow;
265
+ }
266
+ /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
267
+ beginPrompt() {
268
+ this.window = emptyWindow();
269
+ }
270
+ /** The captured ending for a specific turn, when it already ended. */
271
+ turnEndFor(turn) {
272
+ return this.window.turnEnds.get(turn);
273
+ }
274
+ /** The last turn ending inside the current prompt window. */
275
+ get lastTurnEnd() {
276
+ return this.window.lastTurnEnd;
277
+ }
278
+ /** The first failed-turn error inside the current prompt window. */
279
+ get turnError() {
280
+ return this.window.error;
281
+ }
282
+ /** Accumulated ACP usage for the current prompt window. */
283
+ promptUsage() {
284
+ if (!this.window.sawUsage) return void 0;
285
+ const { input, output, cachedRead, cachedWrite, thought } = this.window.usage;
286
+ return {
287
+ totalTokens: input + output + cachedRead + cachedWrite,
288
+ inputTokens: input,
289
+ outputTokens: output,
290
+ ...thought > 0 ? { thoughtTokens: thought } : {},
291
+ ...cachedRead > 0 ? { cachedReadTokens: cachedRead } : {},
292
+ ...cachedWrite > 0 ? { cachedWriteTokens: cachedWrite } : {}
293
+ };
294
+ }
295
+ /**
296
+ * Project one session event onto zero or more ACP updates.
297
+ */
298
+ onEvent(event) {
299
+ const data = event.data ?? {};
300
+ switch (event.type) {
301
+ case "assistant/chunk":
302
+ return this.onChunk(data);
303
+ case "assistant/message":
304
+ return this.onAssistantMessage(data);
305
+ case "tool/call":
306
+ return this.onToolCall(data);
307
+ case "tool/result":
308
+ return this.onToolResult(data);
309
+ case "todo/write": {
310
+ const entries = planEntries(data);
311
+ return entries.length > 0 ? [{ sessionUpdate: "plan", entries }] : [];
312
+ }
313
+ case "turn/end":
314
+ return this.onTurnEnd(data);
315
+ case "request/context": {
316
+ const contextWindow = data["contextWindow"];
317
+ if (typeof contextWindow === "number" && contextWindow > 0) this.contextWindow = contextWindow;
318
+ return [];
319
+ }
320
+ case "session/title": {
321
+ const title = asString(data["title"]);
322
+ if (title === void 0 || title === this.title) return [];
323
+ this.title = title;
324
+ return [{ sessionUpdate: "session_info_update", title, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }];
325
+ }
326
+ default:
327
+ return [];
328
+ }
329
+ }
330
+ stepKey(data) {
331
+ return `${String(data["turn"])}:${String(data["step"])}`;
332
+ }
333
+ onChunk(data) {
334
+ const chunk = data["chunk"];
335
+ if (chunk === null || typeof chunk === "undefined" || typeof chunk !== "object") return [];
336
+ const c = chunk;
337
+ const text = c["text"];
338
+ if (typeof text !== "string" || text.length === 0) return [];
339
+ if (c["type"] === "text-delta") {
340
+ this.streamedText.add(this.stepKey(data));
341
+ return [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }];
342
+ }
343
+ if (c["type"] === "reasoning-delta") {
344
+ this.streamedReasoning.add(this.stepKey(data));
345
+ return [{ sessionUpdate: "agent_thought_chunk", content: { type: "text", text } }];
346
+ }
347
+ return [];
348
+ }
349
+ onAssistantMessage(data) {
350
+ const updates = [];
351
+ const key = this.stepKey(data);
352
+ const { text, reasoning } = extractMessageTexts(data["message"]);
353
+ if (reasoning.length > 0 && !this.streamedReasoning.has(key)) {
354
+ updates.push({ sessionUpdate: "agent_thought_chunk", content: { type: "text", text: reasoning } });
355
+ }
356
+ if (text.length > 0 && !this.streamedText.has(key)) {
357
+ updates.push({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
358
+ }
359
+ this.streamedText.delete(key);
360
+ this.streamedReasoning.delete(key);
361
+ const usage = data["usage"];
362
+ if (usage !== null && typeof usage === "object") {
363
+ const u = usage;
364
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
365
+ const input = num(u["inputTokens"]);
366
+ const output = num(u["outputTokens"]);
367
+ const cachedRead = num(u["cacheReadTokens"]);
368
+ const cachedWrite = num(u["cacheWriteTokens"]);
369
+ this.window.usage.input += input;
370
+ this.window.usage.output += output;
371
+ this.window.usage.cachedRead += cachedRead;
372
+ this.window.usage.cachedWrite += cachedWrite;
373
+ this.window.usage.thought += num(u["reasoningTokens"]);
374
+ this.window.sawUsage = true;
375
+ this.lastContextUse = input + cachedRead + cachedWrite + output;
376
+ if (this.contextWindow !== void 0 && this.lastContextUse > 0) {
377
+ updates.push({
378
+ sessionUpdate: "usage_update",
379
+ used: this.lastContextUse,
380
+ size: this.contextWindow
381
+ });
382
+ }
383
+ }
384
+ return updates;
385
+ }
386
+ onToolCall(data) {
387
+ const callId = asString(data["callId"]);
388
+ const name3 = asString(data["name"]) ?? "tool";
389
+ if (callId === void 0) return [];
390
+ const rawArguments = typeof data["arguments"] === "string" ? data["arguments"] : "{}";
391
+ const facts = classifyToolCall(name3, rawArguments);
392
+ this.toolCalls.set(callId, facts);
393
+ return [
394
+ {
395
+ sessionUpdate: "tool_call",
396
+ toolCallId: callId,
397
+ title: facts.title,
398
+ name: name3,
399
+ kind: facts.kind,
400
+ status: "in_progress",
401
+ rawInput: facts.rawInput,
402
+ ...facts.locations.length > 0 ? { locations: facts.locations } : {}
403
+ }
404
+ ];
405
+ }
406
+ onToolResult(data) {
407
+ const message = data["message"];
408
+ const callId = asString(
409
+ message !== null && typeof message === "object" ? (() => {
410
+ const content2 = message["content"];
411
+ if (!Array.isArray(content2)) return void 0;
412
+ const block = content2.find(
413
+ (b) => b !== null && typeof b === "object" && b["type"] === "tool-result"
414
+ );
415
+ return block === void 0 ? void 0 : block["toolCallId"];
416
+ })() : void 0
417
+ ) ?? asString(data["callId"]);
418
+ if (callId === void 0) return [];
419
+ const { failed, text, diffs } = extractToolResult(data);
420
+ const content = diffs.map((diff) => ({
421
+ type: "diff",
422
+ path: diff.path,
423
+ ...diff.oldText !== null ? { oldText: diff.oldText } : {},
424
+ newText: diff.newText
425
+ }));
426
+ if (text.length > 0 && diffs.length === 0) {
427
+ const block = { type: "text", text };
428
+ content.push({ type: "content", content: block });
429
+ }
430
+ this.toolCalls.delete(callId);
431
+ return [
432
+ {
433
+ sessionUpdate: "tool_call_update",
434
+ toolCallId: callId,
435
+ status: failed ? "failed" : "completed",
436
+ ...content.length > 0 ? { content } : {},
437
+ ...text.length > 0 ? { rawOutput: { output: text, isError: failed } } : {}
438
+ }
439
+ ];
440
+ }
441
+ onTurnEnd(data) {
442
+ const reason = data["reason"];
443
+ const turn = data["turn"];
444
+ if (reason !== null && typeof reason === "object") {
445
+ const r = reason;
446
+ if (typeof turn === "number") this.window.turnEnds.set(turn, r);
447
+ this.window.lastTurnEnd = r;
448
+ if (r.kind === "error" && this.window.error === void 0) {
449
+ this.window.error = { message: r.error?.message ?? "model turn failed" };
450
+ }
451
+ }
452
+ return [];
453
+ }
454
+ };
455
+
456
+ // src/bridge/history.ts
457
+ function userText(data) {
458
+ if (data === void 0) return void 0;
459
+ const source = data["source"];
460
+ const kind = source !== null && typeof source === "object" ? source["kind"] : void 0;
461
+ if (kind !== "user") return void 0;
462
+ const content = data["content"];
463
+ if (!Array.isArray(content)) return void 0;
464
+ const parts = [];
465
+ for (const block of content) {
466
+ if (block !== null && typeof block === "object") {
467
+ const b = block;
468
+ if (b["type"] === "text" && typeof b["text"] === "string") parts.push(b["text"]);
469
+ }
470
+ }
471
+ const text = parts.join("");
472
+ return text.length > 0 ? text : void 0;
473
+ }
474
+ function buildReplay(events) {
475
+ const projection = new SessionProjection();
476
+ const updates = [];
477
+ let plan;
478
+ let title;
479
+ for (const event of events) {
480
+ if (event.type === "user/message") {
481
+ const text = userText(event.data);
482
+ if (text !== void 0) {
483
+ updates.push({ sessionUpdate: "user_message_chunk", content: { type: "text", text } });
484
+ }
485
+ continue;
486
+ }
487
+ if (event.type === "session/title") {
488
+ const t = event.data?.["title"];
489
+ if (typeof t === "string" && t.length > 0) title = t;
490
+ continue;
491
+ }
492
+ for (const update of projection.onEvent(event)) {
493
+ if (update.sessionUpdate === "usage_update") continue;
494
+ if (update.sessionUpdate === "plan") {
495
+ plan = update;
496
+ continue;
497
+ }
498
+ updates.push(update);
499
+ }
500
+ }
501
+ if (plan !== void 0) updates.push(plan);
502
+ return { updates, title, contextWindow: projection.contextWindow };
503
+ }
504
+
505
+ // src/bridge/prompt.ts
506
+ var UnsupportedPromptContentError = class extends Error {
507
+ constructor(contentType) {
508
+ super(`unsupported prompt content type: ${contentType}`);
509
+ this.contentType = contentType;
510
+ }
511
+ contentType;
512
+ };
513
+ function contextBlock(uri, text) {
514
+ return `<context ref=${JSON.stringify(uri)}>
515
+ ${text}
516
+ </context>`;
517
+ }
518
+ function convertPrompt(prompt) {
519
+ const parts = [];
520
+ const display = [];
521
+ for (const block of prompt) {
522
+ switch (block.type) {
523
+ case "text":
524
+ parts.push(block.text);
525
+ display.push(block.text);
526
+ break;
527
+ case "resource_link":
528
+ parts.push(`
529
+ [resource_link name=${JSON.stringify(block.name)} uri=${JSON.stringify(block.uri)}]
530
+ `);
531
+ display.push(`@${block.name}`);
532
+ break;
533
+ case "resource": {
534
+ const resource = block.resource;
535
+ if ("text" in resource && typeof resource.text === "string") {
536
+ parts.push(`
537
+ ${contextBlock(resource.uri, resource.text)}
538
+ `);
539
+ display.push(`@${resource.uri}`);
540
+ break;
541
+ }
542
+ throw new UnsupportedPromptContentError("resource (binary)");
543
+ }
544
+ default:
545
+ throw new UnsupportedPromptContentError(block.type);
546
+ }
547
+ }
548
+ const text = parts.join("");
549
+ return {
550
+ blocks: text.length > 0 ? [{ type: "text", text }] : [],
551
+ displayText: display.join(" ").trim()
552
+ };
553
+ }
554
+
555
+ // src/bridge/index.ts
556
+ var name = "acp-bridge";
557
+ var inject = ["agents"];
558
+ function invalidParams(detail) {
559
+ return RequestError.invalidParams(void 0, detail);
560
+ }
561
+ function internalError(detail) {
562
+ return RequestError.internalError(void 0, detail);
563
+ }
564
+ function authRequired(detail) {
565
+ return RequestError.authRequired(void 0, detail);
566
+ }
567
+ function envCredentialPresent() {
568
+ return Boolean(process.env["DEEPSEEK_API_KEY"] || process.env["DEEPSEEK_BASE_URL"]);
569
+ }
570
+ var MODE_LABELS = {
571
+ "read-only": { name: "Read-only", description: "Bash and file mutations are denied by the sandbox" },
572
+ "workspace-write": {
573
+ name: "Workspace write",
574
+ description: "Writes are confined to the session workspace; wider access asks for permission"
575
+ },
576
+ "danger-full-access": {
577
+ name: "Full access",
578
+ description: "No sandbox confinement and no permission prompts"
579
+ }
580
+ };
581
+ async function apply(ctx, config = {}) {
582
+ const agents = ctx.agents;
583
+ const harness = config.harness ?? (await Promise.resolve().then(() => (init_self_harness(), self_harness_exports))).selfHarness();
584
+ const { createUserMessage: createUserMessage2, errorChain: errorChain2, sessionId: SessionId2, foldSessionTitle: foldSessionTitle2, setSandboxMode: setSandboxMode2 } = harness;
585
+ const SANDBOX_MODES2 = harness.sandboxModes;
586
+ const sessions = /* @__PURE__ */ new Map();
587
+ let closed = false;
588
+ let conn;
589
+ const modelCandidates = () => {
590
+ const seen = /* @__PURE__ */ new Set();
591
+ if (config.model !== void 0) seen.add(config.model);
592
+ for (const model of config.models ?? []) if (model.trim().length > 0) seen.add(model.trim());
593
+ return [...seen];
594
+ };
595
+ const defaultSelection = () => {
596
+ const service = ctx.get("agentDefaultModel");
597
+ if (service === void 0) return {};
598
+ try {
599
+ return service.currentSelection();
600
+ } catch (error) {
601
+ logDebug(`agentDefaultModel.currentSelection failed: ${String(error)}`);
602
+ return {};
603
+ }
604
+ };
605
+ const defaultProvider = () => config.provider ?? defaultSelection().provider;
606
+ const encodeChoice = (provider, model) => provider === void 0 || provider === defaultProvider() ? model : `${provider}::${model}`;
607
+ const decodeChoice = (value) => {
608
+ const i = value.indexOf("::");
609
+ if (i <= 0) return { provider: defaultProvider(), model: value };
610
+ return { provider: value.slice(0, i), model: value.slice(i + 2) };
611
+ };
612
+ let liveCatalog;
613
+ const discoverModels = async () => {
614
+ if (liveCatalog !== void 0) return liveCatalog;
615
+ const llm = ctx.get("llm");
616
+ const found = [];
617
+ if (llm !== void 0) {
618
+ try {
619
+ const providers = llm.listProviders();
620
+ const multi = providers.length > 1;
621
+ for (const provider of providers) {
622
+ const models = await llm.listModels(provider.id).catch((error) => {
623
+ logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
624
+ return [];
625
+ });
626
+ for (const model of models) {
627
+ found.push({
628
+ provider: provider.id,
629
+ model: model.id,
630
+ label: multi ? `${model.name} (${provider.name})` : model.name
631
+ });
632
+ }
633
+ }
634
+ } catch (error) {
635
+ logDebug(`model discovery failed: ${String(error)}`);
636
+ }
637
+ }
638
+ liveCatalog = found;
639
+ return found;
640
+ };
641
+ ctx.on("llm/adapters-updated", () => {
642
+ liveCatalog = void 0;
643
+ });
644
+ const credentialPresent = async () => {
645
+ if (envCredentialPresent()) return true;
646
+ const brand = harness.credentialRef;
647
+ if (brand === void 0) return false;
648
+ const deadline = Date.now() + 5e3;
649
+ for (; ; ) {
650
+ const seam = ctx.get("credentials");
651
+ if (seam !== void 0) {
652
+ try {
653
+ return await seam.resolve(brand("DEEPSEEK_API_KEY")) !== void 0;
654
+ } catch (error) {
655
+ logWarn(`credential lookup failed: ${String(error)}`);
656
+ return false;
657
+ }
658
+ }
659
+ if (Date.now() >= deadline) {
660
+ logDebug("credential gate: no credentials service registered within 5s");
661
+ return false;
662
+ }
663
+ await new Promise((resolve) => setTimeout(resolve, 100));
664
+ }
665
+ };
666
+ const agentOptionsFor = (model, provider) => ({
667
+ ...provider !== void 0 ? { provider } : config.provider !== void 0 ? { provider: config.provider } : {},
668
+ ...model !== void 0 ? { model } : {},
669
+ ...config.maxTokens !== void 0 ? { maxTokens: config.maxTokens } : {}
670
+ });
671
+ const ownedRecord = (agent) => {
672
+ const record = sessions.get(String(agent.session.id));
673
+ return record?.agent === agent ? record : void 0;
674
+ };
675
+ const requireSession = (sessionId) => {
676
+ const record = sessions.get(sessionId);
677
+ if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
678
+ return record;
679
+ };
680
+ const assertOpen = () => {
681
+ if (closed) throw internalError("the ACP bridge has been disposed");
682
+ };
683
+ const notify = (sessionId, update) => {
684
+ void conn.sessionUpdate({ sessionId, update }).catch((error) => {
685
+ logWarn(`session/update failed: ${String(error)}`);
686
+ });
687
+ };
688
+ const settlePrompt = (record, reason) => {
689
+ const inflight = record.inflight;
690
+ if (inflight === void 0) return;
691
+ record.inflight = void 0;
692
+ inflight.resolve(reason);
693
+ };
694
+ const modeState = (record) => ({
695
+ currentModeId: record.modeId,
696
+ availableModes: SANDBOX_MODES2.map((mode) => {
697
+ const label = MODE_LABELS[mode] ?? { name: mode, description: "" };
698
+ return { id: mode, name: label.name, description: label.description };
699
+ })
700
+ });
701
+ const configOptions = async (record) => {
702
+ const seen = /* @__PURE__ */ new Set();
703
+ const options = [];
704
+ const push = (value, name3) => {
705
+ if (seen.has(value)) return;
706
+ seen.add(value);
707
+ options.push({ value, name: name3 });
708
+ };
709
+ const current = encodeChoice(
710
+ record.provider,
711
+ record.model ?? config.model ?? defaultSelection().model ?? record.agent.options.model ?? ""
712
+ );
713
+ if (current.length === 0) return [];
714
+ const discovered = await discoverModels();
715
+ push(current, discovered.find((c) => encodeChoice(c.provider, c.model) === current)?.label ?? current);
716
+ for (const choice of discovered) {
717
+ push(encodeChoice(choice.provider, choice.model), choice.label);
718
+ }
719
+ for (const model of modelCandidates()) push(encodeChoice(void 0, model), model);
720
+ if (options.length < 2) return [];
721
+ return [
722
+ {
723
+ type: "select",
724
+ id: "model",
725
+ name: "Model",
726
+ category: "model",
727
+ currentValue: current,
728
+ options
729
+ }
730
+ ];
731
+ };
732
+ const publishCommands = (sessionId) => {
733
+ notify(sessionId, {
734
+ sessionUpdate: "available_commands_update",
735
+ availableCommands: [
736
+ { name: "status", description: "Show adapter, model, mode, and token status" },
737
+ {
738
+ name: "login",
739
+ description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
740
+ input: { hint: "<api-key>" }
741
+ },
742
+ { name: "logout", description: "Remove the API key stored in the harness credential store" }
743
+ ]
744
+ });
745
+ };
746
+ const credentialStore = () => {
747
+ const brand = harness.credentialRef;
748
+ const store = ctx.get("credentials");
749
+ if (brand === void 0 || store === void 0) return void 0;
750
+ return { store, ref: brand("DEEPSEEK_API_KEY") };
751
+ };
752
+ const maskKey = (value) => value.length <= 8 ? "\u2026" : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
753
+ const describeCredential = async () => {
754
+ if (envCredentialPresent()) {
755
+ const viaEnv = process.env["DEEPSEEK_API_KEY"] !== void 0 ? "DEEPSEEK_API_KEY" : "DEEPSEEK_BASE_URL";
756
+ return `process environment (${viaEnv})`;
757
+ }
758
+ const cs = credentialStore();
759
+ if (cs === void 0) return "not configured";
760
+ try {
761
+ const info = await cs.store.describe(cs.ref);
762
+ if (!info.configured) return "not configured";
763
+ return info.source ?? "credential store";
764
+ } catch (error) {
765
+ logWarn(`credential describe failed: ${String(error)}`);
766
+ return "unknown";
767
+ }
768
+ };
769
+ const loginText = async (rawArgument) => {
770
+ const key = rawArgument.trim();
771
+ const cs = credentialStore();
772
+ if (cs === void 0) {
773
+ return "This composition has no writable credential store; export DEEPSEEK_API_KEY in the environment that launches the agent instead.";
774
+ }
775
+ if (key.length === 0) {
776
+ return [
777
+ "Usage: `/login <api-key>` \u2014 stores the key in the harness credential store",
778
+ "(`~/.dsh/.credentials.yaml`, the same file the dsh Web UI writes; mode 600).",
779
+ "",
780
+ `Current credential source: ${await describeCredential()}.`,
781
+ "",
782
+ "Note: text entered here also lands in your client's chat history. If you",
783
+ "prefer to keep the key out of it, save the key in the dsh Web UI instead."
784
+ ].join("\n");
785
+ }
786
+ try {
787
+ await cs.store.set(cs.ref, key);
788
+ } catch (error) {
789
+ return [
790
+ `Could not store the key: ${error instanceof Error ? error.message : String(error)}`,
791
+ "",
792
+ "A read-only source (usually the process environment) is currently supplying",
793
+ "this credential, so a stored value would be shadowed. Unset DEEPSEEK_API_KEY",
794
+ "in the launching environment, or keep using it as the credential."
795
+ ].join("\n");
796
+ }
797
+ 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.`;
798
+ };
799
+ const logoutText = async () => {
800
+ const cs = credentialStore();
801
+ if (cs === void 0) return "This composition has no writable credential store; nothing to remove.";
802
+ try {
803
+ await cs.store.unset(cs.ref);
804
+ } catch (error) {
805
+ return `Could not remove the stored key: ${error instanceof Error ? error.message : String(error)}`;
806
+ }
807
+ const remaining = await describeCredential();
808
+ 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}.`;
809
+ };
810
+ const statusText = async (record) => {
811
+ const used = record.projection.contextWindow;
812
+ const lines = [
813
+ `**dsh-acp** ${VERSION} \u2014 DeepSeek Harness ACP bridge`,
814
+ "",
815
+ `| | |`,
816
+ `|---|---|`,
817
+ `| Provider | ${record.provider ?? defaultProvider() ?? "(composition default)"} |`,
818
+ `| Model | ${record.model ?? config.model ?? defaultSelection().model ?? record.agent.options.model ?? "(composition default)"} |`,
819
+ `| Credential | ${await describeCredential()} |`,
820
+ `| Permission mode | ${record.modeId} |`,
821
+ `| Workspace | ${record.agent.session.header.cwd ?? process.cwd()} |`,
822
+ `| Session | ${String(record.agent.session.id)} |`,
823
+ ...used !== void 0 ? [`| Context window | ${used.toLocaleString()} tokens |`] : []
824
+ ];
825
+ return lines.join("\n");
826
+ };
827
+ const registerRecord = (sessionId, agent, dispose, model, modeId, projection) => {
828
+ const record = {
829
+ agent,
830
+ dispose,
831
+ projection: projection ?? new SessionProjection(),
832
+ modeId,
833
+ model,
834
+ cancelled: false,
835
+ inflight: void 0
836
+ };
837
+ sessions.set(sessionId, record);
838
+ return record;
839
+ };
840
+ ctx.on("session/event", (session, event) => {
841
+ const sessionId = String(session.header.id);
842
+ const record = sessions.get(sessionId);
843
+ if (record === void 0 || record.agent.session !== session) return;
844
+ try {
845
+ for (const update of record.projection.onEvent(event)) {
846
+ notify(sessionId, update);
847
+ }
848
+ } finally {
849
+ const inflight = record.inflight;
850
+ if (inflight !== void 0 && event.type === "turn/end" && inflight.turn === event.data.turn) {
851
+ if (event.data.reason.kind === "error") {
852
+ record.inflight = void 0;
853
+ inflight.reject(internalError(`turn failed: ${event.data.reason.error.message}`));
854
+ }
855
+ }
856
+ }
857
+ });
858
+ ctx.on("agent/inbox/claimed", ({ agent, message, turn }) => {
859
+ const record = ownedRecord(agent);
860
+ const inflight = record?.inflight;
861
+ if (inflight !== void 0 && inflight.messageId === message.id) inflight.turn = turn;
862
+ });
863
+ ctx.on("agent/error", ({ agent, turn, error }) => {
864
+ const record = ownedRecord(agent);
865
+ const inflight = record?.inflight;
866
+ if (record === void 0 || inflight === void 0 || inflight.turn === turn) return;
867
+ record.inflight = void 0;
868
+ inflight.reject(internalError(`turn failed: ${errorChain2(error)}`));
869
+ });
870
+ ctx.on("approval/request", (request, next) => {
871
+ const record = ownedRecord(request.agent);
872
+ if (record === void 0 || request.callId === void 0) return next();
873
+ return conn.requestPermission({
874
+ sessionId: String(record.agent.session.id),
875
+ toolCall: { toolCallId: request.callId },
876
+ options: [
877
+ { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
878
+ { optionId: "allow-always", name: "Always allow (this session)", kind: "allow_always" },
879
+ { optionId: "reject-once", name: "Reject", kind: "reject_once" }
880
+ ]
881
+ }).then(({ outcome }) => {
882
+ if (outcome.outcome === "cancelled") return "cancelled";
883
+ if (outcome.optionId === "allow-always") {
884
+ try {
885
+ ctx.get("approval")?.setPolicy?.(record.agent, "never");
886
+ } catch (error) {
887
+ logWarn(`approval policy switch failed: ${String(error)}`);
888
+ }
889
+ return "allowed-once";
890
+ }
891
+ return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
892
+ });
893
+ });
894
+ const makeAgent = (connection) => {
895
+ conn = connection;
896
+ return {
897
+ initialize(params) {
898
+ const hasPersistence = ctx.get("sessionPersistence") !== void 0;
899
+ const requested = params.protocolVersion;
900
+ return Promise.resolve({
901
+ protocolVersion: typeof requested === "number" && requested >= 1 && requested < PROTOCOL_VERSION ? requested : PROTOCOL_VERSION,
902
+ agentInfo: { name: "dsh-acp", title: "DeepSeek Harness", version: VERSION },
903
+ agentCapabilities: {
904
+ loadSession: hasPersistence,
905
+ promptCapabilities: { image: false, audio: false, embeddedContext: true },
906
+ // Stdio servers always work; streamable HTTP maps onto
907
+ // mcp-client's second transport. Legacy SSE does not.
908
+ mcpCapabilities: { http: true, sse: false },
909
+ ...hasPersistence ? { sessionCapabilities: { list: {} } } : {}
910
+ },
911
+ authMethods: []
912
+ });
913
+ },
914
+ async authenticate(_params) {
915
+ if (!await credentialPresent()) {
916
+ throw authRequired(
917
+ "no DeepSeek credential found: save one in the dsh Web UI (Settings \u2192 Models) or set DEEPSEEK_API_KEY"
918
+ );
919
+ }
920
+ },
921
+ async newSession(params) {
922
+ assertOpen();
923
+ validateCwd(params.cwd);
924
+ syncMcpServers(params.mcpServers, params.cwd);
925
+ const sessionId = SessionId2(randomUUID());
926
+ const handle = await agents.create({
927
+ sessionId,
928
+ meta: { cwd: params.cwd },
929
+ agentOptions: agentOptionsFor(config.model)
930
+ });
931
+ if (closed) {
932
+ await handle.dispose();
933
+ throw internalError("connection closed during session/new");
934
+ }
935
+ const record = registerRecord(
936
+ String(sessionId),
937
+ handle.agent,
938
+ () => handle.dispose(),
939
+ config.model,
940
+ config.permissionMode ?? "workspace-write"
941
+ );
942
+ queueMicrotask(() => publishCommands(String(sessionId)));
943
+ const options = await configOptions(record);
944
+ return {
945
+ sessionId: String(sessionId),
946
+ modes: modeState(record),
947
+ ...options.length > 0 ? { configOptions: options } : {}
948
+ };
949
+ },
950
+ async loadSession(params) {
951
+ assertOpen();
952
+ validateCwd(params.cwd);
953
+ syncMcpServers(params.mcpServers, params.cwd);
954
+ const persistence = requirePersistence();
955
+ const sessionId = params.sessionId;
956
+ const existing = sessions.get(sessionId);
957
+ if (existing !== void 0) {
958
+ sessions.delete(sessionId);
959
+ existing.agent.cancel({ kind: "user" });
960
+ settlePrompt(existing, "cancelled");
961
+ await existing.dispose().catch((error) => {
962
+ logWarn(`dispose before reload failed: ${String(error)}`);
963
+ });
964
+ }
965
+ let events;
966
+ try {
967
+ ({ events } = await persistence.inspect(SessionId2(sessionId)));
968
+ } catch (error) {
969
+ throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
970
+ }
971
+ const replay = buildReplay(events);
972
+ for (const update of replay.updates) notify(sessionId, update);
973
+ if (replay.title !== void 0) {
974
+ notify(sessionId, {
975
+ sessionUpdate: "session_info_update",
976
+ title: replay.title
977
+ });
978
+ }
979
+ const handle = await agents.resume({
980
+ resumeSessionId: SessionId2(sessionId),
981
+ agentOptions: agentOptionsFor(config.model)
982
+ });
983
+ const projection = new SessionProjection(replay.contextWindow);
984
+ projection.title = replay.title;
985
+ const record = registerRecord(
986
+ sessionId,
987
+ handle.agent,
988
+ () => handle.dispose(),
989
+ config.model,
990
+ config.permissionMode ?? "workspace-write",
991
+ projection
992
+ );
993
+ queueMicrotask(() => publishCommands(sessionId));
994
+ const options = await configOptions(record);
995
+ return {
996
+ modes: modeState(record),
997
+ ...options.length > 0 ? { configOptions: options } : {}
998
+ };
999
+ },
1000
+ async listSessions(params) {
1001
+ assertOpen();
1002
+ const persistence = requirePersistence();
1003
+ const headers = await persistence.list();
1004
+ headers.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
1005
+ const filtered = params.cwd !== void 0 && params.cwd !== null ? headers.filter((header) => header.cwd === params.cwd) : headers;
1006
+ const page = filtered.slice(0, 100);
1007
+ const withTitles = await Promise.allSettled(
1008
+ page.slice(0, 20).map(async (header) => {
1009
+ const { events } = await persistence.inspect(header.id);
1010
+ return foldSessionTitle2(events)?.title;
1011
+ })
1012
+ );
1013
+ const sessionsInfo = page.map((header, index) => {
1014
+ const settled = withTitles[index];
1015
+ const title = settled !== void 0 && settled.status === "fulfilled" ? settled.value : void 0;
1016
+ return {
1017
+ sessionId: String(header.id),
1018
+ cwd: header.cwd ?? "",
1019
+ ...title !== void 0 ? { title } : {},
1020
+ ...header.createdAt !== void 0 ? { updatedAt: new Date(header.createdAt).toISOString() } : {}
1021
+ };
1022
+ });
1023
+ return { sessions: sessionsInfo };
1024
+ },
1025
+ async prompt(params) {
1026
+ assertOpen();
1027
+ const record = requireSession(params.sessionId);
1028
+ if (record.inflight !== void 0) {
1029
+ throw invalidParams("a prompt is already in flight for this session");
1030
+ }
1031
+ let converted;
1032
+ try {
1033
+ converted = convertPrompt(params.prompt);
1034
+ } catch (error) {
1035
+ if (error instanceof UnsupportedPromptContentError) throw invalidParams(error.message);
1036
+ throw error;
1037
+ }
1038
+ if (converted.blocks.length === 0) throw invalidParams("empty prompt");
1039
+ const trimmed = converted.displayText.trim();
1040
+ const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/);
1041
+ const respond = (text) => {
1042
+ notify(params.sessionId, {
1043
+ sessionUpdate: "agent_message_chunk",
1044
+ content: { type: "text", text }
1045
+ });
1046
+ return { stopReason: "end_turn" };
1047
+ };
1048
+ if (commandMatch?.[1] === "status") return respond(await statusText(record));
1049
+ if (commandMatch?.[1] === "login") {
1050
+ return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1051
+ }
1052
+ if (commandMatch?.[1] === "logout") return respond(await logoutText());
1053
+ if (!await credentialPresent()) {
1054
+ return respond(
1055
+ [
1056
+ "No DeepSeek credential is configured, so this prompt was not sent to the model.",
1057
+ "",
1058
+ "Configure one of:",
1059
+ "- `/login <api-key>` \u2014 store a key in the harness credential store right here,",
1060
+ "- the dsh Web UI (`dsh web`, Settings \u2192 Models),",
1061
+ "- `DEEPSEEK_API_KEY` in the environment that launches this agent."
1062
+ ].join("\n")
1063
+ );
1064
+ }
1065
+ if (agents.get(record.agent.id) !== record.agent) {
1066
+ throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1067
+ }
1068
+ record.cancelled = false;
1069
+ record.projection.beginPrompt();
1070
+ const message = createUserMessage2({
1071
+ content: converted.blocks,
1072
+ source: { kind: "user" }
1073
+ });
1074
+ const stopReason = await new Promise((resolve, reject) => {
1075
+ const inflight = { resolve, reject, messageId: message.id, turn: void 0 };
1076
+ record.inflight = inflight;
1077
+ try {
1078
+ record.agent.followup(message);
1079
+ } catch (error) {
1080
+ record.inflight = void 0;
1081
+ const detail = error instanceof Error ? error.message : String(error);
1082
+ throw internalError(`prompt was not queued: ${detail}`);
1083
+ }
1084
+ void record.agent.whenIdle().then(() => {
1085
+ if (record.inflight !== inflight) return;
1086
+ record.inflight = void 0;
1087
+ if (record.cancelled) {
1088
+ resolve("cancelled");
1089
+ return;
1090
+ }
1091
+ const end = inflight.turn !== void 0 ? record.projection.turnEndFor(inflight.turn) ?? record.projection.lastTurnEnd : record.projection.lastTurnEnd;
1092
+ resolve(end === void 0 ? "cancelled" : turnEndToStopReason(end));
1093
+ });
1094
+ });
1095
+ const usage = record.projection.promptUsage();
1096
+ return { stopReason, ...usage !== void 0 ? { usage } : {} };
1097
+ },
1098
+ setSessionMode(params) {
1099
+ const record = requireSession(params.sessionId);
1100
+ const mode = SANDBOX_MODES2.find((candidate) => candidate === params.modeId);
1101
+ if (mode === void 0) throw invalidParams(`unknown mode: ${params.modeId}`);
1102
+ setSandboxMode2(record.agent.session, mode);
1103
+ try {
1104
+ ctx.get("approval")?.setPolicy?.(record.agent, mode === "danger-full-access" ? "never" : "ask");
1105
+ } catch (error) {
1106
+ logWarn(`approval policy switch failed: ${String(error)}`);
1107
+ }
1108
+ record.modeId = mode;
1109
+ notify(params.sessionId, { sessionUpdate: "current_mode_update", currentModeId: mode });
1110
+ return Promise.resolve({});
1111
+ },
1112
+ async setSessionConfigOption(params) {
1113
+ const record = requireSession(params.sessionId);
1114
+ if (params.configId !== "model") throw invalidParams(`unknown config option: ${params.configId}`);
1115
+ if (record.inflight !== void 0) {
1116
+ throw invalidParams("cannot switch models while a prompt is running");
1117
+ }
1118
+ const value = typeof params.value === "string" ? params.value : void 0;
1119
+ if (value === void 0) throw invalidParams(`unknown model: ${String(params.value)}`);
1120
+ const choice = decodeChoice(value);
1121
+ const known = /* @__PURE__ */ new Set([
1122
+ ...modelCandidates().map((model) => encodeChoice(void 0, model)),
1123
+ ...(await discoverModels()).map((entry) => encodeChoice(entry.provider, entry.model)),
1124
+ encodeChoice(record.provider, record.model ?? "")
1125
+ ]);
1126
+ if (!known.has(value)) throw invalidParams(`unknown model: ${value}`);
1127
+ if (choice.model !== record.model || choice.provider !== (record.provider ?? defaultProvider())) {
1128
+ const sessionId = record.agent.session.id;
1129
+ await record.dispose().catch((error) => {
1130
+ logWarn(`dispose during model switch failed: ${String(error)}`);
1131
+ });
1132
+ let handle;
1133
+ try {
1134
+ handle = await agents.resume({
1135
+ resumeSessionId: sessionId,
1136
+ agentOptions: agentOptionsFor(choice.model, choice.provider)
1137
+ });
1138
+ } catch (error) {
1139
+ sessions.delete(params.sessionId);
1140
+ throw internalError(`model switch failed: ${errorChain2(error)}`);
1141
+ }
1142
+ record.agent = handle.agent;
1143
+ record.dispose = () => handle.dispose();
1144
+ record.model = choice.model;
1145
+ if (choice.provider !== void 0 && choice.provider !== defaultProvider()) {
1146
+ record.provider = choice.provider;
1147
+ } else {
1148
+ delete record.provider;
1149
+ }
1150
+ }
1151
+ return { configOptions: await configOptions(record) };
1152
+ },
1153
+ async closeSession(params) {
1154
+ const record = sessions.get(params.sessionId);
1155
+ if (record === void 0) return;
1156
+ sessions.delete(params.sessionId);
1157
+ record.agent.cancel({ kind: "user" });
1158
+ settlePrompt(record, "cancelled");
1159
+ await record.dispose().catch((error) => {
1160
+ logWarn(`session close failed: ${String(error)}`);
1161
+ });
1162
+ },
1163
+ cancel(params) {
1164
+ const record = sessions.get(params.sessionId);
1165
+ if (record === void 0) return Promise.resolve();
1166
+ record.cancelled = true;
1167
+ record.agent.cancel({ kind: "user" });
1168
+ settlePrompt(record, "cancelled");
1169
+ return Promise.resolve();
1170
+ }
1171
+ };
1172
+ };
1173
+ function requirePersistence() {
1174
+ const persistence = getPersistence();
1175
+ if (persistence === void 0) {
1176
+ throw internalError("session persistence is not composed; session history is unavailable");
1177
+ }
1178
+ return persistence;
1179
+ }
1180
+ function getPersistence() {
1181
+ return ctx.get("sessionPersistence");
1182
+ }
1183
+ function validateCwd(cwd) {
1184
+ if (!isAbsolute(cwd)) throw invalidParams(`cwd must be an absolute path: ${cwd}`);
1185
+ }
1186
+ const mcpMounts = /* @__PURE__ */ new Map();
1187
+ let warnedNoMcpClient = false;
1188
+ const sanitizeServerName = (raw) => {
1189
+ const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 32);
1190
+ return cleaned.length > 0 ? cleaned : "server";
1191
+ };
1192
+ const mcpConfigFor = (server, cwd, serverName) => {
1193
+ const envList = (list) => Object.fromEntries(
1194
+ (Array.isArray(list) ? list : []).map((e) => [
1195
+ e.name,
1196
+ e.value
1197
+ ])
1198
+ );
1199
+ if (typeof server["command"] === "string") {
1200
+ return {
1201
+ transport: "stdio",
1202
+ serverName,
1203
+ command: server["command"],
1204
+ args: Array.isArray(server["args"]) ? server["args"] : [],
1205
+ env: envList(server["env"]),
1206
+ cwd: typeof server["cwd"] === "string" ? server["cwd"] : cwd,
1207
+ // A dead or misconfigured server must not take the session
1208
+ // down; the client surfaces missing tools on its own.
1209
+ failOnStartupError: false
1210
+ };
1211
+ }
1212
+ if (server["type"] === "http" && typeof server["url"] === "string") {
1213
+ return {
1214
+ transport: "streamable-http",
1215
+ serverName,
1216
+ url: server["url"],
1217
+ headers: envList(server["headers"]),
1218
+ failOnStartupError: false
1219
+ };
1220
+ }
1221
+ return void 0;
1222
+ };
1223
+ const syncMcpServers = (servers, cwd) => {
1224
+ if (servers === void 0 || servers.length === 0) return;
1225
+ const mcpClient2 = harness.mcpClient;
1226
+ if (mcpClient2 === void 0) {
1227
+ if (!warnedNoMcpClient) {
1228
+ warnedNoMcpClient = true;
1229
+ logWarn(
1230
+ `ignoring ${servers.length} MCP server(s): this DeepSeek Harness installation lacks @deepseek-ai/dsh-mcp-client`
1231
+ );
1232
+ }
1233
+ return;
1234
+ }
1235
+ const taken = /* @__PURE__ */ new Set();
1236
+ for (const entry of servers) {
1237
+ const server = entry;
1238
+ const base = sanitizeServerName(typeof server["name"] === "string" ? server["name"] : "server");
1239
+ let serverName = base;
1240
+ for (let n = 2; taken.has(serverName); n += 1) serverName = `${base.slice(0, 28)}_${n}`;
1241
+ const cfg = mcpConfigFor(server, cwd, serverName);
1242
+ if (cfg === void 0) {
1243
+ logWarn(`skipping MCP server "${serverName}": unsupported transport`);
1244
+ continue;
1245
+ }
1246
+ const configJson = JSON.stringify(cfg);
1247
+ const existing = mcpMounts.get(serverName);
1248
+ if (existing !== void 0) {
1249
+ if (existing.configJson === configJson) {
1250
+ taken.add(serverName);
1251
+ continue;
1252
+ }
1253
+ try {
1254
+ void existing.fiber.dispose();
1255
+ } catch (error) {
1256
+ logWarn(`disposing MCP server "${serverName}": ${String(error)}`);
1257
+ }
1258
+ mcpMounts.delete(serverName);
1259
+ }
1260
+ try {
1261
+ const fiber = ctx.plugin(mcpClient2, cfg);
1262
+ mcpMounts.set(serverName, { configJson, fiber });
1263
+ taken.add(serverName);
1264
+ logDebug(`mounted MCP server "${serverName}" (${String(cfg["transport"])})`);
1265
+ } catch (error) {
1266
+ logWarn(`failed to mount MCP server "${serverName}": ${String(error)}`);
1267
+ }
1268
+ }
1269
+ };
1270
+ const stream = config.stream ?? ndJsonStream(
1271
+ Writable.toWeb(process.stdout),
1272
+ Readable.toWeb(process.stdin)
1273
+ );
1274
+ conn = new AgentSideConnection(makeAgent, stream);
1275
+ let quiescing;
1276
+ const quiesce = () => {
1277
+ if (quiescing !== void 0) return quiescing;
1278
+ closed = true;
1279
+ const records = [...sessions.values()];
1280
+ sessions.clear();
1281
+ for (const record of records) {
1282
+ record.agent.cancel({ kind: "user" });
1283
+ settlePrompt(record, "cancelled");
1284
+ }
1285
+ quiescing = (async () => {
1286
+ const subagents = ctx.get("subagents");
1287
+ if (subagents !== void 0) {
1288
+ try {
1289
+ await subagents.drainContinuableDescendants(records.map((record) => record.agent));
1290
+ } catch (error) {
1291
+ logWarn(`continuable subagent teardown failed: ${String(error)}`);
1292
+ }
1293
+ }
1294
+ const disposals = await Promise.allSettled(records.map((record) => record.dispose()));
1295
+ const failures = disposals.filter(
1296
+ (result) => result.status === "rejected"
1297
+ );
1298
+ if (failures.length > 0) {
1299
+ const detail = failures.map((failure) => errorChain2(failure.reason)).join("; ");
1300
+ throw new AggregateError(
1301
+ failures.map((failure) => failure.reason),
1302
+ `ACP agent teardown failed for ${failures.length} session(s): ${detail}`
1303
+ );
1304
+ }
1305
+ })();
1306
+ return quiescing;
1307
+ };
1308
+ void conn.closed.catch((error) => {
1309
+ logWarn(`connection closed with an error: ${String(error)}`);
1310
+ }).then(quiesce).catch((error) => {
1311
+ logWarn(`connection-close teardown failed: ${String(error)}`);
1312
+ });
1313
+ ctx.effect(() => quiesce, "acp-bridge.connection");
1314
+ }
1315
+
1316
+ // src/bundle.ts
1317
+ init_self_harness();
1318
+ var name2 = name;
1319
+ var inject2 = inject;
1320
+ function apply2(ctx, config) {
1321
+ apply(ctx, {
1322
+ ...config.provider !== void 0 ? { provider: config.provider } : {},
1323
+ ...config.model !== void 0 ? { model: config.model } : {},
1324
+ ...config.models !== void 0 ? { models: config.models } : {},
1325
+ ...config.maxTokens !== void 0 ? { maxTokens: config.maxTokens } : {},
1326
+ ...config.permissionMode !== void 0 ? { permissionMode: config.permissionMode } : {},
1327
+ harness: selfHarness()
1328
+ });
1329
+ }
1330
+ export {
1331
+ apply2 as apply,
1332
+ inject2 as inject,
1333
+ name2 as name
1334
+ };