@love-moon/ai-sdk 0.7.7 → 0.9.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/context-files.d.ts +16 -0
  5. package/dist/context-files.js +86 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +4 -1
  8. package/dist/manager/index.d.ts +1 -0
  9. package/dist/manager/index.js +1 -0
  10. package/dist/manager/install.js +29 -2
  11. package/dist/manager/manager.d.ts +5 -1
  12. package/dist/manager/manager.js +8 -0
  13. package/dist/manager/network.js +4 -2
  14. package/dist/manager/quota/dsh.d.ts +55 -0
  15. package/dist/manager/quota/dsh.js +146 -0
  16. package/dist/manager/types.d.ts +26 -1
  17. package/dist/media-adapters.d.ts +34 -0
  18. package/dist/media-adapters.js +57 -0
  19. package/dist/media-input.d.ts +21 -0
  20. package/dist/media-input.js +197 -0
  21. package/dist/providers/chat-web-session.d.ts +7 -1
  22. package/dist/providers/chat-web-session.js +12 -1
  23. package/dist/providers/claude-agent-sdk-session.d.ts +10 -1
  24. package/dist/providers/claude-agent-sdk-session.js +24 -4
  25. package/dist/providers/codex-app-server-session.d.ts +12 -1
  26. package/dist/providers/codex-app-server-session.js +49 -20
  27. package/dist/providers/codex-exec-session.d.ts +8 -3
  28. package/dist/providers/codex-exec-session.js +13 -13
  29. package/dist/providers/copilot-sdk-session.d.ts +6 -1
  30. package/dist/providers/copilot-sdk-session.js +10 -3
  31. package/dist/providers/dsh-sdk-session.d.ts +194 -0
  32. package/dist/providers/dsh-sdk-session.js +951 -0
  33. package/dist/providers/kimi-cli-session.d.ts +11 -1
  34. package/dist/providers/kimi-cli-session.js +58 -4
  35. package/dist/providers/kimi-print-session.d.ts +10 -3
  36. package/dist/providers/kimi-print-session.js +23 -53
  37. package/dist/providers/opencode-sdk-session.d.ts +6 -1
  38. package/dist/providers/opencode-sdk-session.js +11 -9
  39. package/dist/resume/dsh.d.ts +23 -0
  40. package/dist/resume/dsh.js +122 -0
  41. package/dist/resume/index.js +2 -0
  42. package/dist/session-factory.d.ts +3 -1
  43. package/dist/session-factory.js +5 -1
  44. package/dsh/cordis.yml +100 -0
  45. package/package.json +24 -3
