@love-moon/ai-sdk 0.8.0 → 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 +6 -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 +1 -1
  26. package/dist/providers/codex-app-server-session.js +14 -12
  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 +6 -1
  34. package/dist/providers/kimi-cli-session.js +13 -3
  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
@@ -83,6 +83,11 @@ export class KimiCliSession extends EventEmitter<[never]> {
83
83
  command: any;
84
84
  } | null;
85
85
  currentTurnStatus: any;
86
+ capabilities: {
87
+ media: Readonly<{
88
+ image: "native";
89
+ }>;
90
+ };
86
91
  pid: any;
87
92
  };
88
93
  getSessionInfo(): {
@@ -149,7 +154,7 @@ export class KimiCliSession extends EventEmitter<[never]> {
149
154
  handleTransportFailure(error: any): void;
150
155
  handleTransportExit(payload: any): void;
151
156
  interruptCurrentTurn(): Promise<boolean>;
152
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
157
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
153
158
  useInitialImages?: boolean | undefined;
154
159
  onProgress?: null | undefined;
155
160
  jsonSchema?: null | undefined;
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { KIMI_CLI_WIRE_VARIANT as KIMI_PROVIDER_VARIANT } from "../built-in-backends.js";
4
+ import { appendContextFilesToPrompt } from "../context-files.js";
5
+ import { PROVIDER_MEDIA_CAPABILITIES, buildKimiContent } from "../media-adapters.js";
6
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
4
7
  import { KimiWireTransport } from "../transports/kimi-wire-transport.js";
5
8
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
6
9
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
@@ -230,6 +233,7 @@ export class KimiCliSession extends EventEmitter {
230
233
  }
231
234
  : null,
232
235
  currentTurnStatus: this.getCurrentTurnStatus(),
236
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
233
237
  pid: this.transport.pid || undefined,
234
238
  };
235
239
  }
@@ -873,11 +877,15 @@ export class KimiCliSession extends EventEmitter {
873
877
  }
874
878
  return false;
875
879
  }
876
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
880
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
877
881
  if (this.closeRequested || this.closed) {
878
882
  throw this.createSessionClosedError();
879
883
  }
880
- let effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
884
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
885
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT]);
886
+ let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
887
+ (media.length ? defaultPromptForMedia(media) : "");
888
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
881
889
  if (jsonSchema && typeof jsonSchema === "object" && effectivePrompt) {
882
890
  effectivePrompt = injectJsonSchemaPrompt(effectivePrompt, jsonSchema);
883
891
  }
@@ -930,7 +938,9 @@ export class KimiCliSession extends EventEmitter {
930
938
  }, onProgress);
