lody 0.79.2 → 0.81.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.
@@ -0,0 +1,660 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { n as ndJsonStream, A as AgentSideConnection, R as RequestError, P as PROTOCOL_VERSION } from "./chunks/acp-CrvzLY5A.js";
5
+ import { b as DEEPSEEK_HARNESS_MODELS, D as DEEPSEEK_HARNESS_PERMISSION_MODES, c as DEEPSEEK_HARNESS_REASONING_OPTIONS, e as ACP_EXTENSION_DSH_VERSION, a as DEEPSEEK_HARNESS_AGENT_PRESETS } from "./chunks/profile-DW2t9oI8.js";
6
+ import "./chunks/schemas-Du3qLZPS.js";
7
+ const name = "acp-extension-dsh";
8
+ const inject = [
9
+ "agents",
10
+ "agentPresets",
11
+ "loader",
12
+ "permissionPresets",
13
+ "sessionPersistence",
14
+ "sessionQuery"
15
+ ];
16
+ const MODEL_CONFIG_ID = "model";
17
+ const MODE_CONFIG_ID = "mode";
18
+ const REASONING_EFFORT_CONFIG_ID = "reasoning_effort";
19
+ const AGENT_PRESET_CONFIG_ID = "agent_preset";
20
+ const MCP_CLIENT_PACKAGE = "@deepseek-ai/dsh-mcp-client";
21
+ const MCP_TOOL_CALL_TIMEOUT_MS = 6e4;
22
+ const MCP_SERVER_NAME_MAX_LENGTH = 32;
23
+ const MCP_SERVER_NAME_HASH_LENGTH = 8;
24
+ const INVALID_MCP_SERVER_NAME_CHARS = /[^A-Za-z0-9_-]/gu;
25
+ const MODEL_IDS = new Set(DEEPSEEK_HARNESS_MODELS.map((model) => model.modelId));
26
+ const PERMISSION_MODE_IDS = new Set(DEEPSEEK_HARNESS_PERMISSION_MODES.map((mode) => mode.id));
27
+ const REASONING_EFFORT_IDS = new Set(DEEPSEEK_HARNESS_REASONING_OPTIONS.map((effort) => effort.value));
28
+ function invalidParams(detail) {
29
+ return RequestError.invalidParams(void 0, detail);
30
+ }
31
+ function internalError(detail) {
32
+ return RequestError.internalError(void 0, detail);
33
+ }
34
+ function nonEmptyString(value, fallback) {
35
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
36
+ }
37
+ function resolveAdapterConfig(config) {
38
+ const provider = nonEmptyString(config?.provider, "deepseek-official");
39
+ const model = nonEmptyString(config?.model, "deepseek-v4-pro");
40
+ const reasoningEffort = config?.reasoningEffort ?? "max";
41
+ if (!MODEL_IDS.has(model)) {
42
+ throw new Error(`acp-extension-dsh: unsupported model ${JSON.stringify(model)}`);
43
+ }
44
+ if (!REASONING_EFFORT_IDS.has(reasoningEffort)) {
45
+ throw new Error(`acp-extension-dsh: unsupported reasoning effort ${JSON.stringify(reasoningEffort)}`);
46
+ }
47
+ return {
48
+ provider,
49
+ model,
50
+ reasoningEffort,
51
+ ...config?.stream ? { stream: config.stream } : {}
52
+ };
53
+ }
54
+ async function loadMcpClientPlugin(agentContext) {
55
+ const module = agentContext.loader.unwrapExports(await agentContext.loader.import(MCP_CLIENT_PACKAGE));
56
+ if (typeof module !== "object" || module === null || !("apply" in module) || typeof module.apply !== "function") {
57
+ throw new Error(`${MCP_CLIENT_PACKAGE} does not export a Cordis plugin`);
58
+ }
59
+ return module;
60
+ }
61
+ function shortHash(value) {
62
+ return createHash("sha256").update(value).digest("hex").slice(0, MCP_SERVER_NAME_HASH_LENGTH);
63
+ }
64
+ function normalizedMcpServerName(serverName, fallbackIndex) {
65
+ const normalized = serverName.replace(INVALID_MCP_SERVER_NAME_CHARS, "_");
66
+ const base = normalized || `server_${fallbackIndex + 1}`;
67
+ if (base === serverName && base.length <= MCP_SERVER_NAME_MAX_LENGTH)
68
+ return base;
69
+ const hash = shortHash(serverName);
70
+ return `${base.slice(0, MCP_SERVER_NAME_MAX_LENGTH - hash.length - 1)}_${hash}`;
71
+ }
72
+ function reserveMcpServerNames(servers, sessionId, activeNames) {
73
+ const names = [];
74
+ for (const [index, server] of servers.entries()) {
75
+ const base = normalizedMcpServerName(server.name, index);
76
+ let reservedName = base;
77
+ let attempt = 0;
78
+ while (activeNames.has(reservedName)) {
79
+ const suffix = shortHash(`${sessionId}\0${index}\0${attempt}`);
80
+ reservedName = `${base.slice(0, MCP_SERVER_NAME_MAX_LENGTH - suffix.length - 1)}_${suffix}`;
81
+ attempt += 1;
82
+ }
83
+ activeNames.add(reservedName);
84
+ names.push(reservedName);
85
+ }
86
+ let released = false;
87
+ return {
88
+ names,
89
+ release() {
90
+ if (released)
91
+ return;
92
+ released = true;
93
+ for (const reservedName of names)
94
+ activeNames.delete(reservedName);
95
+ }
96
+ };
97
+ }
98
+ function entriesToRecord(entries) {
99
+ return Object.fromEntries(entries.map(({ name: entryName, value }) => [entryName, value]));
100
+ }
101
+ function mcpClientConfig(server, serverName, cwd) {
102
+ if (!("type" in server)) {
103
+ return {
104
+ transport: "stdio",
105
+ serverName,
106
+ command: server.command,
107
+ args: [...server.args],
108
+ env: entriesToRecord(server.env),
109
+ cwd,
110
+ toolCallTimeoutMs: MCP_TOOL_CALL_TIMEOUT_MS,
111
+ failOnStartupError: true
112
+ };
113
+ }
114
+ if (server.type === "http") {
115
+ return {
116
+ transport: "streamable-http",
117
+ serverName,
118
+ url: server.url,
119
+ headers: entriesToRecord(server.headers),
120
+ toolCallTimeoutMs: MCP_TOOL_CALL_TIMEOUT_MS,
121
+ failOnStartupError: true
122
+ };
123
+ }
124
+ throw invalidParams(`MCP transport ${server.type} is not supported`);
125
+ }
126
+ async function mountMcpServers(agentContext, servers, serverNames, cwd) {
127
+ if (servers.length === 0)
128
+ return;
129
+ const plugin = await loadMcpClientPlugin(agentContext);
130
+ const handles = servers.map((server, index) => {
131
+ const serverName = serverNames[index];
132
+ if (!serverName)
133
+ throw new Error(`missing MCP namespace for server ${index}`);
134
+ return agentContext.plugin(plugin, mcpClientConfig(server, serverName, cwd));
135
+ });
136
+ await Promise.all(handles.map((handle) => handle.await()));
137
+ }
138
+ function cloneSelection(selection) {
139
+ return { ...selection };
140
+ }
141
+ function installModelSelection(agentContext, selection) {
142
+ agentContext.on("system-prompt/assemble", async (_assembly, _context, next) => {
143
+ const selected = cloneSelection(selection.current);
144
+ const assembled = await next();
145
+ selection.assembled = selected;
146
+ return {
147
+ ...assembled,
148
+ variables: {
149
+ ...assembled.variables,
150
+ provider: selected.provider,
151
+ model: selected.model
152
+ }
153
+ };
154
+ });
155
+ agentContext.on("agent/request", async (_payload, next) => {
156
+ const resolved = await next();
157
+ const selected = selection.assembled ?? selection.current;
158
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
159
+ return {
160
+ ...withoutInheritedEffort,
161
+ provider: selected.provider,
162
+ model: selected.model,
163
+ reasoningEffort: selected.reasoningEffort
164
+ };
165
+ });
166
+ }
167
+ function configOptions(record) {
168
+ return [
169
+ {
170
+ id: MODE_CONFIG_ID,
171
+ name: "Permission",
172
+ description: "Sandbox and approval policy for the session",
173
+ category: "mode",
174
+ type: "select",
175
+ currentValue: record.permissionMode,
176
+ options: DEEPSEEK_HARNESS_PERMISSION_MODES.map((mode) => ({
177
+ value: mode.id,
178
+ name: mode.name,
179
+ description: mode.description ?? null
180
+ }))
181
+ },
182
+ {
183
+ id: AGENT_PRESET_CONFIG_ID,
184
+ name: "Agent preset",
185
+ description: "Tools, prompt, and capabilities composed for the session",
186
+ category: "agent_preset",
187
+ type: "select",
188
+ currentValue: record.agentPreset,
189
+ options: record.agentPresetOptions.map((preset) => {
190
+ const builtIn = DEEPSEEK_HARNESS_AGENT_PRESETS.find((candidate) => candidate.value === preset.id);
191
+ return {
192
+ value: preset.id,
193
+ name: builtIn?.name ?? preset.name ?? preset.id,
194
+ description: builtIn?.description ?? preset.description ?? null
195
+ };
196
+ })
197
+ },
198
+ {
199
+ id: MODEL_CONFIG_ID,
200
+ name: "Model",
201
+ description: "DeepSeek model used for the session",
202
+ category: "model",
203
+ type: "select",
204
+ currentValue: record.selection.current.model,
205
+ options: DEEPSEEK_HARNESS_MODELS.map((model) => ({
206
+ value: model.modelId,
207
+ name: model.name,
208
+ description: model.description ?? null
209
+ }))
210
+ },
211
+ {
212
+ id: REASONING_EFFORT_CONFIG_ID,
213
+ name: "Reasoning effort",
214
+ description: "How much reasoning effort the model should use",
215
+ category: "thought_level",
216
+ type: "select",
217
+ currentValue: record.selection.current.reasoningEffort,
218
+ options: DEEPSEEK_HARNESS_REASONING_OPTIONS.map((effort) => ({
219
+ value: effort.value,
220
+ name: effort.name,
221
+ description: effort.description ?? null
222
+ }))
223
+ }
224
+ ];
225
+ }
226
+ function modeState(record) {
227
+ return {
228
+ currentModeId: record.permissionMode,
229
+ availableModes: DEEPSEEK_HARNESS_PERMISSION_MODES.map((mode) => ({
230
+ id: mode.id,
231
+ name: mode.name,
232
+ description: mode.description ?? null
233
+ }))
234
+ };
235
+ }
236
+ function acpPromptToText(prompt) {
237
+ return prompt.flatMap((block) => {
238
+ if (block.type === "text")
239
+ return [block.text];
240
+ if (block.type === "resource_link") {
241
+ return [
242
+ `
243
+ [resource_link name=${JSON.stringify(block.name)} uri=${JSON.stringify(block.uri)}]
244
+ `
245
+ ];
246
+ }
247
+ return [];
248
+ }).join("");
249
+ }
250
+ function promptHasUnsupportedContent(prompt) {
251
+ return prompt.some((block) => block.type !== "text" && block.type !== "resource_link");
252
+ }
253
+ function createUserMessage(text) {
254
+ return Object.freeze({
255
+ id: randomUUID(),
256
+ role: "user",
257
+ content: [Object.freeze({ type: "text", text })],
258
+ source: Object.freeze({ kind: "user" })
259
+ });
260
+ }
261
+ function turnEndToStopReason(reason) {
262
+ switch (reason.kind) {
263
+ case "completed":
264
+ return "end_turn";
265
+ case "max-tokens":
266
+ return "max_tokens";
267
+ case "interrupted":
268
+ return "cancelled";
269
+ case "aborted":
270
+ case "blocked":
271
+ case "error":
272
+ return "end_turn";
273
+ default:
274
+ return "end_turn";
275
+ }
276
+ }
277
+ function errorChain(value) {
278
+ const seen = /* @__PURE__ */ new Set();
279
+ const render = (current) => {
280
+ if (seen.has(current))
281
+ return "<circular cause>";
282
+ seen.add(current);
283
+ try {
284
+ if (!(current instanceof Error))
285
+ return String(current);
286
+ const message = current.message || current.name;
287
+ const cause = current.cause == null ? "" : render(current.cause);
288
+ return cause && cause !== message ? `${message}: ${cause}` : message;
289
+ } finally {
290
+ seen.delete(current);
291
+ }
292
+ };
293
+ return render(value);
294
+ }
295
+ function validateSessionParams(params) {
296
+ if (!isAbsolute(params.cwd)) {
297
+ throw invalidParams(`cwd must be an absolute path: ${params.cwd}`);
298
+ }
299
+ if (params.additionalDirectories && params.additionalDirectories.length > 0) {
300
+ throw invalidParams("additionalDirectories is not supported");
301
+ }
302
+ for (const server of params.mcpServers) {
303
+ if ("type" in server && server.type !== "http") {
304
+ throw invalidParams(`MCP transport ${server.type} is not supported`);
305
+ }
306
+ }
307
+ }
308
+ function requireSelectValue(params) {
309
+ if (typeof params.value !== "string") {
310
+ throw invalidParams(`${params.configId} requires a select value`);
311
+ }
312
+ return params.value;
313
+ }
314
+ function assertAllowed(value, allowed, label) {
315
+ if (!allowed.has(value)) {
316
+ throw invalidParams(`unknown ${label}: ${value} (available: ${[...allowed].join(", ")})`);
317
+ }
318
+ }
319
+ function apply(ctx, rawConfig) {
320
+ const config = resolveAdapterConfig(rawConfig);
321
+ const sessions = /* @__PURE__ */ new Map();
322
+ const activeMcpServerNames = /* @__PURE__ */ new Set();
323
+ let closed = false;
324
+ let conn;
325
+ for (const mode of PERMISSION_MODE_IDS) {
326
+ if (!ctx.permissionPresets.names.includes(mode)) {
327
+ throw new Error(`acp-extension-dsh: permission preset ${JSON.stringify(mode)} is not composed`);
328
+ }
329
+ }
330
+ const assertOpen = () => {
331
+ if (closed)
332
+ throw internalError("the ACP bridge has been disposed");
333
+ };
334
+ const requireSession = (sessionId) => {
335
+ const record = sessions.get(sessionId);
336
+ if (!record)
337
+ throw invalidParams(`unknown session: ${sessionId}`);
338
+ return record;
339
+ };
340
+ const ownedRecord = (agent) => {
341
+ const record = sessions.get(agent.session.id);
342
+ return record?.agent === agent ? record : void 0;
343
+ };
344
+ const notify = (notification) => {
345
+ void conn.sessionUpdate(notification).catch((error) => {
346
+ ctx.logger.warn(`acp-extension-dsh: session/update failed: ${String(error)}`);
347
+ });
348
+ };
349
+ const settlePrompt = (record, reason) => {
350
+ const inflight = record.inflight;
351
+ if (!inflight)
352
+ return;
353
+ record.inflight = void 0;
354
+ inflight.resolve(reason);
355
+ };
356
+ const disposeRecords = async (records) => {
357
+ const subagents = ctx.get("subagents");
358
+ if (subagents) {
359
+ try {
360
+ await subagents.drainContinuableDescendants(records.map((record) => record.agent));
361
+ } catch (error) {
362
+ ctx.logger.warn(`acp-extension-dsh: continuable subagent teardown failed: ${String(error)}`);
363
+ }
364
+ }
365
+ const results = await Promise.allSettled(records.map((record) => record.dispose()));
366
+ const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
367
+ if (failures.length > 0) {
368
+ throw new AggregateError(failures, `DeepSeek ACP teardown failed for ${failures.length} session(s): ${failures.map(errorChain).join("; ")}`);
369
+ }
370
+ };
371
+ ctx.on("session/event", (session, event) => {
372
+ const record = sessions.get(session.header.id);
373
+ if (!record || record.agent.session !== session)
374
+ return;
375
+ try {
376
+ if (event.type === "assistant/message") {
377
+ for (const block of event.data.message?.content ?? []) {
378
+ if (block.type === "text" && "text" in block && block.text.length > 0) {
379
+ notify({
380
+ sessionId: record.agent.session.id,
381
+ update: {
382
+ sessionUpdate: "agent_message_chunk",
383
+ content: { type: "text", text: block.text }
384
+ }
385
+ });
386
+ } else if (block.type === "image" && "attachment" in block) {
387
+ notify({
388
+ sessionId: record.agent.session.id,
389
+ update: {
390
+ sessionUpdate: "agent_message_chunk",
391
+ content: {
392
+ type: "text",
393
+ text: `[image attachment ${block.attachment.attachmentId}]`
394
+ }
395
+ }
396
+ });
397
+ }
398
+ }
399
+ }
400
+ } finally {
401
+ const inflight = record.inflight;
402
+ if (inflight && event.type === "turn/end" && inflight.turn === event.data.turn && event.data.reason) {
403
+ if (event.data.reason.kind === "error") {
404
+ record.inflight = void 0;
405
+ inflight.reject(internalError(`turn failed: ${event.data.reason.error.message}`));
406
+ } else {
407
+ inflight.endReason = event.data.reason;
408
+ }
409
+ }
410
+ }
411
+ });
412
+ ctx.on("agent/inbox/claimed", ({ agent, message, turn }) => {
413
+ const inflight = ownedRecord(agent)?.inflight;
414
+ if (inflight && inflight.messageId === message.id)
415
+ inflight.turn = turn;
416
+ });
417
+ ctx.on("agent/error", ({ agent, turn, error }) => {
418
+ const record = ownedRecord(agent);
419
+ const inflight = record?.inflight;
420
+ if (!record || !inflight || inflight.turn !== void 0 && inflight.turn !== turn)
421
+ return;
422
+ record.inflight = void 0;
423
+ inflight.reject(internalError(`turn failed: ${errorChain(error)}`));
424
+ });
425
+ ctx.on("approval/request", (request, next) => {
426
+ const record = ownedRecord(request.agent);
427
+ if (!record || !request.callId)
428
+ return next();
429
+ return conn.requestPermission({
430
+ sessionId: record.agent.session.id,
431
+ toolCall: { toolCallId: request.callId },
432
+ options: [
433
+ { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
434
+ { optionId: "reject-once", name: "Reject", kind: "reject_once" }
435
+ ]
436
+ }).then(({ outcome }) => {
437
+ if (outcome.outcome === "cancelled")
438
+ return "cancelled";
439
+ return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
440
+ });
441
+ });
442
+ const setPermissionMode = (record, modeId) => {
443
+ assertAllowed(modeId, PERMISSION_MODE_IDS, "permission mode");
444
+ ctx.permissionPresets.set(record.agent.session, modeId);
445
+ record.permissionMode = modeId;
446
+ };
447
+ const setConfigOption = (record, params) => {
448
+ const value = requireSelectValue(params);
449
+ if (params.configId === MODE_CONFIG_ID) {
450
+ setPermissionMode(record, value);
451
+ } else if (params.configId === AGENT_PRESET_CONFIG_ID) {
452
+ const available = new Set(record.agentPresetOptions.map((preset) => preset.id));
453
+ assertAllowed(value, available, "agent preset");
454
+ if (value === record.agentPreset)
455
+ return { configOptions: configOptions(record) };
456
+ if (record.started) {
457
+ throw invalidParams("agent preset is fixed after the session has started");
458
+ }
459
+ return ctx.agentPresets.recompose(record.agent.ctx, value).then((preset) => {
460
+ record.agent.session.append("agent-preset/selected", { agentPreset: preset.id });
461
+ record.agentPreset = preset.id;
462
+ return { configOptions: configOptions(record) };
463
+ }).catch((error) => {
464
+ if (error instanceof RequestError)
465
+ throw error;
466
+ throw invalidParams(`failed to select agent preset ${JSON.stringify(value)}: ${errorChain(error)}`);
467
+ });
468
+ } else if (params.configId === MODEL_CONFIG_ID) {
469
+ assertAllowed(value, MODEL_IDS, "model");
470
+ record.selection.current = { ...record.selection.current, model: value };
471
+ } else if (params.configId === REASONING_EFFORT_CONFIG_ID) {
472
+ assertAllowed(value, REASONING_EFFORT_IDS, "reasoning effort");
473
+ record.selection.current = {
474
+ ...record.selection.current,
475
+ reasoningEffort: value
476
+ };
477
+ } else {
478
+ throw invalidParams(`unknown config option: ${params.configId}`);
479
+ }
480
+ return { configOptions: configOptions(record) };
481
+ };
482
+ const makeAgent = (connection) => {
483
+ conn = connection;
484
+ return {
485
+ initialize(_params) {
486
+ return Promise.resolve({
487
+ protocolVersion: PROTOCOL_VERSION,
488
+ agentInfo: { name: "acp-extension-dsh", version: ACP_EXTENSION_DSH_VERSION },
489
+ agentCapabilities: {
490
+ promptCapabilities: { image: false, audio: false, embeddedContext: false },
491
+ mcpCapabilities: { http: true },
492
+ sessionCapabilities: { close: {} }
493
+ },
494
+ authMethods: []
495
+ });
496
+ },
497
+ authenticate(_params) {
498
+ return Promise.resolve();
499
+ },
500
+ async newSession(params) {
501
+ assertOpen();
502
+ validateSessionParams(params);
503
+ const sessionId = randomUUID();
504
+ const selection = {
505
+ current: {
506
+ provider: config.provider,
507
+ model: config.model,
508
+ reasoningEffort: config.reasoningEffort
509
+ }
510
+ };
511
+ const agentPresetOptions = (await ctx.agentPresets.list()).filter((preset) => preset.broken === void 0);
512
+ const requestedPreset = ctx.agentPresets.defaultId;
513
+ if (!agentPresetOptions.some((preset) => preset.id === requestedPreset)) {
514
+ throw internalError(`default agent preset ${JSON.stringify(requestedPreset)} is unavailable`);
515
+ }
516
+ const mcpServerNames = reserveMcpServerNames(params.mcpServers, sessionId, activeMcpServerNames);
517
+ let mountedPreset = requestedPreset;
518
+ let handle;
519
+ try {
520
+ handle = await ctx.agents.create({
521
+ sessionId,
522
+ meta: { cwd: params.cwd, agentPreset: requestedPreset },
523
+ agentOptions: { provider: config.provider, model: config.model },
524
+ setup: async (agentContext) => {
525
+ installModelSelection(agentContext, selection);
526
+ mountedPreset = (await ctx.agentPresets.mount(agentContext, requestedPreset)).id;
527
+ await mountMcpServers(agentContext, params.mcpServers, mcpServerNames.names, params.cwd);
528
+ }
529
+ });
530
+ } catch (error) {
531
+ mcpServerNames.release();
532
+ if (error instanceof RequestError)
533
+ throw error;
534
+ throw internalError(`failed to create session: ${errorChain(error)}`);
535
+ }
536
+ const dispose = async () => {
537
+ try {
538
+ await handle.dispose();
539
+ } finally {
540
+ mcpServerNames.release();
541
+ }
542
+ };
543
+ if (closed) {
544
+ await dispose();
545
+ throw internalError("connection closed during session/new");
546
+ }
547
+ let permissionMode;
548
+ try {
549
+ permissionMode = ctx.permissionPresets.current(handle.agent.session.events);
550
+ assertAllowed(permissionMode, PERMISSION_MODE_IDS, "permission mode");
551
+ } catch (error) {
552
+ await dispose();
553
+ throw error;
554
+ }
555
+ const record = {
556
+ agent: handle.agent,
557
+ dispose,
558
+ selection,
559
+ permissionMode,
560
+ agentPreset: mountedPreset,
561
+ agentPresetOptions,
562
+ started: false
563
+ };
564
+ sessions.set(sessionId, record);
565
+ return {
566
+ sessionId,
567
+ modes: modeState(record),
568
+ configOptions: configOptions(record)
569
+ };
570
+ },
571
+ setSessionMode(params) {
572
+ const record = requireSession(params.sessionId);
573
+ setPermissionMode(record, params.modeId);
574
+ },
575
+ setSessionConfigOption(params) {
576
+ return setConfigOption(requireSession(params.sessionId), params);
577
+ },
578
+ async prompt(params) {
579
+ assertOpen();
580
+ const record = requireSession(params.sessionId);
581
+ if (record.inflight)
582
+ throw invalidParams("a prompt is already in flight for this session");
583
+ if (promptHasUnsupportedContent(params.prompt)) {
584
+ throw invalidParams("only text and resource_link prompt content is supported");
585
+ }
586
+ const text = acpPromptToText(params.prompt);
587
+ if (!text.trim())
588
+ throw invalidParams("empty prompt");
589
+ if (ctx.agents.get(record.agent.id) !== record.agent) {
590
+ throw internalError("prompt was not queued: the agent was disposed outside the bridge");
591
+ }
592
+ const message = createUserMessage(text);
593
+ record.started = true;
594
+ const stopReason = await new Promise((resolve, reject) => {
595
+ const inflight = { resolve, reject, messageId: message.id };
596
+ record.inflight = inflight;
597
+ try {
598
+ record.agent.followup(message);
599
+ } catch (error) {
600
+ record.inflight = void 0;
601
+ record.started = false;
602
+ throw internalError(`prompt was not queued: ${error instanceof Error ? error.message : String(error)}`);
603
+ }
604
+ void record.agent.whenIdle().then(() => {
605
+ if (record.inflight !== inflight)
606
+ return;
607
+ record.inflight = void 0;
608
+ const end = inflight.endReason;
609
+ inflight.resolve(end ? end.kind === "max-tokens" ? "end_turn" : turnEndToStopReason(end) : "cancelled");
610
+ });
611
+ });
612
+ return { stopReason };
613
+ },
614
+ cancel(params) {
615
+ const record = sessions.get(params.sessionId);
616
+ if (!record)
617
+ return Promise.resolve();
618
+ record.agent.cancel({ kind: "user" });
619
+ settlePrompt(record, "cancelled");
620
+ return Promise.resolve();
621
+ },
622
+ async closeSession(params) {
623
+ const record = requireSession(params.sessionId);
624
+ sessions.delete(params.sessionId);
625
+ record.agent.cancel({ kind: "user" });
626
+ settlePrompt(record, "cancelled");
627
+ await disposeRecords([record]);
628
+ }
629
+ };
630
+ };
631
+ const stream = config.stream ?? ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
632
+ conn = new AgentSideConnection(makeAgent, stream);
633
+ let quiescing;
634
+ const quiesce = () => {
635
+ if (quiescing)
636
+ return quiescing;
637
+ closed = true;
638
+ const records = [...sessions.values()];
639
+ sessions.clear();
640
+ for (const record of records) {
641
+ record.agent.cancel({ kind: "user" });
642
+ settlePrompt(record, "cancelled");
643
+ }
644
+ quiescing = (async () => {
645
+ await disposeRecords(records);
646
+ })();
647
+ return quiescing;
648
+ };
649
+ void conn.closed.catch((error) => {
650
+ ctx.logger.warn(`acp-extension-dsh: connection closed with an error: ${String(error)}`);
651
+ }).then(quiesce).catch((error) => {
652
+ ctx.logger.warn(`acp-extension-dsh: connection-close teardown failed: ${String(error)}`);
653
+ });
654
+ ctx.effect(() => quiesce, "acp-extension-dsh.connection");
655
+ }
656
+ export {
657
+ apply,
658
+ inject,
659
+ name
660
+ };