@love-moon/ai-sdk 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.13.1
4
+
5
+ ## 0.13.0
6
+
3
7
  ## 0.12.0
4
8
 
5
9
  ## 0.11.2
package/dist/client.d.ts CHANGED
@@ -56,6 +56,11 @@ export class RemoteAiSession extends EventEmitter<[never]> {
56
56
  usesSessionFileReplyStream(): boolean;
57
57
  getSessionInfo(): any;
58
58
  getCurrentTurnStatus(): any;
59
+ /**
60
+ * Ask the worker for the live turn status (including `active_tool`). Uses
61
+ * the control lane so it answers while `runTurn` holds the request queue.
62
+ */
63
+ fetchCurrentTurnStatus(): Promise<any>;
59
64
  setSessionMessageHandler(handler: any): void;
60
65
  setWorkingStatusHandler(handler: any): void;
61
66
  setSessionReplyTarget(replyTo: any): void;
package/dist/client.js CHANGED
@@ -155,6 +155,17 @@ export class RemoteAiSession extends EventEmitter {
155
155
  getCurrentTurnStatus() {
156
156
  return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
157
157
  }
158
+ /**
159
+ * Ask the worker for the live turn status (including `active_tool`). Uses
160
+ * the control lane so it answers while `runTurn` holds the request queue.
161
+ */
162
+ async fetchCurrentTurnStatus() {
163
+ const status = await this.callWorker("getCurrentTurnStatus", [], { messageType: "control" });
164
+ if (status && typeof status === "object") {
165
+ this.currentTurnStatus = { ...status };
166
+ }
167
+ return this.getCurrentTurnStatus();
168
+ }
158
169
  setSessionMessageHandler(handler) {
159
170
  this.sessionMessageHandler = typeof handler === "function" ? handler : null;
160
171
  }
@@ -172,7 +183,9 @@ export class RemoteAiSession extends EventEmitter {
172
183
  return this.callWorker("ensureSessionInfo", []);
173
184
  }
174
185
  async getSessionUsageSummary() {
175
- return this.callWorker("getSessionUsageSummary", []);
186
+ // Control lane: fire awaits this before every runtime-status report, so
187
+ // queueing it behind a running `runTurn` would stall mid-turn statuses.
188
+ return this.callWorker("getSessionUsageSummary", [], { messageType: "control" });
176
189
  }
177
190
  async interruptCurrentTurn() {
178
191
  return this.callWorker("interruptCurrentTurn", [], {
@@ -3,7 +3,7 @@ import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in
3
3
  import { appendContextFilesToPrompt } from "../context-files.js";
4
4
  import { PROVIDER_MEDIA_CAPABILITIES, buildClaudeContent } from "../media-adapters.js";
5
5
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
6
- import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
6
+ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
7
7
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
8
8
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
9
9
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -135,9 +135,11 @@ export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
135
135
  function normalizeText(value) {
136
136
  return typeof value === "string" ? value : "";
137
137
  }
138
+ function contentBlocks(message) {
139
+ return Array.isArray(message?.content) ? message.content : [];
140
+ }
138
141
  function extractAssistantText(message) {
139
- const content = Array.isArray(message?.content) ? message.content : [];
140
- return content
142
+ return contentBlocks(message)
141
143
  .map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
142
144
  .filter(Boolean)
143
145
  .join("");
@@ -339,7 +341,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
339
341
  return this.sessionInfo ? { ...this.sessionInfo } : null;
340
342
  }
341
343
  getCurrentTurnStatus() {
342
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
344
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
343
345
  }
344
346
  async ensureSessionInfo() {
345
347
  return this.getSessionInfo();
@@ -773,6 +775,11 @@ export class ClaudeAgentSdkSession extends EventEmitter {
773
775
  }
774
776
  case "assistant": {
775
777
  this.updateSessionInfo(message.session_id || message.sessionId);
778
+ for (const block of contentBlocks(message.message)) {
779
+ if (block?.type === "tool_use") {
780
+ noteToolStarted(currentTurn, block.id, block.name, block.input);
781
+ }
782
+ }
776
783
  const text = extractAssistantText(message.message);
777
784
  if (!text) {
778
785
  return;
@@ -788,6 +795,13 @@ export class ClaudeAgentSdkSession extends EventEmitter {
788
795
  await this.emitAssistantMessage(text);
789
796
  return;
790
797
  }
798
+ case "user":
799
+ for (const block of contentBlocks(message.message)) {
800
+ if (block?.type === "tool_result") {
801
+ noteToolFinished(currentTurn, block.tool_use_id);
802
+ }
803
+ }
804
+ return;
791
805
  case "rate_limit_event":
792
806
  this.updateSessionInfo(message.session_id || message.sessionId);
793
807
  this.rateLimitInfo =
@@ -4,7 +4,7 @@ import { appendContextFilesToPrompt } from "../context-files.js";
4
4
  import { PROVIDER_MEDIA_CAPABILITIES, buildCodexAppServerInput } from "../media-adapters.js";
5
5
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
6
6
  import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
7
- import { TERMINAL_GOAL_STATUSES, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
7
+ import { TERMINAL_GOAL_STATUSES, describeCodexToolItem, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
8
8
  const TERMINAL_GOAL_STATUSES_LOCAL = new Set(TERMINAL_GOAL_STATUSES);
9
9
  function isTerminalGoalStatusLocal(status) {
10
10
  return typeof status === "string" && TERMINAL_GOAL_STATUSES_LOCAL.has(status);
@@ -301,7 +301,7 @@ export class CodexAppServerSession extends EventEmitter {
301
301
  return this.sessionInfo ? { ...this.sessionInfo } : null;
302
302
  }
303
303
  getCurrentTurnStatus() {
304
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
304
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
305
305
  }
306
306
  async ensureSessionInfo() {
307
307
  await this.boot();
@@ -835,6 +835,8 @@ export class CodexAppServerSession extends EventEmitter {
835
835
  if (!currentTurn) {
836
836
  return;
837
837
  }
838
+ const tool = describeCodexToolItem(params?.item);
839
+ noteToolStarted(currentTurn, extractItemId(params?.item), tool?.name, tool?.input);
838
840
  const phase = resolveItemPhase(params?.item);
839
841
  if (!phase) {
840
842
  return;
@@ -864,6 +866,7 @@ export class CodexAppServerSession extends EventEmitter {
864
866
  if (!currentTurn) {
865
867
  return;
866
868
  }
869
+ noteToolFinished(currentTurn, extractItemId(params?.item));
867
870
  const phase = resolveItemPhase(params?.item);
868
871
  if (phase !== "assistant_message") {
869
872
  return;
@@ -9,7 +9,7 @@ import { CODEX_EXEC_VARIANT as CODEX_EXEC_PROVIDER_VARIANT } from "../built-in-b
9
9
  import { appendContextFilesToPrompt } from "../context-files.js";
10
10
  import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
11
11
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
12
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
12
+ import { describeCodexToolItem, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
13
13
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
14
14
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
15
15
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -250,7 +250,7 @@ export class CodexExecSession extends EventEmitter {
250
250
  return this.sessionInfo ? { ...this.sessionInfo } : null;
251
251
  }
252
252
  getCurrentTurnStatus() {
253
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
253
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
254
254
  }
255
255
  async ensureSessionInfo() {
256
256
  this.announceSession();
@@ -475,6 +475,13 @@ export class CodexExecSession extends EventEmitter {
475
475
  payload = { type: "raw", line: normalizedLine };
476
476
  }
477
477
  currentTurn.stdoutEvents.push(payload);
478
+ if (payload?.type === "item.started") {
479
+ const tool = describeCodexToolItem(payload.item);
480
+ noteToolStarted(currentTurn, payload.item?.id, tool?.name, tool?.input);
481
+ }
482
+ else if (payload?.type === "item.completed") {
483
+ noteToolFinished(currentTurn, payload.item?.id);
484
+ }
478
485
  const phase = resolveExecPhase(payload);
479
486
  if (phase) {
480
487
  void this.emitWorkingStatus({
@@ -6,7 +6,7 @@ import { COPILOT_SDK_VARIANT as COPILOT_PROVIDER_VARIANT } from "../built-in-bac
6
6
  import { appendContextFilesToPrompt } from "../context-files.js";
7
7
  import { PROVIDER_MEDIA_CAPABILITIES, buildCopilotAttachments } from "../media-adapters.js";
8
8
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
9
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
9
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, withActiveTool, } from "../shared.js";
10
10
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
11
11
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
12
12
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -601,7 +601,7 @@ export class CopilotSdkSession extends EventEmitter {
601
601
  return this.sessionInfo ? { ...this.sessionInfo } : null;
602
602
  }
603
603
  getCurrentTurnStatus() {
604
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
604
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
605
605
  }
606
606
  async ensureSessionInfo() {
607
607
  await this.boot();
@@ -1199,6 +1199,7 @@ export class CopilotSdkSession extends EventEmitter {
1199
1199
  }
1200
1200
  currentTurn.activeToolName = normalizeText(event.data?.toolName);
1201
1201
  currentTurn.activeToolPhase = toolPhaseForName(currentTurn.activeToolName);
1202
+ noteToolStarted(currentTurn, event.data?.toolCallId, currentTurn.activeToolName, event.data?.arguments);
1202
1203
  await this.emitWorkingStatus({
1203
1204
  phase: currentTurn.activeToolPhase,
1204
1205
  reply_in_progress: true,
@@ -1239,6 +1240,7 @@ export class CopilotSdkSession extends EventEmitter {
1239
1240
  }
1240
1241
  currentTurn.activeToolName = "";
1241
1242
  currentTurn.activeToolPhase = "";
1243
+ noteToolFinished(currentTurn, event.data?.toolCallId);
1242
1244
  const preview = extractToolResultPreview(event.data?.result);
1243
1245
  await this.emitWorkingStatus({
1244
1246
  phase: "message_aggregation",
@@ -6,7 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { DSH_SDK_VARIANT } from "../built-in-backends.js";
9
- import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
9
+ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
10
10
  const require = createRequire(import.meta.url);
11
11
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
12
12
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -220,7 +220,7 @@ export class DshSdkSession extends EventEmitter {
220
220
  return this.sessionInfo ? { ...this.sessionInfo } : null;
221
221
  }
222
222
  getCurrentTurnStatus() {
223
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
223
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
224
224
  }
225
225
  async ensureSessionInfo() {
226
226
  return this.getSessionInfo();
@@ -714,6 +714,7 @@ export class DshSdkSession extends EventEmitter {
714
714
  }
715
715
  case "tool/call": {
716
716
  const toolName = String(event.data?.name || "");
717
+ noteToolStarted(currentTurn, event.data?.callId, toolName, event.data?.arguments);
717
718
  const phase = toolPhaseForName(toolName);
718
719
  const argsPreview = sanitizeForLog(event.data?.arguments, 120);
719
720
  await this.emitWorkingStatus({
@@ -723,6 +724,9 @@ export class DshSdkSession extends EventEmitter {
723
724
  }, onProgress);
724
725
  return;
725
726
  }
727
+ case "tool/result":
728
+ noteToolFinished(currentTurn, event.data?.message?.content?.[0]?.toolCallId);
729
+ return;
726
730
  case "todo/write": {
727
731
  const todos = Array.isArray(event.data?.todos) ? event.data.todos : [];
728
732
  const done = todos.filter((item) => item?.status === "completed").length;
@@ -5,7 +5,7 @@ import { appendContextFilesToPrompt } from "../context-files.js";
5
5
  import { PROVIDER_MEDIA_CAPABILITIES, buildKimiContent } from "../media-adapters.js";
6
6
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
7
7
  import { KimiWireTransport } from "../transports/kimi-wire-transport.js";
8
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
8
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
9
9
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
10
10
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
11
11
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -241,7 +241,7 @@ export class KimiCliSession extends EventEmitter {
241
241
  return this.sessionInfo ? { ...this.sessionInfo } : null;
242
242
  }
243
243
  getCurrentTurnStatus() {
244
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
244
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
245
245
  }
246
246
  async ensureSessionInfo() {
247
247
  await this.boot();
@@ -802,6 +802,7 @@ export class KimiCliSession extends EventEmitter {
802
802
  if (toolId) {
803
803
  currentTurn.toolCalls.set(toolId, toolName);
804
804
  }
805
+ noteToolStarted(currentTurn, toolId, toolName, normalizedPayload?.function?.arguments);
805
806
  const phase = toolPhaseForName(toolName);
806
807
  await this.emitWorkingStatus({
807
808
  phase,
@@ -813,6 +814,7 @@ export class KimiCliSession extends EventEmitter {
813
814
  case "ToolResult": {
814
815
  const toolId = normalizeText(normalizedPayload.tool_call_id);
815
816
  const toolName = currentTurn.toolCalls.get(toolId) || "";
817
+ noteToolFinished(currentTurn, toolId);
816
818
  const phase = toolPhaseForName(toolName);
817
819
  const statusDoneLine = sanitizeSummary(normalizedPayload?.return_value?.message || normalizedPayload?.return_value?.output, 160) || undefined;
818
820
  await this.emitWorkingStatus({
@@ -8,7 +8,7 @@ import { KIMI_CLI_PRINT_VARIANT as KIMI_PRINT_PROVIDER_VARIANT } from "../built-
8
8
  import { appendContextFilesToPrompt } from "../context-files.js";
9
9
  import { PROVIDER_MEDIA_CAPABILITIES, UNSUPPORTED_MEDIA_CAPABILITIES, buildKimiContent, } from "../media-adapters.js";
10
10
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
11
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
11
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
12
12
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
13
13
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
14
14
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -281,7 +281,7 @@ export class KimiPrintSession extends EventEmitter {
281
281
  return this.sessionInfo ? { ...this.sessionInfo } : null;
282
282
  }
283
283
  getCurrentTurnStatus() {
284
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
284
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
285
285
  }
286
286
  async ensureSessionInfo() {
287
287
  if (!this.sessionInfo?.sessionIdDeferred) {
@@ -532,6 +532,10 @@ export class KimiPrintSession extends EventEmitter {
532
532
  const stdoutReader = readline.createInterface({ input: child.stdout });
533
533
  const stderrReader = readline.createInterface({ input: child.stderr });
534
534
  let timeoutId = null;
535
+ let lastActivityAt = Date.now();
536
+ const noteTurnActivity = () => {
537
+ lastActivityAt = Date.now();
538
+ };
535
539
  const settle = (error, value = null) => {
536
540
  if (currentTurn.settled) {
537
541
  return;
@@ -548,17 +552,35 @@ export class KimiPrintSession extends EventEmitter {
548
552
  }
549
553
  resolve(value);
550
554
  };
551
- timeoutId = setTimeout(() => {
552
- try {
553
- child.kill("SIGTERM");
554
- }
555
- catch {
556
- // ignore
555
+ // Activity-based idle deadline (aligned with wire mode): any stdout/stderr
556
+ // output resets the idle clock, so a busy turn is never killed mid-work;
557
+ // only a turn with no output at all for turnDeadlineMs is considered stuck.
558
+ const scheduleTurnDeadline = () => {
559
+ if (currentTurn.settled) {
560
+ return;
557
561
  }
558
- settle(createTurnError("Kimi print turn timed out", {
559
- reason: "turn_timeout",
560
- }));
561
- }, this.turnDeadlineMs);
562
+ const idleMs = Math.max(0, Date.now() - lastActivityAt);
563
+ const waitMs = Math.max(1, this.turnDeadlineMs - idleMs);
564
+ timeoutId = setTimeout(() => {
565
+ if (currentTurn.settled) {
566
+ return;
567
+ }
568
+ if (Date.now() - lastActivityAt < this.turnDeadlineMs) {
569
+ scheduleTurnDeadline();
570
+ return;
571
+ }
572
+ try {
573
+ child.kill("SIGTERM");
574
+ }
575
+ catch {
576
+ // ignore
577
+ }
578
+ settle(createTurnError("Kimi print turn timed out", {
579
+ reason: "turn_timeout",
580
+ }));
581
+ }, waitMs);
582
+ };
583
+ scheduleTurnDeadline();
562
584
  stdoutReader.on("line", (line) => {
563
585
  const normalizedLine = String(line || "").trim();
564
586
  if (!normalizedLine) {
@@ -571,6 +593,7 @@ export class KimiPrintSession extends EventEmitter {
571
593
  catch {
572
594
  payload = { role: "raw", content: normalizedLine };
573
595
  }
596
+ noteTurnActivity();
574
597
  currentTurn.items.push(payload);
575
598
  const role = String(payload?.role || "").trim().toLowerCase();
576
599
  if (role === "meta") {
@@ -583,6 +606,9 @@ export class KimiPrintSession extends EventEmitter {
583
606
  if (role === "assistant") {
584
607
  const assistantText = normalizeTextContent(payload.content);
585
608
  if (Array.isArray(payload?.tool_calls) && payload.tool_calls.length > 0) {
609
+ for (const call of payload.tool_calls) {
610
+ noteToolStarted(currentTurn, call?.id, call?.function?.name, call?.function?.arguments);
611
+ }
586
612
  void this.emitWorkingStatus({
587
613
  phase: "command_execution",
588
614
  reply_in_progress: true,
@@ -603,6 +629,7 @@ export class KimiPrintSession extends EventEmitter {
603
629
  return;
604
630
  }
605
631
  if (role === "tool") {
632
+ noteToolFinished(currentTurn, payload.tool_call_id);
606
633
  void this.emitWorkingStatus({
607
634
  phase: "command_execution",
608
635
  reply_in_progress: true,
@@ -615,6 +642,7 @@ export class KimiPrintSession extends EventEmitter {
615
642
  if (!normalizedLine.trim()) {
616
643
  return;
617
644
  }
645
+ noteTurnActivity();
618
646
  currentTurn.stderrTail.push(normalizedLine);
619
647
  if (currentTurn.stderrTail.length > 20) {
620
648
  currentTurn.stderrTail.shift();
@@ -4,7 +4,7 @@ import { appendContextFilesToPrompt } from "../context-files.js";
4
4
  import { PROVIDER_MEDIA_CAPABILITIES, buildOpencodeParts } from "../media-adapters.js";
5
5
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
6
6
  import { OpencodeServerTransport } from "../transports/opencode-server-transport.js";
7
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
7
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
8
8
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
9
9
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
10
10
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -234,7 +234,7 @@ export class OpencodeSdkSession extends EventEmitter {
234
234
  return this.sessionInfo ? { ...this.sessionInfo } : null;
235
235
  }
236
236
  getCurrentTurnStatus() {
237
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
237
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
238
238
  }
239
239
  async ensureSessionInfo() {
240
240
  await this.boot();
@@ -742,6 +742,12 @@ export class OpencodeSdkSession extends EventEmitter {
742
742
  return;
743
743
  }
744
744
  if (part.type === "tool") {
745
+ if (part.state?.status === "completed" || part.state?.status === "error") {
746
+ noteToolFinished(currentTurn, part.callID || part.id);
747
+ }
748
+ else {
749
+ noteToolStarted(currentTurn, part.callID || part.id, part.tool, part.state?.input);
750
+ }
745
751
  const phase = toolPhaseForName(part.tool);
746
752
  const toolStatus = part.state?.status === "completed"
747
753
  ? part.state?.title || part.state?.output || ""
package/dist/shared.d.ts CHANGED
@@ -20,6 +20,18 @@ export function normalizeLogger(logger: any): any;
20
20
  export function emitLog(logger: any, message: any): void;
21
21
  export function truncateText(value: any, maxLen?: number): string;
22
22
  export function sanitizeForLog(value: any, maxLen?: number): string;
23
+ /**
24
+ * Track tool calls that started but have not finished on a turn object, so
25
+ * `getCurrentTurnStatus()` can report what a long silent turn is running.
26
+ */
27
+ export function noteToolStarted(turn: any, id: any, name: any, input: any): void;
28
+ export function noteToolFinished(turn: any, id: any): void;
29
+ /** Map a codex app-server (camelCase) or exec (snake_case) tool item to `{ name, input }`. */
30
+ export function describeCodexToolItem(item: any): {
31
+ name: any;
32
+ input: any;
33
+ } | null;
34
+ export function withActiveTool(status: any, turn: any): any;
23
35
  export function isTruthyEnv(value: any): boolean;
24
36
  export function getBoundedEnvInt(envName: any, fallback: any, min: any, max: any): any;
25
37
  export function parseCommandParts(commandLine: any): {
package/dist/shared.js CHANGED
@@ -146,6 +146,76 @@ export function sanitizeForLog(value, maxLen = 180) {
146
146
  return "";
147
147
  return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
148
148
  }
149
+ const TOOL_INPUT_SUMMARY_KEYS = ["command", "cmd", "file_path", "filePath", "path", "pattern", "query", "url", "description", "prompt"];
150
+ function summarizeToolInput(input) {
151
+ let value = input;
152
+ if (typeof value === "string" && /^\s*[[{]/.test(value)) {
153
+ try {
154
+ value = JSON.parse(value);
155
+ }
156
+ catch {
157
+ // keep the raw string
158
+ }
159
+ }
160
+ if (value && typeof value === "object" && !Array.isArray(value)) {
161
+ const key = TOOL_INPUT_SUMMARY_KEYS.find((candidate) => value[candidate]);
162
+ value = key ? value[key] : JSON.stringify(value);
163
+ }
164
+ if (Array.isArray(value)) {
165
+ value = value.join(" ");
166
+ }
167
+ return sanitizeForLog(value, 200) || undefined;
168
+ }
169
+ /**
170
+ * Track tool calls that started but have not finished on a turn object, so
171
+ * `getCurrentTurnStatus()` can report what a long silent turn is running.
172
+ */
173
+ export function noteToolStarted(turn, id, name, input) {
174
+ if (!turn || !id || !name) {
175
+ return;
176
+ }
177
+ turn.activeTools ||= new Map();
178
+ const key = String(id);
179
+ if (turn.activeTools.has(key)) {
180
+ return;
181
+ }
182
+ turn.activeTools.set(key, {
183
+ name: String(name),
184
+ summary: summarizeToolInput(input),
185
+ started_at: new Date().toISOString(),
186
+ });
187
+ }
188
+ export function noteToolFinished(turn, id) {
189
+ if (id) {
190
+ turn?.activeTools?.delete(String(id));
191
+ }
192
+ }
193
+ /** Map a codex app-server (camelCase) or exec (snake_case) tool item to `{ name, input }`. */
194
+ export function describeCodexToolItem(item) {
195
+ switch (String(item?.type || "").replace(/_/g, "").toLowerCase()) {
196
+ case "commandexecution":
197
+ return { name: "command", input: item.command };
198
+ case "filechange":
199
+ return { name: "apply_patch", input: (item.changes || []).map((change) => change?.path).join(" ") };
200
+ case "mcptoolcall":
201
+ return { name: [item.server, item.tool].filter(Boolean).join("."), input: item.arguments };
202
+ case "dynamictoolcall":
203
+ case "collabagenttoolcall":
204
+ return { name: item.tool, input: item.arguments ?? item.prompt };
205
+ case "websearch":
206
+ return { name: "web_search", input: item.query };
207
+ default:
208
+ return null;
209
+ }
210
+ }
211
+ export function withActiveTool(status, turn) {
212
+ if (!status) {
213
+ return null;
214
+ }
215
+ const tools = turn?.activeTools ? [...turn.activeTools.values()] : [];
216
+ const activeTool = tools[tools.length - 1];
217
+ return activeTool ? { ...status, active_tool: { ...activeTool } } : { ...status };
218
+ }
149
219
  export function isTruthyEnv(value) {
150
220
  if (value === undefined || value === null)
151
221
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -53,12 +53,12 @@
53
53
  "zod": "^4.1.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@love-moon/chat-web": "^0.12.0"
56
+ "@love-moon/chat-web": "^0.13.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
60
60
  "tsx": "^4.20.6",
61
61
  "typescript": "^5.6.3"
62
62
  },
63
- "gitCommitId": "c1047e8"
63
+ "gitCommitId": "b82ba1c"
64
64
  }