931
939
  const promptResult = await Promise.race([
932
940
  this.transport.request("prompt", {
933
- user_input: effectivePrompt,
941
+ user_input: media.length === 0
942
+ ? effectivePrompt
943
+ : buildKimiContent(effectivePrompt, media),
934
944
  }),
935
945
  turnFailurePromise,
936
946
  closeGuard.promise,
@@ -60,6 +60,13 @@ export class KimiPrintSession extends EventEmitter<[never]> {
60
60
  command: string;
61
61
  } | null;
62
62
  currentTurnStatus: any;
63
+ capabilities: {
64
+ media: Readonly<{
65
+ image: "unsupported";
66
+ }> | Readonly<{
67
+ image: "native";
68
+ }>;
69
+ };
63
70
  pid: any;
64
71
  };
65
72
  getSessionInfo(): {
@@ -100,15 +107,15 @@ export class KimiPrintSession extends EventEmitter<[never]> {
100
107
  emitAssistantMessage(text: any): Promise<void>;
101
108
  buildPrompt(promptText: any): string;
102
109
  buildPrintArgs(promptText?: string): string[];
103
- buildUserMessage(promptText: any, { useInitialImages }?: {
104
- useInitialImages?: boolean | undefined;
110
+ buildUserMessage(promptText: any, { media }?: {
111
+ media?: never[] | undefined;
105
112
  }): {
106
113
  role: string;
107
114
  content: any;
108
115
  };
109
116
  maybeEmitAuthRequired(stderrTail: any): void;
110
117
  interruptCurrentTurn(): Promise<boolean>;
111
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
118
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
112
119
  useInitialImages?: boolean | undefined;
113
120
  onProgress?: null | undefined;
114
121
  jsonSchema?: null | undefined;
@@ -5,6 +5,9 @@ import { randomUUID } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import readline from "node:readline";
7
7
  import { KIMI_CLI_PRINT_VARIANT as KIMI_PRINT_PROVIDER_VARIANT } from "../built-in-backends.js";
8
+ import { appendContextFilesToPrompt } from "../context-files.js";
9
+ import { PROVIDER_MEDIA_CAPABILITIES, UNSUPPORTED_MEDIA_CAPABILITIES, buildKimiContent, } from "../media-adapters.js";
10
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
8
11
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
9
12
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
10
13
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -165,31 +168,6 @@ function normalizeTextContent(content) {
165
168
  }
166
169
  return "";
167
170
  }
168
- function guessMimeType(filePath) {
169
- const ext = path.extname(String(filePath || "")).toLowerCase();
170
- switch (ext) {
171
- case ".png":
172
- return "image/png";
173
- case ".jpg":
174
- case ".jpeg":
175
- return "image/jpeg";
176
- case ".gif":
177
- return "image/gif";
178
- case ".webp":
179
- return "image/webp";
180
- case ".bmp":
181
- return "image/bmp";
182
- case ".svg":
183
- return "image/svg+xml";
184
- default:
185
- return "application/octet-stream";
186
- }
187
- }
188
- function filePathToDataUri(filePath) {
189
- const buffer = fs.readFileSync(filePath);
190
- const mimeType = guessMimeType(filePath);
191
- return `data:${mimeType};base64,${buffer.toString("base64")}`;
192
- }
193
171
  export class KimiPrintSession extends EventEmitter {
194
172
  constructor(backend, options = {}) {
195
173
  super();
@@ -291,6 +269,11 @@ export class KimiPrintSession extends EventEmitter {
291
269
  }
292
270
  : null,
293
271
  currentTurnStatus: this.getCurrentTurnStatus(),
272
+ capabilities: {
273
+ media: this.cliMode === "prompt"
274
+ ? UNSUPPORTED_MEDIA_CAPABILITIES
275
+ : PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT],
276
+ },
294
277
  pid: this.currentTurn?.child?.pid || undefined,
295
278
  };
296
279
  }
@@ -444,31 +427,16 @@ export class KimiPrintSession extends EventEmitter {
444
427
  }
445
428
  return args;
446
429
  }
447
- buildUserMessage(promptText, { useInitialImages = false } = {}) {
448
- const images = useInitialImages && Array.isArray(this.options.initialImages)
449
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
450
- : [];
451
- if (images.length === 0) {
430
+ buildUserMessage(promptText, { media = [] } = {}) {
431
+ if (media.length === 0) {
452
432
  return {
453
433
  role: "user",
454
434
  content: promptText,
455
435
  };
456
436
  }
457
- const content = [];
458
- if (promptText) {
459
- content.push({ type: "text", text: promptText });
460
- }
461
- for (const imagePath of images) {
462
- content.push({
463
- type: "image_url",
464
- image_url: {
465
- url: filePathToDataUri(imagePath),
466
- },
467
- });
468
- }
469
437
  return {
470
438
  role: "user",
471
- content,
439
+ content: buildKimiContent(promptText, media),
472
440
  };
473
441
  }
474
442
  maybeEmitAuthRequired(stderrTail) {
@@ -500,7 +468,7 @@ export class KimiPrintSession extends EventEmitter {
500
468
  return false;
501
469
  }
502
470
  }
503
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
471
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
504
472
  if (this.closeRequested || this.closed) {
505
473
  throw this.createSessionClosedError();
506
474
  }
@@ -509,21 +477,23 @@ export class KimiPrintSession extends EventEmitter {
509
477
  reason: "turn_already_running",
510
478
  });
511
479
  }
480
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
481
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT]);
512
482
  let effectivePrompt = this.buildPrompt(promptText);
