@love-moon/ai-sdk 0.2.30 → 0.2.32

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.
@@ -154,6 +154,9 @@ export class OpencodeSdkSession extends EventEmitter {
154
154
  this.currentTurn = null;
155
155
  this.lastUsage = null;
156
156
  this.lastAssistantInfo = null;
157
+ this.currentTurnStatus = null;
158
+ this.currentTurnActivityAt = 0;
159
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
157
160
  this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
158
161
  this.client = null;
159
162
  this.sdkModulePromise = null;
@@ -200,10 +203,14 @@ export class OpencodeSdkSession extends EventEmitter {
200
203
  return this.sessionId;
201
204
  }
202
205
  get threadOptions() {
203
- const model = typeof this.options.model === "string" && this.options.model.trim()
204
- ? this.options.model.trim()
205
- : this.backend;
206
- return { model };
206
+ const model = this.sessionInfo?.model ||
207
+ (typeof this.options.model === "string" && this.options.model.trim()
208
+ ? this.options.model.trim()
209
+ : this.backend);
210
+ return {
211
+ model,
212
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
213
+ };
207
214
  }
208
215
  getSnapshot() {
209
216
  return {
@@ -215,12 +222,16 @@ export class OpencodeSdkSession extends EventEmitter {
215
222
  useSessionFileReplyStream: this.usesSessionFileReplyStream(),
216
223
  resumeReady: Boolean(this.sessionId),
217
224
  manualResume: null,
225
+ currentTurnStatus: this.getCurrentTurnStatus(),
218
226
  pid: this.transport.pid || undefined,
219
227
  };
220
228
  }
221
229
  getSessionInfo() {
222
230
  return this.sessionInfo ? { ...this.sessionInfo } : null;
223
231
  }
232
+ getCurrentTurnStatus() {
233
+ return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
234
+ }
224
235
  async ensureSessionInfo() {
225
236
  await this.boot();
226
237
  return this.getSessionInfo();
@@ -256,6 +267,39 @@ export class OpencodeSdkSession extends EventEmitter {
256
267
  getCurrentReplyTarget() {
257
268
  return this.activeReplyTarget || this.lastReplyTarget || undefined;
258
269
  }
270
+ touchTurnActivity() {
271
+ this.currentTurnActivityAt = this.now();
272
+ }
273
+ updateCurrentTurnStatus(payload) {
274
+ const updatedAtMs = this.now();
275
+ this.currentTurnActivityAt = updatedAtMs;
276
+ this.currentTurnStatus = {
277
+ ...payload,
278
+ updated_at: new Date(updatedAtMs).toISOString(),
279
+ };
280
+ }
281
+ markTurnStartedStatus() {
282
+ this.updateCurrentTurnStatus({
283
+ source: OPENCODE_PROVIDER_VARIANT,
284
+ reply_in_progress: true,
285
+ replyTo: this.getCurrentReplyTarget(),
286
+ phase: "turn_started",
287
+ status_line: "opencode is working",
288
+ thread_id: this.sessionId || undefined,
289
+ session_id: this.sessionId || undefined,
290
+ session_file_path: undefined,
291
+ });
292
+ }
293
+ async failPendingTurnStart(error, onProgress = null) {
294
+ if (this.closeRequested || error?.reason === "session_closed") {
295
+ return;
296
+ }
297
+ await this.emitWorkingStatus({
298
+ phase: "turn_failed",
299
+ reply_in_progress: false,
300
+ status_done_line: error instanceof Error ? error.message : String(error),
301
+ }, onProgress);
302
+ }
259
303
  async emitWorkingStatus(payload, onProgress = null) {
260
304
  const normalized = {
261
305
  source: OPENCODE_PROVIDER_VARIANT,
@@ -270,15 +314,18 @@ export class OpencodeSdkSession extends EventEmitter {
270
314
  session_id: this.sessionId || undefined,
271
315
  session_file_path: undefined,
272
316
  };
317
+ this.updateCurrentTurnStatus(normalized);
318
+ const snapshot = this.getCurrentTurnStatus();
273
319
  if (typeof onProgress === "function") {
274
- onProgress(normalized);
320
+ onProgress(snapshot);
275
321
  }
276
322
  if (typeof this.workingStatusHandler === "function") {
277
- await this.workingStatusHandler(normalized);
323
+ await this.workingStatusHandler(snapshot);
278
324
  }
279
- this.emit("working_status", normalized);
325
+ this.emit("working_status", snapshot);
280
326
  }
281
327
  async emitAssistantMessage(text) {
328
+ this.touchTurnActivity();
282
329
  const payload = {
283
330
  text,
284
331
  preserveWhitespace: true,
@@ -352,8 +399,27 @@ export class OpencodeSdkSession extends EventEmitter {
352
399
  };
353
400
  }
354
401
  let timer = null;
355
- const promise = new Promise((_, reject) => {
402
+ let settled = false;
403
+ const schedule = (reject) => {
404
+ const now = this.now();
405
+ const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
406
+ ? this.currentTurnActivityAt
407
+ : now;
408
+ const elapsedMs = Math.max(0, now - lastActivityAt);
409
+ const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
356
410
  timer = setTimeout(() => {
411
+ if (settled) {
412
+ return;
413
+ }
414
+ const activityNow = this.now();
415
+ const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
416
+ ? this.currentTurnActivityAt
417
+ : activityNow;
418
+ if (activityNow - latestActivityAt < this.turnDeadlineMs) {
419
+ schedule(reject);
420
+ return;
421
+ }
422
+ settled = true;
357
423
  try {
358
424
  onTimeout?.();
359
425
  }
@@ -361,14 +427,18 @@ export class OpencodeSdkSession extends EventEmitter {
361
427
  // best effort
362
428
  }
363
429
  reject(this.createTurnTimeoutError(this.turnDeadlineMs));
364
- }, this.turnDeadlineMs);
365
- if (typeof timer.unref === "function") {
430
+ }, waitMs);
431
+ if (typeof timer?.unref === "function") {
366
432
  timer.unref();
367
433
  }
434
+ };
435
+ const promise = new Promise((_, reject) => {
436
+ schedule(reject);
368
437
  });
369
438
  return {
370
439
  promise,
371
440
  cleanup: () => {
441
+ settled = true;
372
442
  if (timer) {
373
443
  clearTimeout(timer);
374
444
  }
@@ -536,9 +606,20 @@ export class OpencodeSdkSession extends EventEmitter {
536
606
  }
537
607
  const changed = this.sessionId !== normalizedSessionId;
538
608
  this.sessionId = normalizedSessionId;
609
+ const resolvedModel = typeof this.lastAssistantInfo?.model?.modelID === "string" && this.lastAssistantInfo.model.modelID.trim()
610
+ ? this.lastAssistantInfo.model.modelID.trim()
611
+ : typeof this.options.model === "string" && this.options.model.trim()
612
+ ? this.options.model.trim()
613
+ : undefined;
614
+ const resolvedModelProvider = typeof this.lastAssistantInfo?.model?.providerID === "string" && this.lastAssistantInfo.model.providerID.trim()
615
+ ? this.lastAssistantInfo.model.providerID.trim()
616
+ : undefined;
539
617
  this.sessionInfo = {
618
+ ...(this.sessionInfo || {}),
540
619
  backend: this.backend,
541
620
  sessionId: normalizedSessionId,
621
+ model: resolvedModel,
622
+ modelProvider: resolvedModelProvider,
542
623
  };
543
624
  if (changed) {
544
625
  this.trace(`session ready id=${normalizedSessionId}`);
@@ -874,6 +955,9 @@ export class OpencodeSdkSession extends EventEmitter {
874
955
  this.activeReplyTarget = "";
875
956
  this.lastUsage = this.buildUsageFromAssistantInfo(currentTurn.lastAssistantInfo);
876
957
  this.lastAssistantInfo = currentTurn.lastAssistantInfo || null;
958
+ this.applySessionInfo({
959
+ id: this.sessionId,
960
+ });
877
961
  await this.emitTerminalWorkingStatus(currentTurn, {
878
962
  phase: "turn_completed",
879
963
  status_done_line: "opencode finished",
@@ -1058,14 +1142,21 @@ export class OpencodeSdkSession extends EventEmitter {
1058
1142
  if (!effectivePrompt) {
1059
1143
  return buildEmptyTurnResult();
1060
1144
  }
1061
- await this.boot();
1062
1145
  if (this.currentTurn) {
1063
1146
  throw createTurnError("Opencode turn already running", {
1064
1147
  reason: "turn_already_running",
1065
1148
  });
1066
1149
  }
1067
- if (!this.client?.session || typeof this.client.session.promptAsync !== "function") {
1068
- throw new Error("Opencode session client is unavailable");
1150
+ this.markTurnStartedStatus();
1151
+ try {
1152
+ await this.boot();
1153
+ if (!this.client?.session || typeof this.client.session.promptAsync !== "function") {
1154
+ throw new Error("Opencode session client is unavailable");
1155
+ }
1156
+ }
1157
+ catch (error) {
1158
+ await this.failPendingTurnStart(error, onProgress);
1159
+ throw error;
1069
1160
  }
1070
1161
  this.history.push({ role: "user", content: promptText });
1071
1162
  const abortController = new AbortController();
@@ -1,8 +1,8 @@
1
- export function normalizeBackend(backend: any): string;
2
- export function isSupportedBackend(backend: any): boolean;
3
- export function providerVariantForBackend(backend: any): "codex-app-server" | "claude-agent-sdk" | "kimi-cli-wire" | "opencode-sdk";
4
- export function assertSupportedBackend(backend: any): "codex" | "claude" | "kimi" | "opencode";
5
- export function createLocalAiSession(backend: any, options?: {}): CodexAppServerSession | ClaudeAgentSdkSession | KimiCliSession | OpencodeSdkSession;
1
+ export function normalizeBackend(backend: any, options?: {}): Promise<any>;
2
+ export function isSupportedBackend(backend: any, options?: {}): Promise<boolean>;
3
+ export function providerVariantForBackend(backend: any, options?: {}): Promise<any>;
4
+ export function assertSupportedBackend(backend: any, options?: {}): Promise<any>;
5
+ export function createLocalAiSession(backend: any, options?: {}): Promise<any>;
6
6
  export const DEFAULT_PROVIDER_VARIANT: "codex-app-server";
7
7
  export const CLAUDE_PROVIDER_VARIANT: "claude-agent-sdk";
8
8
  export const KIMI_PROVIDER_VARIANT: "kimi-cli-wire";
@@ -2,11 +2,12 @@ import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
2
2
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
3
3
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
4
4
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
5
+ import { getExternalProviderDescriptor, resolveExternalBackend, } from "./external-provider-registry.js";
5
6
  export const DEFAULT_PROVIDER_VARIANT = "codex-app-server";
6
7
  export const CLAUDE_PROVIDER_VARIANT = "claude-agent-sdk";
7
8
  export const KIMI_PROVIDER_VARIANT = "kimi-cli-wire";
8
9
  export const OPENCODE_PROVIDER_VARIANT = "opencode-sdk";
9
- export function normalizeBackend(backend) {
10
+ function normalizeBuiltInBackendName(backend) {
10
11
  const normalized = String(backend || "").trim().toLowerCase();
11
12
  if (normalized === "code") {
12
13
  return "codex";
@@ -22,12 +23,23 @@ export function normalizeBackend(backend) {
22
23
  }
23
24
  return normalized;
24
25
  }
25
- export function isSupportedBackend(backend) {
26
- const normalized = normalizeBackend(backend);
27
- return normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode";
26
+ export async function normalizeBackend(backend, options = {}) {
27
+ const normalized = normalizeBuiltInBackendName(backend);
28
+ if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
29
+ return normalized;
30
+ }
31
+ return await resolveExternalBackend(normalized, options);
32
+ }
33
+ export async function isSupportedBackend(backend, options = {}) {
34
+ const normalized = await normalizeBackend(backend, options);
35
+ if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
36
+ return true;
37
+ }
38
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
39
+ return Boolean(descriptor);
28
40
  }
29
- export function providerVariantForBackend(backend) {
30
- const normalized = normalizeBackend(backend);
41
+ export async function providerVariantForBackend(backend, options = {}) {
42
+ const normalized = await normalizeBackend(backend, options);
31
43
  if (normalized === "claude") {
32
44
  return CLAUDE_PROVIDER_VARIANT;
33
45
  }
@@ -37,17 +49,28 @@ export function providerVariantForBackend(backend) {
37
49
  if (normalized === "opencode") {
38
50
  return OPENCODE_PROVIDER_VARIANT;
39
51
  }
52
+ if (normalized === "codex") {
53
+ return DEFAULT_PROVIDER_VARIANT;
54
+ }
55
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
56
+ if (descriptor?.variant) {
57
+ return descriptor.variant;
58
+ }
40
59
  return DEFAULT_PROVIDER_VARIANT;
41
60
  }
42
- export function assertSupportedBackend(backend) {
43
- const normalized = normalizeBackend(backend);
61
+ export async function assertSupportedBackend(backend, options = {}) {
62
+ const normalized = await normalizeBackend(backend, options);
44
63
  if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
45
64
  return normalized;
46
65
  }
47
- throw new Error(`Unsupported AI SDK backend "${backend}". Only codex app-server, claude agent-sdk, kimi cli wire, and opencode sdk are supported.`);
66
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
67
+ if (descriptor) {
68
+ return normalized;
69
+ }
70
+ throw new Error(`Unsupported AI SDK backend "${backend}". Built-in backends are codex app-server, claude agent-sdk, kimi cli wire, and opencode sdk. Set AISDK_PROVIDER_PATH to load external providers.`);
48
71
  }
49
- export function createLocalAiSession(backend, options = {}) {
50
- const normalized = assertSupportedBackend(backend);
72
+ export async function createLocalAiSession(backend, options = {}) {
73
+ const normalized = await assertSupportedBackend(backend, options);
51
74
  if (normalized === "claude") {
52
75
  return new ClaudeAgentSdkSession(normalized, options);
53
76
  }
@@ -57,7 +80,14 @@ export function createLocalAiSession(backend, options = {}) {
57
80
  if (normalized === "opencode") {
58
81
  return new OpencodeSdkSession(normalized, options);
59
82
  }
60
- return new CodexAppServerSession(normalized, options);
83
+ if (normalized === "codex") {
84
+ return new CodexAppServerSession(normalized, options);
85
+ }
86
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
87
+ if (!descriptor) {
88
+ throw new Error(`External AI SDK provider "${normalized}" is unavailable.`);
89
+ }
90
+ return await descriptor.createSession(normalized, options);
61
91
  }
62
92
  export { CodexAppServerSession };
63
93
  export { ClaudeAgentSdkSession };
package/dist/worker.js CHANGED
@@ -33,7 +33,7 @@ async function handleCreate(message) {
33
33
  throw new Error("AI worker session already created");
34
34
  }
35
35
  sessionCreated = true;
36
- session = createLocalAiSession(message.backend, {
36
+ session = await createLocalAiSession(message.backend, {
37
37
  ...(message.options && typeof message.options === "object" ? message.options : {}),
38
38
  logger: {
39
39
  log: (line) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,5 +25,5 @@
25
25
  "@types/node": "^22.10.2",
26
26
  "typescript": "^5.6.3"
27
27
  },
28
- "gitCommitId": "e6a71ad"
28
+ "gitCommitId": "c749d4b"
29
29
  }