@@ -0,0 +1,951 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import fs from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { DSH_SDK_VARIANT } from "../built-in-backends.js";
9
+ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
10
+ const require = createRequire(import.meta.url);
11
+ const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
12
+ const MIN_TURN_DEADLINE_MS = 30 * 1000;
13
+ const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
14
+ // Pinned defaults for the dsh runtime route. `deepseek-official` is the
15
+ // provider route the stock `@deepseek-ai/dsh-llm-deepseek` adapter registers;
16
+ // the model can be overridden via options.model or an allow_cli_list command
17
+ // like `dsh --model deepseek-v4`.
18
+ const DEFAULT_DSH_PROVIDER = "deepseek-official";
19
+ const DEFAULT_DSH_MODEL = "deepseek-v4-flash";
20
+ const CORDIS_CONFIG_PATH = fileURLToPath(new URL("../../dsh/cordis.yml", import.meta.url));
21
+ function waitForever() {
22
+ return new Promise(() => { });
23
+ }
24
+ function createTurnError(message, extras = {}) {
25
+ const error = new Error(message);
26
+ for (const [key, value] of Object.entries(extras)) {
27
+ error[key] = value;
28
+ }
29
+ return error;
30
+ }
31
+ function normalizeDshBackend(backend) {
32
+ const normalized = String(backend || "").trim().toLowerCase();
33
+ if (normalized === "deepseek-harness") {
34
+ return "dsh";
35
+ }
36
+ return normalized || "dsh";
37
+ }
38
+ export function defaultDshSessionRoot() {
39
+ return path.join(os.homedir(), ".conductor", "dsh-sessions");
40
+ }
41
+ function extractTextBlocks(content) {
42
+ if (!Array.isArray(content)) {
43
+ return "";
44
+ }
45
+ return content
46
+ .map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
47
+ .filter(Boolean)
48
+ .join("");
49
+ }
50
+ function toolPhaseForName(toolName) {
51
+ const normalized = String(toolName || "").trim().toLowerCase();
52
+ if (!normalized) {
53
+ return "tool_call";
54
+ }
55
+ if (normalized.includes("bash") || normalized.includes("pwsh") || normalized.includes("terminal")) {
56
+ return "command_execution";
57
+ }
58
+ if (normalized.includes("str_replace") || normalized.includes("edit") || normalized.includes("write")) {
59
+ return "file_update";
60
+ }
61
+ if (normalized.includes("fs") || normalized.includes("read") || normalized.includes("search") || normalized.includes("glob")) {
62
+ return "workspace_inspection";
63
+ }
64
+ if (normalized.includes("web") || normalized.includes("fetch")) {
65
+ return "web_lookup";
66
+ }
67
+ if (normalized.includes("subagent") || normalized.includes("todo") || normalized.includes("workflow")) {
68
+ return "task_progress";
69
+ }
70
+ return "tool_call";
71
+ }
72
+ function statusLineForPhase(phase, toolName = "") {
73
+ switch (phase) {
74
+ case "context_compaction":
75
+ return "dsh compacting context";
76
+ case "command_execution":
77
+ return "dsh running command";
78
+ case "file_update":
79
+ return "dsh editing files";
80
+ case "workspace_inspection":
81
+ return "dsh reading workspace";
82
+ case "web_lookup":
83
+ return "dsh browsing";
84
+ case "message_aggregation":
85
+ return "dsh composing reply";
86
+ case "task_progress":
87
+ return toolName ? `dsh running ${toolName}` : "dsh running task";
88
+ case "tool_call":
89
+ return toolName ? `dsh calling ${toolName}` : "dsh calling tool";
90
+ default:
91
+ return "dsh is working";
92
+ }
93
+ }
94
+ function accumulateUsage(total, usage) {
95
+ if (!usage || typeof usage !== "object") {
96
+ return total;
97
+ }
98
+ const next = total && typeof total === "object" ? { ...total } : {};
99
+ for (const key of [
100
+ "inputTokens",
101
+ "outputTokens",
102
+ "cacheReadTokens",
103
+ "cacheWriteTokens",
104
+ "reasoningTokens",
105
+ ]) {
106
+ const value = Number(usage[key]);
107
+ if (Number.isFinite(value)) {
108
+ next[key] = (Number.isFinite(Number(next[key])) ? Number(next[key]) : 0) + value;
109
+ }
110
+ }
111
+ return next;
112
+ }
113
+ /**
114
+ * DeepSeek Harness (dsh) backend session.
115
+ *
116
+ * Drives a dsh runtime subprocess through `@deepseek-ai/dsh-sdk-client`
117
+ * (stdio JSON-RPC). The runtime composition is the package-owned
118
+ * `dsh/cordis.yml`; the pinned runtime bin comes from
119
+ * `@deepseek-ai/dsh-sdk-jsonrpc-demo`. One DshSdkSession owns at most one
120
+ * runtime subprocess across turns; the dsh-side session id is minted by us
121
+ * (or taken from resumeSessionId) so the same conversation resumes across
122
+ * runtime restarts through dsh's JSONL session persistence.
123
+ *
124
+ * Interrupt semantics: the dsh SDK wire has NO mid-turn cancel. Interrupting
125
+ * closes the runtime subprocess (SIGTERM ladder inside the SDK client) and
126
+ * the next runTurn spawns a fresh runtime resuming the same session id.
127
+ */
128
+ export class DshSdkSession extends EventEmitter {
129
+ static capabilities = Object.freeze({ goal: false });
130
+ getCapabilities() {
131
+ return { ...DshSdkSession.capabilities };
132
+ }
133
+ constructor(backend, options = {}) {
134
+ super();
135
+ this.backend = normalizeDshBackend(backend);
136
+ this.options = options;
137
+ this.logger = normalizeLogger(options.logger);
138
+ this.cwd =
139
+ typeof options.cwd === "string" && options.cwd.trim()
140
+ ? options.cwd.trim()
141
+ : process.cwd();
142
+ this.resumeSessionId =
143
+ typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
144
+ // dsh session ids are client-minted. The SDK runtime (rc.6) can only
145
+ // CREATE sessions — prompting an id whose JSONL log already exists on
146
+ // disk fails with an id collision. So the wire id is always fresh;
147
+ // resuming seeds the previous session's conversation history into the
148
+ // first prompt instead (loaded lazily from the persisted log).
149
+ this.pendingResumeFromSessionId = this.resumeSessionId;
150
+ this.sessionId = `session-${randomUUID().replaceAll("-", "")}`;
151
+ this.sessionIdUsed = false;
152
+ this.model =
153
+ (typeof options.model === "string" && options.model.trim()) ||
154
+ extractLongFlagFromCommandLine(options.commandLine, "model") ||
155
+ DEFAULT_DSH_MODEL;
156
+ this.dshProvider =
157
+ (typeof options.dshProvider === "string" && options.dshProvider.trim()) ||
158
+ extractLongFlagFromCommandLine(options.commandLine, "provider") ||
159
+ DEFAULT_DSH_PROVIDER;
160
+ const maxTokens = Number(options.maxTokens);
161
+ this.maxTokens = Number.isFinite(maxTokens) && maxTokens > 0 ? Math.floor(maxTokens) : undefined;
162
+ this.sessionRoot =
163
+ typeof options.dshSessionRoot === "string" && options.dshSessionRoot.trim()
164
+ ? options.dshSessionRoot.trim()
165
+ : defaultDshSessionRoot();
166
+ this.sessionInfo = {
167
+ backend: this.backend,
168
+ sessionId: this.sessionId,
169
+ model: this.model,
170
+ modelProvider: this.dshProvider,
171
+ };
172
+ this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
173
+ this.pendingHistorySeed = this.history.length > 0;
174
+ this.closeRequested = false;
175
+ this.closed = false;
176
+ this.closeWaiters = new Set();
177
+ this.sessionMessageHandler = null;
178
+ this.workingStatusHandler = null;
179
+ this.activeReplyTarget = "";
180
+ this.lastReplyTarget = "";
181
+ this.manualResumeReady = Boolean(this.resumeSessionId);
182
+ this.sessionAnnounced = false;
183
+ this.currentTurn = null;
184
+ this.harness = null;
185
+ this.lastUsage = null;
186
+ this.currentTurnStatus = null;
187
+ this.currentTurnActivityAt = 0;
188
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
189
+ this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
190
+ this.sdkModulePromise = null;
191
+ const envConfig = loadEnvConfig(options.configFile);
192
+ const proxyEnv = proxyToEnv(envConfig);
193
+ const extraEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
194
+ this.env = {
195
+ ...extraEnv,
196
+ ...(options.env && typeof options.env === "object" ? options.env : {}),
197
+ };
198
+ }
199
+ writeLog(message) {
200
+ emitLog(this.logger, message);
201
+ }
202
+ trace(message) {
203
+ this.writeLog(`[${this.backend}] [dsh-sdk] ${message}`);
204
+ }
205
+ getSnapshot() {
206
+ return {
207
+ backend: this.backend,
208
+ provider: DSH_SDK_VARIANT,
209
+ cwd: this.cwd,
210
+ sessionId: this.sessionId,
211
+ sessionInfo: this.getSessionInfo(),
212
+ useSessionFileReplyStream: true,
213
+ resumeReady: this.manualResumeReady,
214
+ manualResume: null,
215
+ currentTurnStatus: this.getCurrentTurnStatus(),
216
+ capabilities: this.getCapabilities(),
217
+ };
218
+ }
219
+ getSessionInfo() {
220
+ return this.sessionInfo ? { ...this.sessionInfo } : null;
221
+ }
222
+ getCurrentTurnStatus() {
223
+ return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
224
+ }
225
+ async ensureSessionInfo() {
226
+ return this.getSessionInfo();
227
+ }
228
+ async getSessionUsageSummary() {
229
+ return {
230
+ sessionId: this.sessionId,
231
+ sessionFilePath: undefined,
232
+ totalCostUsd: undefined,
233
+ usage: this.lastUsage ? { ...this.lastUsage } : null,
234
+ modelUsage: null,
235
+ rateLimits: null,
236
+ manualResume: null,
237
+ };
238
+ }
239
+ usesSessionFileReplyStream() {
240
+ return true;
241
+ }
242
+ setSessionMessageHandler(handler) {
243
+ this.sessionMessageHandler = typeof handler === "function" ? handler : null;
244
+ }
245
+ setWorkingStatusHandler(handler) {
246
+ this.workingStatusHandler = typeof handler === "function" ? handler : null;
247
+ }
248
+ setSessionReplyTarget(replyTo) {
249
+ const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
250
+ this.activeReplyTarget = normalizedReplyTo;
251
+ if (normalizedReplyTo) {
252
+ this.lastReplyTarget = normalizedReplyTo;
253
+ }
254
+ }
255
+ getCurrentReplyTarget() {
256
+ return this.activeReplyTarget || this.lastReplyTarget || undefined;
257
+ }
258
+ touchTurnActivity() {
259
+ this.currentTurnActivityAt = this.now();
260
+ }
261
+ updateCurrentTurnStatus(payload) {
262
+ const updatedAtMs = this.now();
263
+ this.currentTurnActivityAt = updatedAtMs;
264
+ this.currentTurnStatus = {
265
+ ...payload,
266
+ updated_at: new Date(updatedAtMs).toISOString(),
267
+ };
268
+ }
269
+ async emitWorkingStatus(payload, onProgress = null) {
270
+ const normalized = {
271
+ source: DSH_SDK_VARIANT,
272
+ reply_in_progress: Boolean(payload?.reply_in_progress),
273
+ replyTo: payload?.replyTo || this.getCurrentReplyTarget(),
274
+ state: payload?.state,
275
+ phase: payload?.phase,
276
+ status_line: payload?.status_line,
277
+ status_done_line: payload?.status_done_line,
278
+ reply_preview: payload?.reply_preview,
279
+ thread_id: this.sessionId,
280
+ };
281
+ this.updateCurrentTurnStatus(normalized);
282
+ const snapshot = this.getCurrentTurnStatus();
283
+ if (typeof onProgress === "function") {
284
+ onProgress(snapshot);
285
+ }
286
+ if (typeof this.workingStatusHandler === "function") {
287
+ await this.workingStatusHandler(snapshot);
288
+ }
289
+ this.emit("working_status", snapshot);
290
+ }
291
+ async emitAssistantMessage(text) {
292
+ this.touchTurnActivity();
293
+ const payload = {
294
+ text,
295
+ preserveWhitespace: true,
296
+ source: DSH_SDK_VARIANT,
297
+ replyTo: this.getCurrentReplyTarget(),
298
+ sessionId: this.sessionId,
299
+ sessionFilePath: undefined,
300
+ timestamp: new Date().toISOString(),
301
+ };
302
+ if (typeof this.sessionMessageHandler === "function") {
303
+ await this.sessionMessageHandler(payload);
304
+ }
305
+ this.emit("assistant_message", payload);
306
+ }
307
+ async emitTerminalWorkingStatus(currentTurn, payload, onProgress = null) {
308
+ if (!currentTurn || currentTurn.terminalWorkingStatusEmitted) {
309
+ return;
310
+ }
311
+ currentTurn.terminalWorkingStatusEmitted = true;
312
+ await this.emitWorkingStatus({
313
+ ...payload,
314
+ reply_in_progress: false,
315
+ }, onProgress);
316
+ }
317
+ announceSession() {
318
+ if (this.sessionAnnounced) {
319
+ return;
320
+ }
321
+ this.sessionAnnounced = true;
322
+ this.manualResumeReady = true;
323
+ this.trace(`session ready id=${this.sessionId}`);
324
+ this.emit("session", this.getSessionInfo());
325
+ }
326
+ /**
327
+ * Mint a fresh wire session id after the previous one's runtime was torn
328
+ * down (interrupt/timeout). The persisted log of the old id would collide
329
+ * on the next create, so the conversation continues on a new id seeded
330
+ * with the in-memory history; the new id is re-announced on success.
331
+ */
332
+ rotateSessionId() {
333
+ const previousSessionId = this.sessionId;
334
+ this.sessionId = `session-${randomUUID().replaceAll("-", "")}`;
335
+ this.sessionIdUsed = false;
336
+ this.sessionAnnounced = false;
337
+ this.pendingHistorySeed = this.history.length > 0;
338
+ this.sessionInfo = { ...this.sessionInfo, sessionId: this.sessionId };
339
+ this.trace(`rotated session id ${previousSessionId} -> ${this.sessionId}`);
340
+ }
341
+ findPersistedSessionLog(sessionId) {
342
+ const normalized = typeof sessionId === "string" ? sessionId.trim() : "";
343
+ if (!normalized) {
344
+ return null;
345
+ }
346
+ let projectEntries = [];
347
+ try {
348
+ projectEntries = fs.readdirSync(this.sessionRoot, { withFileTypes: true });
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ for (const projectEntry of projectEntries) {
354
+ if (!projectEntry.isDirectory()) {
355
+ continue;
356
+ }
357
+ const candidate = path.join(this.sessionRoot, projectEntry.name, normalized, "session.jsonl");
358
+ try {
359
+ if (fs.statSync(candidate).isFile()) {
360
+ return candidate;
361
+ }
362
+ }
363
+ catch {
364
+ // keep scanning
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+ /**
370
+ * Recover the user/assistant conversation from a persisted dsh JSONL log
371
+ * so a cross-process resume can seed it into the fresh wire session.
372
+ */
373
+ loadPersistedHistory(sessionId) {
374
+ const logPath = this.findPersistedSessionLog(sessionId);
375
+ if (!logPath) {
376
+ return [];
377
+ }
378
+ const restored = [];
379
+ let content;
380
+ try {
381
+ content = fs.readFileSync(logPath, "utf8");
382
+ }
383
+ catch {
384
+ return [];
385
+ }
386
+ for (const line of content.split("\n")) {
387
+ const trimmed = line.trim();
388
+ if (!trimmed) {
389
+ continue;
390
+ }
391
+ let event;
392
+ try {
393
+ event = JSON.parse(trimmed);
394
+ }
395
+ catch {
396
+ continue;
397
+ }
398
+ if (event?.type === "user/message" && event.data?.source?.kind === "user") {
399
+ const text = extractTextBlocks(event.data?.content);
400
+ if (text) {
401
+ restored.push({ role: "user", content: text });
402
+ }
403
+ continue;
404
+ }
405
+ if (event?.type === "assistant/message") {
406
+ const text = extractTextBlocks(event.data?.message?.content);
407
+ if (text) {
408
+ restored.push({ role: "assistant", content: text });
409
+ }
410
+ }
411
+ }
412
+ return restored;
413
+ }
414
+ createSessionClosedError() {
415
+ const error = new Error("dsh SDK session closed");
416
+ error.reason = "session_closed";
417
+ return error;
418
+ }
419
+ createTurnTimeoutError(timeoutMs) {
420
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
421
+ const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
422
+ error.reason = "turn_timeout";
423
+ error.timeoutMs = timeoutMs;
424
+ return error;
425
+ }
426
+ createCloseGuard(onClose) {
427
+ if (this.closeRequested) {
428
+ return {
429
+ promise: Promise.reject(this.createSessionClosedError()),
430
+ cleanup: () => { },
431
+ };
432
+ }
433
+ let waiter = null;
434
+ const promise = new Promise((_, reject) => {
435
+ waiter = () => {
436
+ try {
437
+ onClose?.();
438
+ }
439
+ catch {
440
+ // best effort
441
+ }
442
+ reject(this.createSessionClosedError());
443
+ };
444
+ this.closeWaiters.add(waiter);
445
+ });
446
+ return {
447
+ promise,
448
+ cleanup: () => {
449
+ if (waiter) {
450
+ this.closeWaiters.delete(waiter);
451
+ }
452
+ },
453
+ };
454
+ }
455
+ createTurnTimeoutGuard(onTimeout) {
456
+ if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
457
+ return {
458
+ promise: waitForever(),
459
+ cleanup: () => { },
460
+ };
461
+ }
462
+ let timer = null;
463
+ let settled = false;
464
+ const schedule = (reject) => {
465
+ const now = this.now();
466
+ const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
467
+ ? this.currentTurnActivityAt
468
+ : now;
469
+ const elapsedMs = Math.max(0, now - lastActivityAt);
470
+ const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
471
+ timer = setTimeout(() => {
472
+ if (settled) {
473
+ return;
474
+ }
475
+ const activityNow = this.now();
476
+ const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
477
+ ? this.currentTurnActivityAt
478
+ : activityNow;
479
+ if (activityNow - latestActivityAt < this.turnDeadlineMs) {
480
+ schedule(reject);
481
+ return;
482
+ }
483
+ settled = true;
484
+ try {
485
+ onTimeout?.();
486
+ }
487
+ catch {
488
+ // best effort
489
+ }
490
+ reject(this.createTurnTimeoutError(this.turnDeadlineMs));
491
+ }, waitMs);
492
+ if (typeof timer?.unref === "function") {
493
+ timer.unref();
494
+ }
495
+ };
496
+ const promise = new Promise((_, reject) => {
497
+ schedule(reject);
498
+ });
499
+ return {
500
+ promise,
501
+ cleanup: () => {
502
+ settled = true;
503
+ if (timer) {
504
+ clearTimeout(timer);
505
+ }
506
+ },
507
+ };
508
+ }
509
+ flushCloseWaiters() {
510
+ if (this.closeWaiters.size === 0) {
511
+ return;
512
+ }
513
+ for (const waiter of this.closeWaiters) {
514
+ try {
515
+ waiter();
516
+ }
517
+ catch {
518
+ // best effort
519
+ }
520
+ }
521
+ this.closeWaiters.clear();
522
+ }
523
+ buildPrompt(promptText, { useInitialImages = false } = {}) {
524
+ let effectivePrompt = String(promptText || "").trim();
525
+ if (!effectivePrompt) {
526
+ return "";
527
+ }
528
+ if (this.pendingHistorySeed) {
529
+ const historyText = this.history
530
+ .map((item) => {
531
+ const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
532
+ return `${role}: ${String(item?.content || "").trim()}`;
533
+ })
534
+ .filter(Boolean)
535
+ .join("\n\n");
536
+ if (historyText) {
537
+ effectivePrompt = [
538
+ "Continue the existing conversation with this history.",
539
+ "",
540
+ historyText,
541
+ "",
542
+ `User: ${effectivePrompt}`,
543
+ ].join("\n");
544
+ }
545
+ this.pendingHistorySeed = false;
546
+ }
547
+ const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
548
+ if (useInitialImages && images.length > 0) {
549
+ const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
550
+ effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
551
+ }
552
+ return effectivePrompt;
553
+ }
554
+ resolveLaunchSpec() {
555
+ if (typeof this.options.dshRuntimeCommand === "string" && this.options.dshRuntimeCommand.trim()) {
556
+ return {
557
+ command: this.options.dshRuntimeCommand.trim(),
558
+ args: Array.isArray(this.options.dshRuntimeArgs)
559
+ ? this.options.dshRuntimeArgs.map((item) => String(item))
560
+ : [],
561
+ };
562
+ }
563
+ let runtimeBin;
564
+ try {
565
+ runtimeBin = require.resolve("@deepseek-ai/dsh-sdk-jsonrpc-demo/bin");
566
+ }
567
+ catch (error) {
568
+ throw createTurnError("dsh runtime is unavailable: @deepseek-ai/dsh-sdk-jsonrpc-demo is not installed"
569
+ + ` (${error?.message || error})`, { reason: "runtime_unavailable" });
570
+ }
571
+ return {
572
+ command: process.execPath,
573
+ args: [runtimeBin, CORDIS_CONFIG_PATH],
574
+ };
575
+ }
576
+ buildRuntimeEnv() {
577
+ const env = {
578
+ ...process.env,
579
+ ...this.env,
580
+ DSH_CWD: this.cwd,
581
+ DSH_SESSION_ROOT: this.sessionRoot,
582
+ };
583
+ const systemPrompt = typeof this.options.systemPrompt === "string" && this.options.systemPrompt.trim()
584
+ ? this.options.systemPrompt.trim()
585
+ : "";
586
+ if (systemPrompt) {
587
+ env.DSH_SYSTEM_PROMPT = systemPrompt;
588
+ }
589
+ return env;
590
+ }
591
+ async getSdkModule() {
592
+ if (this.sdkModulePromise) {
593
+ return this.sdkModulePromise;
594
+ }
595
+ if (this.options.sdkModule && typeof this.options.sdkModule === "object") {
596
+ this.sdkModulePromise = Promise.resolve(this.options.sdkModule);
597
+ return this.sdkModulePromise;
598
+ }
599
+ this.sdkModulePromise = import("@deepseek-ai/dsh-sdk-client");
600
+ return this.sdkModulePromise;
601
+ }
602
+ async getHarness() {
603
+ if (this.harness) {
604
+ return this.harness;
605
+ }
606
+ const sdkModule = await this.getSdkModule();
607
+ if (!sdkModule || typeof sdkModule.DeepSeekHarness !== "function") {
608
+ throw new Error("dsh SDK client is unavailable");
609
+ }
610
+ const launch = this.resolveLaunchSpec();
611
+ this.trace(`launching runtime: ${launch.command} ${launch.args.join(" ")}`);
612
+ this.harness = new sdkModule.DeepSeekHarness({
613
+ launch: {
614
+ command: launch.command,
615
+ args: launch.args,
616
+ cwd: this.cwd,
617
+ env: this.buildRuntimeEnv(),
618
+ },
619
+ cwd: this.cwd,
620
+ provider: this.dshProvider,
621
+ model: this.model,
622
+ ...(this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens }),
623
+ });
624
+ return this.harness;
625
+ }
626
+ async disposeHarness() {
627
+ const harness = this.harness;
628
+ this.harness = null;
629
+ if (!harness) {
630
+ return;
631
+ }
632
+ try {
633
+ await harness.close();
634
+ }
635
+ catch {
636
+ // best effort
637
+ }
638
+ }
639
+ maybeEmitAuthRequired(message) {
640
+ const normalized = String(message || "").toLowerCase();
641
+ if (!normalized) {
642
+ return;
643
+ }
644
+ if (!/(api key|credential|unauthorized|401|auth)/.test(normalized)) {
645
+ return;
646
+ }
647
+ this.emit("auth_required", {
648
+ reason: "login_required",
649
+ message: String(message),
650
+ });
651
+ }
652
+ async handleNotification(notification, currentTurn, { onProgress }) {
653
+ this.touchTurnActivity();
654
+ // Token-level replay chunks are high-volume and carry nothing the bridge
655
+ // consumes; keep `items` at message/tool granularity so the result stays
656
+ // small crossing the worker IPC boundary.
657
+ if (notification?.params?.event?.type !== "assistant/chunk") {
658
+ currentTurn.items.push(notification);
659
+ }
660
+ if (notification?.method === "session.status") {
661
+ if (notification.params?.sessionId !== this.sessionId) {
662
+ return;
663
+ }
664
+ if (notification.params?.status === "running") {
665
+ await this.emitWorkingStatus({
666
+ phase: "turn_started",
667
+ reply_in_progress: true,
668
+ status_line: "dsh is working",
669
+ }, onProgress);
670
+ }
671
+ return;
672
+ }
673
+ if (notification?.method === "subagent.started") {
674
+ await this.emitWorkingStatus({
675
+ phase: "task_progress",
676
+ reply_in_progress: true,
677
+ status_line: "dsh delegating to subagent",
678
+ }, onProgress);
679
+ return;
680
+ }
681
+ if (notification?.method === "subagent.finished") {
682
+ await this.emitWorkingStatus({
683
+ phase: "task_progress",
684
+ reply_in_progress: true,
685
+ status_line: `dsh subagent ${notification.params?.status === "ok" ? "finished" : "failed"}`,
686
+ }, onProgress);
687
+ return;
688
+ }
689
+ if (notification?.method !== "session.event" || notification.params?.sessionId !== this.sessionId) {
690
+ return;
691
+ }
692
+ const event = notification.params?.event;
693
+ switch (event?.type) {
694
+ case "assistant/message": {
695
+ const usage = event.data?.usage;
696
+ if (usage) {
697
+ currentTurn.usage = accumulateUsage(currentTurn.usage, usage);
698
+ this.lastUsage = accumulateUsage(this.lastUsage, usage);
699
+ }
700
+ const text = extractTextBlocks(event.data?.message?.content);
701
+ if (!text) {
702
+ return;
703
+ }
704
+ currentTurn.emittedAssistantMessage = true;
705
+ currentTurn.fullText = text;
706
+ await this.emitWorkingStatus({
707
+ phase: "message_aggregation",
708
+ reply_in_progress: true,
709
+ status_line: statusLineForPhase("message_aggregation"),
710
+ reply_preview: sanitizeForLog(text, 120),
711
+ }, onProgress);
712
+ await this.emitAssistantMessage(text);
713
+ return;
714
+ }
715
+ case "tool/call": {
716
+ const toolName = String(event.data?.name || "");
717
+ const phase = toolPhaseForName(toolName);
718
+ const argsPreview = sanitizeForLog(event.data?.arguments, 120);
719
+ await this.emitWorkingStatus({
720
+ phase,
721
+ reply_in_progress: true,
722
+ status_line: statusLineForPhase(phase, toolName) + (argsPreview ? `: ${argsPreview}` : ""),
723
+ }, onProgress);
724
+ return;
725
+ }
726
+ case "todo/write": {
727
+ const todos = Array.isArray(event.data?.todos) ? event.data.todos : [];
728
+ const done = todos.filter((item) => item?.status === "completed").length;
729
+ await this.emitWorkingStatus({
730
+ phase: "task_progress",
731
+ reply_in_progress: true,
732
+ status_line: `dsh todo ${done}/${todos.length}`,
733
+ }, onProgress);
734
+ return;
735
+ }
736
+ case "compaction/start": {
737
+ await this.emitWorkingStatus({
738
+ phase: "context_compaction",
739
+ reply_in_progress: true,
740
+ status_line: statusLineForPhase("context_compaction"),
741
+ }, onProgress);
742
+ return;
743
+ }
744
+ case "turn/end": {
745
+ currentTurn.turnEndReason = event.data?.reason || null;
746
+ return;
747
+ }
748
+ default:
749
+ return;
750
+ }
751
+ }
752
+ async interruptCurrentTurn() {
753
+ const currentTurn = this.currentTurn;
754
+ if (!currentTurn) {
755
+ return false;
756
+ }
757
+ // The dsh SDK wire has no mid-turn cancel: tear the runtime down and let
758
+ // the next turn respawn it, resuming the same persisted session id. The
759
+ // signal also settles a turn that has not attached its runtime yet.
760
+ currentTurn.interrupted = true;
761
+ currentTurn.signalInterrupt?.();
762
+ await this.disposeHarness();
763
+ return true;
764
+ }
765
+ async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
766
+ if (this.closeRequested) {
767
+ throw this.createSessionClosedError();
768
+ }
769
+ if (!String(promptText || "").trim()) {
770
+ return {
771
+ text: "",
772
+ usage: null,
773
+ items: [],
774
+ events: [],
775
+ };
776
+ }
777
+ if (this.currentTurn) {
778
+ throw createTurnError("dsh SDK turn already running", {
779
+ reason: "turn_already_running",
780
+ });
781
+ }
782
+ if (this.pendingResumeFromSessionId) {
783
+ const restored = this.loadPersistedHistory(this.pendingResumeFromSessionId);
784
+ this.trace(`restored ${restored.length} messages from resumed session ${this.pendingResumeFromSessionId}`);
785
+ this.pendingResumeFromSessionId = "";
786
+ if (restored.length > 0) {
787
+ this.history = [...restored, ...this.history];
788
+ this.pendingHistorySeed = true;
789
+ }
790
+ }
791
+ // A fresh runtime cannot prompt an id whose log is already on disk;
792
+ // continue on a rotated id seeded with the in-memory history.
793
+ if (!this.harness && this.sessionIdUsed) {
794
+ this.rotateSessionId();
795
+ }
796
+ const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
797
+ this.history.push({ role: "user", content: promptText });
798
+ const currentTurn = {
799
+ emittedAssistantMessage: false,
800
+ fullText: "",
801
+ interrupted: false,
802
+ items: [],
803
+ terminalWorkingStatusEmitted: false,
804
+ turnEndReason: null,
805
+ usage: null,
806
+ };
807
+ this.currentTurn = currentTurn;
808
+ this.updateCurrentTurnStatus({
809
+ source: DSH_SDK_VARIANT,
810
+ reply_in_progress: true,
811
+ replyTo: this.getCurrentReplyTarget(),
812
+ phase: "turn_started",
813
+ status_line: "dsh is working",
814
+ thread_id: this.sessionId,
815
+ });
816
+ const closeGuard = this.createCloseGuard(() => {
817
+ void this.disposeHarness();
818
+ });
819
+ const turnTimeoutGuard = this.createTurnTimeoutGuard(() => {
820
+ currentTurn.interrupted = true;
821
+ void this.disposeHarness();
822
+ });
823
+ // Settles the turn on interrupt even before the runtime is attached (an
824
+ // interrupt during getHarness() would otherwise leave the run dangling).
825
+ const interruptGuard = new Promise((_, reject) => {
826
+ currentTurn.signalInterrupt = () => reject(createTurnError("dsh turn interrupted", { reason: "turn_interrupted" }));
827
+ });
828
+ // The guard may reject before the race below attaches its handler; keep
829
+ // that early rejection observed so it never surfaces as unhandled.
830
+ interruptGuard.catch(() => { });
831
+ try {
832
+ await this.emitWorkingStatus({
833
+ phase: "turn_started",
834
+ reply_in_progress: true,
835
+ status_line: "dsh is working",
836
+ }, onProgress);
837
+ if (currentTurn.interrupted) {
838
+ throw createTurnError("dsh turn interrupted", { reason: "turn_interrupted" });
839
+ }
840
+ const harness = await this.getHarness();
841
+ // Serialize notification handling so event translation keeps wire order
842
+ // and the turn only settles after the last notification is processed.
843
+ let notificationQueue = Promise.resolve();
844
+ this.sessionIdUsed = true;
845
+ const runResult = await Promise.race([
846
+ harness.session(this.sessionId).run(effectivePrompt, {
847
+ onNotification: (notification) => {
848
+ notificationQueue = notificationQueue.then(() => this.handleNotification(notification, currentTurn, { onProgress })).catch(() => { });
849
+ },
850
+ }),
851
+ closeGuard.promise,
852
+ turnTimeoutGuard.promise,
853
+ interruptGuard,
854
+ ]);
855
+ await notificationQueue;
856
+ this.announceSession();
857
+ const responseText = typeof runResult?.finalResponse === "string" && runResult.finalResponse
858
+ ? runResult.finalResponse
859
+ : currentTurn.fullText;
860
+ if (!currentTurn.emittedAssistantMessage && responseText) {
861
+ await this.emitAssistantMessage(responseText);
862
+ }
863
+ if (responseText) {
864
+ this.history.push({ role: "assistant", content: responseText });
865
+ }
866
+ const turnEndKind = currentTurn.turnEndReason?.kind;
867
+ if (turnEndKind && turnEndKind !== "completed" && turnEndKind !== "max-tokens") {
868
+ const detail = typeof currentTurn.turnEndReason?.error?.message === "string" &&
869
+ currentTurn.turnEndReason.error.message
870
+ ? `: ${currentTurn.turnEndReason.error.message}`
871
+ : "";
872
+ const errorMessage = `dsh turn ended with reason "${turnEndKind}"${detail}`;
873
+ this.maybeEmitAuthRequired(errorMessage);
874
+ await this.emitTerminalWorkingStatus(currentTurn, {
875
+ phase: "turn_failed",
876
+ status_done_line: errorMessage,
877
+ }, onProgress);
878
+ throw createTurnError(errorMessage, {
879
+ reason: "turn_failed",
880
+ turnStatus: turnEndKind,
881
+ });
882
+ }
883
+ this.activeReplyTarget = "";
884
+ await this.emitTerminalWorkingStatus(currentTurn, {
885
+ phase: "turn_completed",
886
+ status_done_line: "dsh finished",
887
+ }, onProgress);
888
+ return {
889
+ text: responseText,
890
+ usage: currentTurn.usage ? { ...currentTurn.usage } : null,
891
+ items: currentTurn.items,
892
+ events: [],
893
+ provider: this.backend,
894
+ metadata: {
895
+ source: DSH_SDK_VARIANT,
896
+ sessionId: this.sessionId,
897
+ model: this.model,
898
+ modelProvider: this.dshProvider,
899
+ turnEndReason: currentTurn.turnEndReason || undefined,
900
+ },
901
+ };
902
+ }
903
+ catch (error) {
904
+ if (error?.reason === "turn_timeout") {
905
+ // Runtime already torn down by the timeout guard.
906
+ this.trace(`turn timed out after ${this.turnDeadlineMs}ms`);
907
+ }
908
+ if (currentTurn.interrupted && error?.reason !== "turn_timeout" && error?.reason !== "session_closed") {
909
+ const interruptError = createTurnError("dsh turn interrupted", {
910
+ reason: "turn_interrupted",
911
+ });
912
+ await this.emitTerminalWorkingStatus(currentTurn, {
913
+ phase: "turn_failed",
914
+ status_done_line: "dsh turn interrupted",
915
+ }, onProgress);
916
+ throw interruptError;
917
+ }
918
+ if (this.closeRequested && error?.reason !== "session_closed") {
919
+ throw this.createSessionClosedError();
920
+ }
921
+ if (!this.closeRequested && error?.reason !== "session_closed") {
922
+ const errorMessage = error instanceof Error ? error.message : String(error);
923
+ await this.emitTerminalWorkingStatus(currentTurn, {
924
+ phase: "turn_failed",
925
+ status_done_line: errorMessage || "dsh turn failed",
926
+ }, onProgress);
927
+ }
928
+ throw error;
929
+ }
930
+ finally {
931
+ this.activeReplyTarget = "";
932
+ if (this.currentTurn === currentTurn) {
933
+ this.currentTurn = null;
934
+ }
935
+ closeGuard.cleanup();
936
+ turnTimeoutGuard.cleanup();
937
+ }
938
+ }
939
+ async close() {
940
+ if (this.closed) {
941
+ return;
942
+ }
943
+ this.closeRequested = true;
944
+ this.flushCloseWaiters();
945
+ if (this.currentTurn) {
946
+ this.currentTurn.interrupted = true;
947
+ }
948
+ await this.disposeHarness();
949
+ this.closed = true;
950
+ }
951
+ }