513
- const imagePaths = useInitialImages && Array.isArray(this.options.initialImages)
514
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
515
- : [];
483
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
516
484
  if (jsonSchema && typeof jsonSchema === "object") {
517
- const promptWithSchema = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached images." : "");
485
+ const promptWithSchema = effectivePrompt || (media.length > 0 ? defaultPromptForMedia(media) : "");
518
486
  if (promptWithSchema) {
519
487
  effectivePrompt = injectJsonSchemaPrompt(promptWithSchema, jsonSchema);
520
488
  }
521
489
  }
522
- if (this.cliMode === "prompt" && imagePaths.length > 0) {
523
- const imageContext = imagePaths.map((item, index) => `${index + 1}. ${item}`).join("\n");
524
- effectivePrompt = `${effectivePrompt || "Analyze the attached images."}\n\nAttached image files:\n${imageContext}`;
490
+ if (this.cliMode === "prompt" && media.length > 0) {
491
+ throw createTurnError("Kimi prompt mode does not support media input", {
492
+ reason: "unsupported_media",
493
+ backend: this.backend,
494
+ });
525
495
  }
526
- if (!effectivePrompt && imagePaths.length === 0) {
496
+ if (!effectivePrompt && media.length === 0) {
527
497
  return buildEmptyTurnResult();
528
498
  }
529
499
  this.announceSession();
@@ -685,7 +655,7 @@ export class KimiPrintSession extends EventEmitter {
685
655
  });
686
656
  });
687
657
  if (this.cliMode === "legacy-print") {
688
- const userMessage = this.buildUserMessage(effectivePrompt, { useInitialImages });
658
+ const userMessage = this.buildUserMessage(effectivePrompt || defaultPromptForMedia(media), { media });
689
659
  child.stdin.write(`${JSON.stringify(userMessage)}\n`);
690
660
  }
691
661
  child.stdin.end();
@@ -78,6 +78,11 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
78
78
  resumeReady: boolean;
79
79
  manualResume: null;
80
80
  currentTurnStatus: any;
81
+ capabilities: {
82
+ media: Readonly<{
83
+ image: "native";
84
+ }>;
85
+ };
81
86
  pid: any;
82
87
  };
83
88
  getSessionInfo(): {
@@ -164,7 +169,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
164
169
  handleTransportFailure(error: any): void;
165
170
  handleTransportExit(payload: any): void;
166
171
  interruptCurrentTurn(): Promise<boolean>;
167
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
172
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
168
173
  useInitialImages?: boolean | undefined;
169
174
  onProgress?: null | undefined;
170
175
  jsonSchema?: null | undefined;
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { OPENCODE_SDK_VARIANT as OPENCODE_PROVIDER_VARIANT } from "../built-in-backends.js";
3
+ import { appendContextFilesToPrompt } from "../context-files.js";
4
+ import { PROVIDER_MEDIA_CAPABILITIES, buildOpencodeParts } from "../media-adapters.js";
5
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
3
6
  import { OpencodeServerTransport } from "../transports/opencode-server-transport.js";
4
7
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
5
8
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
@@ -223,6 +226,7 @@ export class OpencodeSdkSession extends EventEmitter {
223
226
  resumeReady: Boolean(this.sessionId),
224
227
  manualResume: null,
225
228
  currentTurnStatus: this.getCurrentTurnStatus(),
229
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
226
230
  pid: this.transport.pid || undefined,
227
231
  };
228
232
  }
@@ -1136,12 +1140,15 @@ export class OpencodeSdkSession extends EventEmitter {
1136
1140
  }
1137
1141
  return true;
1138
1142
  }
1139
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
1143
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
1140
1144
  if (this.closeRequested) {
1141
1145
  throw this.createSessionClosedError();
1142
1146
  }
1143
- const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
1144
- if (!effectivePrompt) {
1147
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
1148
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT]);
1149
+ const effectivePrompt = appendContextFilesToPrompt(this.buildPrompt(promptText, { useInitialImages: false }) ||
1150
+ (media.length ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
1151
+ if (!effectivePrompt && media.length === 0) {
1145
1152
  return buildEmptyTurnResult();
1146
1153
  }
1147
1154
  if (this.currentTurn) {
@@ -1216,12 +1223,7 @@ export class OpencodeSdkSession extends EventEmitter {
1216
1223
  format: jsonSchema && typeof jsonSchema === "object"
1217
1224
  ? { type: "json_schema", schema: jsonSchema }
1218
1225
  : undefined,
1219
- parts: [
1220
- {
1221
- type: "text",
1222
- text: effectivePrompt,
1223
- },
1224
- ],
1226
+ parts: buildOpencodeParts(effectivePrompt, media),
1225
1227
  }, {
1226
1228
  throwOnError: true,
1227
1229
  responseStyle: "data",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * dsh sessions persist as JSONL logs written by the runtime's
3
+ * `@deepseek-ai/dsh-session-persistence-jsonl` plugin under the session root
4
+ * DshSdkSession sets via DSH_SESSION_ROOT:
5
+ *
6
+ * <root>/<projectKey(cwd)>/<sessionId>/session.jsonl
7
+ *
8
+ * The first line of session.jsonl is a `{"type":"session",...}` header that
9
+ * carries the workspace `cwd`, which is what resume needs back.
10
+ */
11
+ export function buildCliArgs(sessionId: any): string[];
12
+ export function findSessionPath(sessionId: any, options?: {}): Promise<string | null>;
13
+ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
14
+ provider: any;
15
+ sessionId: any;
16
+ sessionPath: null;
17
+ cwd: any;
18
+ debugMetadata: {
19
+ cwdSource: any;
20
+ sessionPath: null;
21
+ };
22
+ }>;
23
+ export const BACKEND: "dsh";
@@ -0,0 +1,122 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { defaultDshSessionRoot } from "../providers/dsh-sdk-session.js";
6
+ import { buildResumeContext, isExistingDirectory, normalizeSessionId, } from "./shared.js";
7
+ export const BACKEND = "dsh";
8
+ /**
9
+ * dsh sessions persist as JSONL logs written by the runtime's
10
+ * `@deepseek-ai/dsh-session-persistence-jsonl` plugin under the session root
11
+ * DshSdkSession sets via DSH_SESSION_ROOT:
12
+ *
13
+ * <root>/<projectKey(cwd)>/<sessionId>/session.jsonl
14
+ *
15
+ * The first line of session.jsonl is a `{"type":"session",...}` header that
16
+ * carries the workspace `cwd`, which is what resume needs back.
17
+ */
18
+ export function buildCliArgs(sessionId) {
19
+ // dsh has no standalone CLI resume command inside conductor (the runtime is
20
+ // relaunched by DshSdkSession through the SDK client), but the resume
21
+ // contract requires a stable arg vector; consumers ignore it for dsh.
22
+ const normalizedSessionId = normalizeSessionId(sessionId);
23
+ if (!normalizedSessionId) {
24
+ return [];
25
+ }
26
+ return [`--resume-session-id=${normalizedSessionId}`];
27
+ }
28
+ function resolveSessionRoot(options = {}) {
29
+ if (typeof options.dshSessionRoot === "string" && options.dshSessionRoot.trim()) {
30
+ return options.dshSessionRoot.trim();
31
+ }
32
+ return defaultDshSessionRoot();
33
+ }
34
+ export async function findSessionPath(sessionId, options = {}) {
35
+ const normalizedSessionId = normalizeSessionId(sessionId);
36
+ if (!normalizedSessionId) {
37
+ return null;
38
+ }
39
+ const root = resolveSessionRoot(options);
40
+ let projectEntries = [];
41
+ try {
42
+ projectEntries = await fsp.readdir(root, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ for (const projectEntry of projectEntries) {
48
+ if (!projectEntry.isDirectory()) {
49
+ continue;
50
+ }
51
+ const sessionDir = path.join(root, projectEntry.name, normalizedSessionId);
52
+ for (const filename of ["session.jsonl", "session.jsonl.zstd"]) {
53
+ const candidate = path.join(sessionDir, filename);
54
+ try {
55
+ const stats = await fsp.stat(candidate);
56
+ if (stats.isFile()) {
57
+ return candidate;
58
+ }
59
+ }
60
+ catch {
61
+ // keep scanning
62
+ }
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ async function readHeaderLine(sessionPath) {
68
+ if (!sessionPath || !sessionPath.endsWith(".jsonl")) {
69
+ return null;
70
+ }
71
+ const rl = readline.createInterface({
72
+ input: fs.createReadStream(sessionPath),
73
+ crlfDelay: Infinity,
74
+ });
75
+ try {
76
+ for await (const line of rl) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) {
79
+ continue;
80
+ }
81
+ try {
82
+ return JSON.parse(trimmed);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ }
89
+ finally {
90
+ rl.close();
91
+ }
92
+ return null;
93
+ }
94
+ export async function resolveResumeContext(sessionId, options = {}) {
95
+ const normalizedSessionId = normalizeSessionId(sessionId);
96
+ if (!normalizedSessionId) {
97
+ throw new Error("--resume requires a session id");
98
+ }
99
+ const sessionPath = await findSessionPath(normalizedSessionId, options);
100
+ if (!sessionPath) {
101
+ throw new Error(`Invalid --resume session id for dsh: ${normalizedSessionId}`);
102
+ }
103
+ const header = await readHeaderLine(sessionPath);
104
+ const cwdFromHeader = header && header.type === "session" && typeof header.cwd === "string" && header.cwd.trim()
105
+ ? header.cwd.trim()
106
+ : "";
107
+ const cwd = cwdFromHeader ||
108
+ (typeof options?.cwd === "string" && options.cwd.trim() ? options.cwd.trim() : "");
109
+ if (!cwd) {
110
+ throw new Error(`Could not resolve workspace for dsh session ${normalizedSessionId}`);
111
+ }
112
+ if (!(await isExistingDirectory(cwd))) {
113
+ throw new Error(`Resume workspace path does not exist: ${cwd}`);
114
+ }
115
+ return buildResumeContext({
116
+ provider: "dsh",
117
+ sessionId: normalizedSessionId,
118
+ sessionPath,
119
+ cwd,
120
+ cwdSource: cwdFromHeader ? "session" : "options_cwd",
121
+ });
122
+ }
@@ -4,6 +4,7 @@ import * as chatWeb from "./chat-web.js";
4
4
  import * as claude from "./claude.js";
5
5
  import * as codex from "./codex.js";
6
6
  import * as copilot from "./copilot.js";
7
+ import * as dsh from "./dsh.js";
7
8
  import * as kimi from "./kimi.js";
8
9
  import * as opencode from "./opencode.js";
9
10
  import { buildResumeContext, isExistingDirectory, normalizeBackend, normalizeProjectPathCandidate, normalizeSessionId, resolveSessionRunDirectory, } from "./shared.js";
@@ -27,6 +28,7 @@ const RESUME_MODULES_BY_BACKEND = new Map([
27
28
  ["kimi", kimi],
28
29
  ["opencode", opencode],
29
30
  ["chat-web", chatWeb],
31
+ ["dsh", dsh],
30
32
  ]);
31
33
  // Sanity-check at module load:
32
34
  // (1) Every registered resume module must point at a real built-in backend.
@@ -11,6 +11,7 @@ export const KIMI_PROVIDER_VARIANT: "kimi-cli-wire";
11
11
  export const KIMI_PRINT_PROVIDER_VARIANT: "kimi-cli-print";
12
12
  export const OPENCODE_PROVIDER_VARIANT: "opencode-sdk";
13
13
  export const CHAT_WEB_PROVIDER_VARIANT: "chat-web-session";
14
+ export const DSH_PROVIDER_VARIANT: "dsh-sdk";
14
15
  import { BUILT_IN_BACKENDS } from "./built-in-backends.js";
15
16
  import { ChatWebSession } from "./providers/chat-web-session.js";
16
17
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
@@ -20,4 +21,5 @@ import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
20
21
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
21
22
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
22
23
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
23
- export { BUILT_IN_BACKENDS, ChatWebSession, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession };
24
+ import { DshSdkSession } from "./providers/dsh-sdk-session.js";
25
+ export { BUILT_IN_BACKENDS, ChatWebSession, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession, DshSdkSession };
@@ -1,9 +1,10 @@
1
- import { BUILT_IN_BACKENDS, CHAT_WEB_SESSION_VARIANT, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
1
+ import { BUILT_IN_BACKENDS, CHAT_WEB_SESSION_VARIANT, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, DSH_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
2
2
  import { ChatWebSession } from "./providers/chat-web-session.js";
3
3
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
4
4
  import { CodexExecSession } from "./providers/codex-exec-session.js";
5
5
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
6
6
  import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
7
+ import { DshSdkSession } from "./providers/dsh-sdk-session.js";
7
8
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
8
9
  import { resolveKimiCliMode } from "./providers/kimi-cli-mode.js";
9
10
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
@@ -17,6 +18,7 @@ export const KIMI_PROVIDER_VARIANT = KIMI_CLI_WIRE_VARIANT;
17
18
  export const KIMI_PRINT_PROVIDER_VARIANT = KIMI_CLI_PRINT_VARIANT;
18
19
  export const OPENCODE_PROVIDER_VARIANT = OPENCODE_SDK_VARIANT;
19
20
  export const CHAT_WEB_PROVIDER_VARIANT = CHAT_WEB_SESSION_VARIANT;
21
+ export const DSH_PROVIDER_VARIANT = DSH_SDK_VARIANT;
20
22
  const SESSION_FACTORIES_BY_BACKEND = new Map([
21
23
  [
22
24
  "codex",
@@ -47,6 +49,7 @@ const SESSION_FACTORIES_BY_BACKEND = new Map([
47
49
  ],
48
50
  ["opencode", (backend, options) => new OpencodeSdkSession(backend, options)],
49
51
  ["chat-web", (backend, options) => new ChatWebSession(backend, options)],
52
+ ["dsh", (backend, options) => new DshSdkSession(backend, options)],
50
53
  ]);
51
54
  function hasStructuredOutputPreference(options = {}) {
52
55
  if (!options || typeof options !== "object") {
@@ -131,3 +134,4 @@ export { CopilotSdkSession };
131
134
  export { KimiCliSession };
132
135
  export { KimiPrintSession };
133
136
  export { OpencodeSdkSession };
137
+ export { DshSdkSession };
package/dsh/cordis.yml ADDED
@@ -0,0 +1,100 @@
1
+ # Conductor-owned DeepSeek Harness runtime composition for the ai-sdk `dsh`
2
+ # backend. Launched by src/providers/dsh-sdk-session.js through
3
+ # @deepseek-ai/dsh-sdk-client (stdio JSON-RPC); stdout is reserved for
4
+ # JSON-RPC, so no console logger or terminal UI may be mounted here.
5
+ #
6
+ # Modeled on deepseek-harness's examples/jsonrpc-agent/cordis.yml (the
7
+ # upstream unattended coding-agent reference) and the Python SDK's bundled
8
+ # zero-config composition. Plugin versions are pinned in package.json; bare
9
+ # plugin names resolve from this file's directory up into the ai-sdk
10
+ # node_modules tree.
11
+ #
12
+ # Environment contract (set by DshSdkSession when spawning the runtime):
13
+ # DEEPSEEK_API_KEY required; resolved by the llm-deepseek adapter
14
+ # DEEPSEEK_BASE_URL optional API endpoint override
15
+ # DSH_CWD workspace directory for bash/fs tools
16
+ # DSH_SESSION_ROOT JSONL session persistence root
17
+ # DSH_SYSTEM_PROMPT optional persona override
18
+
19
+ # Stdio JSON-RPC server entry; without it the agent has no SDK client.
20
+ - id: sdk-jsonrpc-server
21
+ name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
22
+
23
+ # Stock DeepSeek adapter. Resolves DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL from
24
+ # the launching environment; nothing secret is inlined here.
25
+ - id: llm-deepseek
26
+ name: '@deepseek-ai/dsh-llm-deepseek'
27
+
28
+ # Managed child-process groups for the bash executor.
29
+ - id: subprocess
30
+ name: '@deepseek-ai/dsh-subprocess-local'
31
+
32
+ - id: bash
33
+ name: '@deepseek-ai/dsh-bash-local'
34
+ config:
35
+ cwd: !!js process.env.DSH_CWD ?? process.cwd()
36
+ timeoutMs: 60000
37
+
38
+ # Agent spine: agent loop, system prompt, core coding tools.
39
+ - id: agent-spine
40
+ name: '@deepseek-ai/dsh-agent-spine-demo'
41
+ config:
42
+ persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a coding agent.'
43
+ workspaceContext:
44
+ maxBytes: 65536
45
+
46
+ # JSONL persistence. Compression stays off so the resume dispatcher
47
+ # (src/resume/dsh.js) can read the header line back for cwd recovery.
48
+ - id: sessions
49
+ name: '@deepseek-ai/dsh-session-persistence-jsonl'
50
+ config:
51
+ root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
52
+ compression: none
53
+
54
+ - id: session-checkpoints
55
+ name: '@deepseek-ai/dsh-session-checkpoint-policy'
56
+
57
+ # Subagent delegation (in-process spawn provider).
58
+ - id: subagent
59
+ name: '@deepseek-ai/dsh-subagent'
60
+
61
+ - id: subagent-spawn-in-process
62
+ name: '@deepseek-ai/dsh-subagent-spawn-in-process'
63
+ config:
64
+ providerName: spawn
65
+
66
+ - id: tool-subagent
67
+ name: '@deepseek-ai/dsh-tool-subagent'
68
+ config:
69
+ provider: spawn
70
+ toolName: subagent
71
+ enableRunInBackground: false
72
+
73
+ - id: tool-todo
74
+ name: '@deepseek-ai/dsh-tool-todo'
75
+ config:
76
+ allowParallelInProgress: true
77
+
78
+ # Filesystem tools.
79
+ - id: fs-local
80
+ name: '@deepseek-ai/dsh-fs-local'
81
+ config:
82
+ cwd: !!js process.env.DSH_CWD ?? process.cwd()
83
+
84
+ - id: fs-observation-policy
85
+ name: '@deepseek-ai/dsh-fs-observation-policy'
86
+
87
+ - id: tool-fs
88
+ name: '@deepseek-ai/dsh-tool-fs'
89
+
90
+ # Token accounting + context compaction.
91
+ - id: token-meter
92
+ name: '@deepseek-ai/dsh-token-meter'
93
+
94
+ - id: compaction-basic
95
+ name: '@deepseek-ai/dsh-compaction-basic'
96
+ config:
97
+ thresholdRatio: 0.8
98
+ retainRatio: 0.16
99
+ maxTokens: 8192
100
+ compactionRetries: 1