@usabledev/usable-chat 1.152.1 → 1.154.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 (2) hide show
  1. package/cli.js +432 -204
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -77609,11 +77609,11 @@ var require_randomUUID = __commonJS({
77609
77609
  var require_dist_cjs31 = __commonJS({
77610
77610
  "node_modules/.pnpm/@smithy+uuid@1.1.2/node_modules/@smithy/uuid/dist-cjs/index.js"(exports) {
77611
77611
  "use strict";
77612
- var randomUUID7 = require_randomUUID();
77612
+ var randomUUID8 = require_randomUUID();
77613
77613
  var decimalToHex = Array.from({ length: 256 }, (_16, i18) => i18.toString(16).padStart(2, "0"));
77614
77614
  var v42 = () => {
77615
- if (randomUUID7.randomUUID) {
77616
- return randomUUID7.randomUUID();
77615
+ if (randomUUID8.randomUUID) {
77616
+ return randomUUID8.randomUUID();
77617
77617
  }
77618
77618
  const rnds = new Uint8Array(16);
77619
77619
  crypto.getRandomValues(rnds);
@@ -131517,7 +131517,7 @@ var require_gaxios = __commonJS({
131517
131517
  var retry_js_1 = require_retry();
131518
131518
  var stream_1 = __require("stream");
131519
131519
  var interceptor_js_1 = require_interceptor();
131520
- var randomUUID7 = async () => globalThis.crypto?.randomUUID() || (await import("crypto")).randomUUID();
131520
+ var randomUUID8 = async () => globalThis.crypto?.randomUUID() || (await import("crypto")).randomUUID();
131521
131521
  var HTTP_STATUS_NO_CONTENT = 204;
131522
131522
  var Gaxios = class {
131523
131523
  agentCache = /* @__PURE__ */ new Map();
@@ -131790,7 +131790,7 @@ var require_gaxios = __commonJS({
131790
131790
  */
131791
131791
  ["Blob", "File", "FormData"].includes(opts.data?.constructor?.name || "");
131792
131792
  if (opts.multipart?.length) {
131793
- const boundary = await randomUUID7();
131793
+ const boundary = await randomUUID8();
131794
131794
  preparedHeaders.set("content-type", `multipart/related; boundary=${boundary}`);
131795
131795
  opts.body = stream_1.Readable.from(this.getMultipartRequest(opts.multipart, boundary));
131796
131796
  } else if (shouldDirectlyPassData) {
@@ -162589,6 +162589,7 @@ async function adaptOpenRouterStream(stream, options2) {
162589
162589
  let finishReason = null;
162590
162590
  const toolCalls = [];
162591
162591
  const completedToolCalls = [];
162592
+ const toolExecutionPromises = [];
162592
162593
  const toolResults = [];
162593
162594
  let capturedUsage;
162594
162595
  let capturedModel;
@@ -162598,6 +162599,188 @@ async function adaptOpenRouterStream(stream, options2) {
162598
162599
  let lastEmittedReasoning = "";
162599
162600
  const accumulatedReasoningDetails = [];
162600
162601
  const pendingToolCalls = /* @__PURE__ */ new Map();
162602
+ const executeParsedToolCall = async (id, name18, args) => {
162603
+ if (context.abortSignal?.aborted) {
162604
+ streamLogger.info("Abort signal detected before tool execution, returning cancelled result", {
162605
+ toolName: name18
162606
+ });
162607
+ const errorResult = { error: "Request cancelled by user", success: false };
162608
+ toolCalls.push({ toolName: name18, success: false });
162609
+ toolResults.push({ id, name: name18, result: errorResult, isError: true });
162610
+ const errorEvent = emitter.emit("tool-result", {
162611
+ id,
162612
+ toolName: name18,
162613
+ input: args,
162614
+ result: errorResult,
162615
+ success: false,
162616
+ error: errorResult.error
162617
+ });
162618
+ multiplexer.send(errorEvent);
162619
+ return;
162620
+ }
162621
+ streamLogger.debug("Executing tool", { toolName: name18, args });
162622
+ try {
162623
+ const toolResult = await executeTool3(name18, args, {
162624
+ ...context,
162625
+ toolCallId: id
162626
+ });
162627
+ const success2 = !toolResult?.error;
162628
+ toolCalls.push({ toolName: name18, success: success2 });
162629
+ toolResults.push({ id, name: name18, result: toolResult, isError: !success2 });
162630
+ if (context.session) {
162631
+ if (!context.session.toolHistory) {
162632
+ context.session.toolHistory = [];
162633
+ }
162634
+ const toolHistory = context.session.toolHistory;
162635
+ toolHistory.push({
162636
+ toolName: name18,
162637
+ args,
162638
+ result: toolResult,
162639
+ success: success2,
162640
+ timestamp: Date.now()
162641
+ });
162642
+ if (toolHistory.length > 50) {
162643
+ toolHistory.shift();
162644
+ }
162645
+ }
162646
+ const toolResultEvent = emitter.emit("tool-result", {
162647
+ id,
162648
+ toolName: name18,
162649
+ input: args,
162650
+ result: toolResult,
162651
+ success: success2,
162652
+ error: toolResult?.error ? String(toolResult.error) : void 0
162653
+ });
162654
+ multiplexer.send(toolResultEvent);
162655
+ if (name18 === "check-context-window" && success2 && toolResult?.data) {
162656
+ const data2 = toolResult.data;
162657
+ const breakdown = data2.breakdown.systemPrompt !== void 0 ? {
162658
+ systemPrompts: data2.breakdown.systemPrompt + (data2.breakdown.systemTools || 0),
162659
+ contextTokens: data2.breakdown.memoryFiles || 0,
162660
+ toolCallsAndResponses: data2.breakdown.toolUseAndResults || 0,
162661
+ userQuestions: 0,
162662
+ assistantMessages: data2.breakdown.messages || 0
162663
+ } : data2.breakdown;
162664
+ const cardEvent = emitter.emit("card", {
162665
+ component: "context-window",
162666
+ data: {
162667
+ totalTokens: data2.totalTokens,
162668
+ maxTokens: data2.maxTokens,
162669
+ percentage: data2.percentage,
162670
+ breakdown,
162671
+ freeSpace: data2.freeSpace,
162672
+ contextItems: data2.contextItems || {
162673
+ workspaces: 0,
162674
+ fragments: 0,
162675
+ toolResults: 0
162676
+ }
162677
+ }
162678
+ });
162679
+ multiplexer.send(cardEvent);
162680
+ }
162681
+ if (isAskQuestionPause(toolResult)) {
162682
+ pauseDetected = true;
162683
+ if (onPauseDetected) {
162684
+ await onPauseDetected({
162685
+ resumeToken: toolResult.__pause.resumeToken,
162686
+ questions: toolResult.__pause.questions,
162687
+ context: toolResult.__pause.context
162688
+ });
162689
+ }
162690
+ return;
162691
+ }
162692
+ if (toolResult?.__parentToolCall === true) {
162693
+ const parentToolName = toolResult.toolName;
162694
+ const parentToolArgs = toolResult.args;
162695
+ const requestId = `parent_${id}_${Date.now()}`;
162696
+ streamLogger.info("Parent tool call detected - waiting for parent response", {
162697
+ toolName: name18,
162698
+ parentToolName,
162699
+ requestId
162700
+ });
162701
+ const parentToolEvent = emitter.emit("parent-tool-call", {
162702
+ requestId,
162703
+ toolName: parentToolName,
162704
+ args: parentToolArgs
162705
+ });
162706
+ multiplexer.send(parentToolEvent);
162707
+ try {
162708
+ const parentResponse = await waitForParentToolResponse(
162709
+ requestId,
162710
+ parentToolName,
162711
+ parentToolArgs,
162712
+ 3e4
162713
+ );
162714
+ streamLogger.info("Parent tool response received", {
162715
+ requestId,
162716
+ parentToolName,
162717
+ hasResult: parentResponse !== void 0
162718
+ });
162719
+ const resultIndex = toolResults.findIndex((r17) => r17.id === id);
162720
+ if (resultIndex !== -1) {
162721
+ toolResults[resultIndex].result = parentResponse;
162722
+ }
162723
+ const updatedResultEvent = emitter.emit("tool-result", {
162724
+ id,
162725
+ toolName: name18,
162726
+ input: args,
162727
+ result: parentResponse,
162728
+ success: true
162729
+ });
162730
+ multiplexer.send(updatedResultEvent);
162731
+ } catch (parentError) {
162732
+ streamLogger.error("Parent tool call failed", {
162733
+ requestId,
162734
+ parentToolName,
162735
+ error: parentError instanceof Error ? parentError.message : String(parentError)
162736
+ });
162737
+ const errorResult = {
162738
+ error: parentError instanceof Error ? parentError.message : String(parentError),
162739
+ success: false
162740
+ };
162741
+ const resultIndex = toolResults.findIndex((r17) => r17.id === id);
162742
+ if (resultIndex !== -1) {
162743
+ toolResults[resultIndex].result = errorResult;
162744
+ toolResults[resultIndex].isError = true;
162745
+ }
162746
+ const errorEvent = emitter.emit("tool-result", {
162747
+ id,
162748
+ toolName: name18,
162749
+ input: args,
162750
+ result: errorResult,
162751
+ success: false,
162752
+ error: parentError instanceof Error ? parentError.message : String(parentError)
162753
+ });
162754
+ multiplexer.send(errorEvent);
162755
+ }
162756
+ }
162757
+ if (emitObservations && success2) {
162758
+ const observation = generateObservation2(name18, toolResult);
162759
+ const observationEvent = emitter.emit("observation", {
162760
+ observation,
162761
+ toolCallIds: [id]
162762
+ });
162763
+ multiplexer.send(observationEvent);
162764
+ }
162765
+ } catch (toolError) {
162766
+ console.error(`\u274C Tool execution error for ${name18}:`, toolError);
162767
+ toolCalls.push({ toolName: name18, success: false });
162768
+ const errorResult = {
162769
+ error: toolError instanceof Error ? toolError.message : String(toolError),
162770
+ success: false
162771
+ };
162772
+ toolResults.push({ id, name: name18, result: errorResult, isError: true });
162773
+ const errorEvent = emitter.emit("tool-result", {
162774
+ id,
162775
+ toolName: name18,
162776
+ input: args,
162777
+ result: null,
162778
+ success: false,
162779
+ error: toolError instanceof Error ? toolError.message : String(toolError)
162780
+ });
162781
+ multiplexer.send(errorEvent);
162782
+ }
162783
+ };
162601
162784
  try {
162602
162785
  while (true) {
162603
162786
  if (context.abortSignal?.aborted) {
@@ -162855,187 +163038,9 @@ async function adaptOpenRouterStream(stream, options2) {
162855
163038
  });
162856
163039
  multiplexer.send(retrievalEvent);
162857
163040
  }
162858
- if (context.abortSignal?.aborted) {
162859
- streamLogger.info("Abort signal detected before tool execution, skipping", {
162860
- toolName: pending.name
162861
- });
162862
- break;
162863
- }
162864
- streamLogger.debug("Executing tool", {
162865
- toolName: pending.name,
162866
- args
162867
- });
162868
- try {
162869
- const toolResult = await executeTool3(pending.name, args, { ...context, toolCallId: pending.id });
162870
- const success2 = !toolResult?.error;
162871
- toolCalls.push({ toolName: pending.name, success: success2 });
162872
- toolResults.push({
162873
- id: pending.id,
162874
- name: pending.name,
162875
- result: toolResult,
162876
- isError: !success2
162877
- });
162878
- if (context.session) {
162879
- if (!context.session.toolHistory) {
162880
- context.session.toolHistory = [];
162881
- }
162882
- const toolHistory = context.session.toolHistory;
162883
- toolHistory.push({
162884
- toolName: pending.name,
162885
- args,
162886
- result: toolResult,
162887
- success: success2,
162888
- timestamp: Date.now()
162889
- });
162890
- if (toolHistory.length > 50) {
162891
- toolHistory.shift();
162892
- }
162893
- }
162894
- const toolResultEvent = emitter.emit("tool-result", {
162895
- id: pending.id,
162896
- toolName: pending.name,
162897
- input: args,
162898
- // Include input arguments
162899
- result: toolResult,
162900
- success: success2,
162901
- error: toolResult?.error ? String(toolResult.error) : void 0
162902
- });
162903
- multiplexer.send(toolResultEvent);
162904
- if (pending.name === "check-context-window" && success2 && toolResult?.data) {
162905
- const data2 = toolResult.data;
162906
- const breakdown = data2.breakdown.systemPrompt !== void 0 ? {
162907
- // New format - map to old format
162908
- systemPrompts: data2.breakdown.systemPrompt + (data2.breakdown.systemTools || 0),
162909
- contextTokens: data2.breakdown.memoryFiles || 0,
162910
- toolCallsAndResponses: data2.breakdown.toolUseAndResults || 0,
162911
- userQuestions: 0,
162912
- // Will be calculated from messages
162913
- assistantMessages: data2.breakdown.messages || 0
162914
- } : data2.breakdown;
162915
- const cardEvent = emitter.emit("card", {
162916
- component: "context-window",
162917
- data: {
162918
- totalTokens: data2.totalTokens,
162919
- maxTokens: data2.maxTokens,
162920
- percentage: data2.percentage,
162921
- breakdown,
162922
- freeSpace: data2.freeSpace,
162923
- contextItems: data2.contextItems || {
162924
- workspaces: 0,
162925
- fragments: 0,
162926
- toolResults: 0
162927
- }
162928
- }
162929
- });
162930
- multiplexer.send(cardEvent);
162931
- }
162932
- if (isAskQuestionPause(toolResult)) {
162933
- pauseDetected = true;
162934
- if (onPauseDetected) {
162935
- await onPauseDetected({
162936
- resumeToken: toolResult.__pause.resumeToken,
162937
- questions: toolResult.__pause.questions,
162938
- context: toolResult.__pause.context
162939
- });
162940
- }
162941
- break;
162942
- }
162943
- if (toolResult?.__parentToolCall === true) {
162944
- const parentToolName = toolResult.toolName;
162945
- const parentToolArgs = toolResult.args;
162946
- const requestId = `parent_${pending.id}_${Date.now()}`;
162947
- streamLogger.info("Parent tool call detected - waiting for parent response", {
162948
- toolName: pending.name,
162949
- parentToolName,
162950
- requestId
162951
- });
162952
- const parentToolEvent = emitter.emit("parent-tool-call", {
162953
- requestId,
162954
- toolName: parentToolName,
162955
- args: parentToolArgs
162956
- });
162957
- multiplexer.send(parentToolEvent);
162958
- try {
162959
- const parentResponse = await waitForParentToolResponse(
162960
- requestId,
162961
- parentToolName,
162962
- parentToolArgs,
162963
- 3e4
162964
- // 30 second timeout
162965
- );
162966
- streamLogger.info("Parent tool response received", {
162967
- requestId,
162968
- parentToolName,
162969
- hasResult: parentResponse !== void 0
162970
- });
162971
- const resultIndex = toolResults.findIndex((r17) => r17.id === pending.id);
162972
- if (resultIndex !== -1) {
162973
- toolResults[resultIndex].result = parentResponse;
162974
- }
162975
- const updatedResultEvent = emitter.emit("tool-result", {
162976
- id: pending.id,
162977
- toolName: pending.name,
162978
- input: args,
162979
- result: parentResponse,
162980
- success: true
162981
- });
162982
- multiplexer.send(updatedResultEvent);
162983
- } catch (parentError) {
162984
- streamLogger.error("Parent tool call failed", {
162985
- requestId,
162986
- parentToolName,
162987
- error: parentError instanceof Error ? parentError.message : String(parentError)
162988
- });
162989
- const errorResult = {
162990
- error: parentError instanceof Error ? parentError.message : String(parentError),
162991
- success: false
162992
- };
162993
- const resultIndex = toolResults.findIndex((r17) => r17.id === pending.id);
162994
- if (resultIndex !== -1) {
162995
- toolResults[resultIndex].result = errorResult;
162996
- toolResults[resultIndex].isError = true;
162997
- }
162998
- const errorEvent = emitter.emit("tool-result", {
162999
- id: pending.id,
163000
- toolName: pending.name,
163001
- input: args,
163002
- result: errorResult,
163003
- success: false,
163004
- error: parentError instanceof Error ? parentError.message : String(parentError)
163005
- });
163006
- multiplexer.send(errorEvent);
163007
- }
163008
- }
163009
- if (emitObservations && success2) {
163010
- const observation = generateObservation2(pending.name, toolResult);
163011
- const observationEvent = emitter.emit("observation", {
163012
- observation,
163013
- toolCallIds: [pending.id]
163014
- });
163015
- multiplexer.send(observationEvent);
163016
- }
163017
- } catch (toolError) {
163018
- console.error(`\u274C Tool execution error for ${pending.name}:`, toolError);
163019
- toolCalls.push({ toolName: pending.name, success: false });
163020
- const errorResult = {
163021
- error: toolError instanceof Error ? toolError.message : String(toolError),
163022
- success: false
163023
- };
163024
- toolResults.push({
163025
- id: pending.id,
163026
- name: pending.name,
163027
- result: errorResult,
163028
- isError: true
163029
- });
163030
- const errorEvent = emitter.emit("tool-result", {
163031
- id: pending.id,
163032
- toolName: pending.name,
163033
- result: null,
163034
- success: false,
163035
- error: toolError instanceof Error ? toolError.message : String(toolError)
163036
- });
163037
- multiplexer.send(errorEvent);
163038
- }
163041
+ toolExecutionPromises.push(
163042
+ executeParsedToolCall(pending.id, pending.name, args)
163043
+ );
163039
163044
  pendingToolCalls.delete(index2);
163040
163045
  } catch {
163041
163046
  }
@@ -163048,6 +163053,13 @@ async function adaptOpenRouterStream(stream, options2) {
163048
163053
  finishReason: finish_reason,
163049
163054
  hasUsage: !!capturedUsage
163050
163055
  });
163056
+ if (finish_reason === "tool_calls" && toolExecutionPromises.length > 0) {
163057
+ streamLogger.debug("Waiting for OpenRouter tool executions", {
163058
+ toolCount: toolExecutionPromises.length
163059
+ });
163060
+ await Promise.allSettled(toolExecutionPromises);
163061
+ toolExecutionPromises.length = 0;
163062
+ }
163051
163063
  if (finish_reason !== "tool_calls" && !summaryEmitted) {
163052
163064
  if (capturedUsage && (capturedUsage.completion_tokens ?? 0) > 0) {
163053
163065
  const usageData = {
@@ -163082,6 +163094,13 @@ async function adaptOpenRouterStream(stream, options2) {
163082
163094
  }
163083
163095
  }
163084
163096
  }
163097
+ if (toolExecutionPromises.length > 0) {
163098
+ streamLogger.debug("Waiting for pending OpenRouter tool executions after stream end", {
163099
+ toolCount: toolExecutionPromises.length
163100
+ });
163101
+ await Promise.allSettled(toolExecutionPromises);
163102
+ toolExecutionPromises.length = 0;
163103
+ }
163085
163104
  if (finishReason && !summaryEmitted && finishReason !== "tool_calls") {
163086
163105
  streamLogger.warn("Stream ended without usage chunk - emitting summary without usage", {
163087
163106
  finishReason
@@ -175976,6 +175995,33 @@ data: ${JSON.stringify(envelope)}
175976
175995
  });
175977
175996
 
175978
175997
  // src/core/tools/spawn-subagent.ts
175998
+ async function maybeStartBackgroundRelay(redis, context, conversationId, taskId, success2) {
175999
+ if (!redis || !context.startBackgroundSubagentRelay) return false;
176000
+ try {
176001
+ const record2 = await spawnedTaskStore.get(redis, taskId);
176002
+ if (record2?.notifiedTurnAt) {
176003
+ return true;
176004
+ }
176005
+ const lockKey = `subagents:background-relay-lock:${conversationId}`;
176006
+ const acquired = await redis.set(lockKey, taskId, "NX", "EX", 180).catch(() => null);
176007
+ if (!acquired) {
176008
+ return true;
176009
+ }
176010
+ const started = await context.startBackgroundSubagentRelay({
176011
+ conversationId,
176012
+ taskId,
176013
+ reason: success2 ? "completed" : "failed"
176014
+ });
176015
+ return started;
176016
+ } catch (err) {
176017
+ logger.debug("SpawnSubagent", "Failed to start background relay job", {
176018
+ conversationId,
176019
+ taskId,
176020
+ error: err instanceof Error ? err.message : String(err)
176021
+ });
176022
+ return false;
176023
+ }
176024
+ }
175979
176025
  async function maybeEmitPingPrompt(redis, conversationId, taskId) {
175980
176026
  if (!redis) return;
175981
176027
  try {
@@ -176276,7 +176322,10 @@ HOW TO USE THIS:
176276
176322
  tokenUsage: response.tokenUsage
176277
176323
  }
176278
176324
  });
176279
- await maybeEmitPingPrompt(redis, conversationId, taskId);
176325
+ const relayed = await maybeStartBackgroundRelay(redis, context, conversationId, taskId, success2);
176326
+ if (!relayed) {
176327
+ await maybeEmitPingPrompt(redis, conversationId, taskId);
176328
+ }
176280
176329
  } catch (err) {
176281
176330
  const errorMessage = err instanceof Error ? err.message : String(err);
176282
176331
  const status = abort.signal.aborted ? "cancelled" : "failed";
@@ -176291,7 +176340,10 @@ HOW TO USE THIS:
176291
176340
  taskId,
176292
176341
  data: { error: errorMessage }
176293
176342
  });
176294
- await maybeEmitPingPrompt(redis, conversationId, taskId);
176343
+ const relayed = await maybeStartBackgroundRelay(redis, context, conversationId, taskId, false);
176344
+ if (!relayed) {
176345
+ await maybeEmitPingPrompt(redis, conversationId, taskId);
176346
+ }
176295
176347
  } finally {
176296
176348
  spawnedTaskStore.unregisterLocalAbort(taskId);
176297
176349
  }
@@ -249322,7 +249374,8 @@ async function orchestrate(request) {
249322
249374
  chatMode,
249323
249375
  imageGenModel: context.metadata?.imageGenModel,
249324
249376
  imageGenThinking: context.metadata?.imageGenThinking,
249325
- registeredParentToolSchemas: context.registeredParentToolSchemas
249377
+ registeredParentToolSchemas: context.registeredParentToolSchemas,
249378
+ startBackgroundSubagentRelay: context.startBackgroundSubagentRelay
249326
249379
  });
249327
249380
  if (!config3.localFilesystem) {
249328
249381
  aiTools["spawn_subagent"] = {
@@ -252400,6 +252453,7 @@ function createOrchestratorRequest(messages4, context, config3, persona, apiKey,
252400
252453
  metadata: context.metadata,
252401
252454
  allowedWorkspaceIds: context.allowedWorkspaceIds,
252402
252455
  registeredParentToolSchemas: context.registeredParentToolSchemas,
252456
+ startBackgroundSubagentRelay: context.startBackgroundSubagentRelay,
252403
252457
  extensions: context.extensions
252404
252458
  // Plugin hook seam (CLI only; undefined on web)
252405
252459
  },
@@ -254193,6 +254247,7 @@ var init_skills3 = __esm({
254193
254247
  // src/adapters/cli/subagent.ts
254194
254248
  import { spawn as spawn5 } from "node:child_process";
254195
254249
  import { basename as basename8 } from "node:path";
254250
+ import { randomUUID as randomUUID6 } from "node:crypto";
254196
254251
  function currentSubagentDepth() {
254197
254252
  const d21 = Number(process.env.USABLE_SUBAGENT_DEPTH ?? 0);
254198
254253
  return Number.isFinite(d21) && d21 > 0 ? Math.floor(d21) : 0;
@@ -254277,11 +254332,73 @@ async function runCliSubagent(input) {
254277
254332
  }
254278
254333
  return { ok: true, text: res.out.trim() };
254279
254334
  }
254335
+ function publicTask(task) {
254336
+ return {
254337
+ taskId: task.taskId,
254338
+ status: task.status,
254339
+ prompt: task.prompt,
254340
+ model: task.model,
254341
+ cwd: task.cwd,
254342
+ startedAt: task.startedAt,
254343
+ completedAt: task.completedAt,
254344
+ ...task.text ? { text: task.text } : {},
254345
+ ...task.error ? { error: task.error } : {}
254346
+ };
254347
+ }
254348
+ function startCliSubagentTask(input) {
254349
+ const taskId = input.taskId ?? randomUUID6();
254350
+ const abortController = new AbortController();
254351
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
254352
+ const run2 = input._run ?? runCliSubagent;
254353
+ const onParentAbort = () => abortController.abort();
254354
+ input.parentAbortSignal?.addEventListener("abort", onParentAbort, { once: true });
254355
+ const task = {
254356
+ taskId,
254357
+ prompt: input.prompt,
254358
+ model: input.model,
254359
+ cwd: input.cwd,
254360
+ status: "running",
254361
+ startedAt,
254362
+ abortController,
254363
+ promise: Promise.resolve(void 0)
254364
+ };
254365
+ task.promise = (async () => {
254366
+ try {
254367
+ const res = await run2({
254368
+ prompt: input.prompt,
254369
+ model: input.model,
254370
+ cwd: input.cwd,
254371
+ abortSignal: abortController.signal,
254372
+ timeoutMs: input.timeoutMs
254373
+ });
254374
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
254375
+ if (abortController.signal.aborted) {
254376
+ task.status = "cancelled";
254377
+ task.error = "subagent cancelled";
254378
+ } else if (res.ok) {
254379
+ task.status = "completed";
254380
+ task.text = res.text;
254381
+ } else {
254382
+ task.status = "failed";
254383
+ task.text = res.text;
254384
+ task.error = res.error ?? "subagent failed";
254385
+ }
254386
+ } catch (err) {
254387
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
254388
+ task.status = abortController.signal.aborted ? "cancelled" : "failed";
254389
+ task.error = err instanceof Error ? err.message : String(err);
254390
+ } finally {
254391
+ input.parentAbortSignal?.removeEventListener("abort", onParentAbort);
254392
+ }
254393
+ return task;
254394
+ })();
254395
+ cliSubagentTasks.set(taskId, task);
254396
+ return task;
254397
+ }
254280
254398
  function buildCliSubagentTool(opts) {
254281
- const run2 = opts._run ?? runCliSubagent;
254282
254399
  return {
254283
254400
  spawn_subagent: {
254284
- description: SPAWN_DESCRIPTION,
254401
+ description: ASYNC_SPAWN_DESCRIPTION,
254285
254402
  parameters: {
254286
254403
  type: "object",
254287
254404
  properties: {
@@ -254300,31 +254417,89 @@ function buildCliSubagentTool(opts) {
254300
254417
  const prompt = String(args.prompt ?? "").trim();
254301
254418
  if (!prompt) return { error: "prompt is required" };
254302
254419
  const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : opts.model;
254303
- const res = await run2({
254420
+ const task = startCliSubagentTask({
254421
+ taskId: typeof args.taskId === "string" && args.taskId.trim() ? args.taskId.trim() : void 0,
254304
254422
  prompt,
254305
254423
  model,
254306
254424
  cwd: process.cwd(),
254307
- abortSignal: opts.abortSignal
254425
+ parentAbortSignal: opts.abortSignal,
254426
+ _run: opts._run
254308
254427
  });
254309
- if (!res.ok) {
254310
- return { error: res.error ?? "subagent failed", ...res.text ? { partial: res.text } : {} };
254428
+ return publicTask(task);
254429
+ }
254430
+ },
254431
+ list_subagents: {
254432
+ description: "List background CLI subagents in this process. Use this before awaiting when you need task ids or status.",
254433
+ parameters: {
254434
+ type: "object",
254435
+ properties: {
254436
+ includeFinished: {
254437
+ type: "boolean",
254438
+ description: "Include completed, failed, and cancelled tasks. Defaults to true."
254439
+ }
254440
+ }
254441
+ },
254442
+ execute: async (args) => {
254443
+ const includeFinished = args.includeFinished !== false;
254444
+ const tasks = [...cliSubagentTasks.values()].filter(
254445
+ (task) => includeFinished || task.status === "running"
254446
+ );
254447
+ return { tasks: tasks.map(publicTask) };
254448
+ }
254449
+ },
254450
+ await_subagents: {
254451
+ description: "Wait for one or more background CLI subagents and collect their results. Omit taskIds to wait for all running tasks.",
254452
+ parameters: {
254453
+ type: "object",
254454
+ properties: {
254455
+ taskIds: {
254456
+ type: "array",
254457
+ items: { type: "string" },
254458
+ description: "Specific task ids to wait for. Defaults to all running tasks."
254459
+ },
254460
+ timeout: {
254461
+ type: "number",
254462
+ description: "Maximum seconds to wait. Defaults to 300."
254463
+ }
254464
+ }
254465
+ },
254466
+ execute: async (args) => {
254467
+ const ids = Array.isArray(args.taskIds) ? args.taskIds.filter((id) => typeof id === "string" && id.length > 0) : [...cliSubagentTasks.values()].filter((task) => task.status === "running").map((task) => task.taskId);
254468
+ const tasks = ids.map((id) => cliSubagentTasks.get(id));
254469
+ const missing = ids.filter((id, index2) => !tasks[index2]);
254470
+ const existing = tasks.filter((task) => !!task);
254471
+ if (missing.length > 0) return { error: `Unknown subagent task id(s): ${missing.join(", ")}` };
254472
+ if (existing.length === 0) return { results: [] };
254473
+ const timeoutS = typeof args.timeout === "number" && Number.isFinite(args.timeout) ? Math.max(1, args.timeout) : 300;
254474
+ const timeout = new Promise(
254475
+ (resolve8) => setTimeout(() => resolve8("timeout"), timeoutS * 1e3)
254476
+ );
254477
+ const settled = await Promise.race([
254478
+ Promise.all(existing.map((task) => task.promise)),
254479
+ timeout
254480
+ ]);
254481
+ if (settled === "timeout") {
254482
+ return {
254483
+ error: `Timed out waiting for subagents after ${timeoutS}s`,
254484
+ pending: existing.filter((task) => task.status === "running").map(publicTask),
254485
+ results: existing.filter((task) => task.status !== "running").map(publicTask)
254486
+ };
254311
254487
  }
254312
- return { result: res.text };
254488
+ return { results: settled.map(publicTask) };
254313
254489
  }
254314
254490
  }
254315
254491
  };
254316
254492
  }
254317
- var SUBAGENT_TIMEOUT_MS, SUBAGENT_MAX_DEPTH, SPAWN_DESCRIPTION;
254493
+ var SUBAGENT_TIMEOUT_MS, SUBAGENT_MAX_DEPTH, cliSubagentTasks, ASYNC_SPAWN_DESCRIPTION;
254318
254494
  var init_subagent = __esm({
254319
254495
  "src/adapters/cli/subagent.ts"() {
254320
254496
  "use strict";
254321
254497
  SUBAGENT_TIMEOUT_MS = 5 * 60 * 1e3;
254322
254498
  SUBAGENT_MAX_DEPTH = 2;
254323
- SPAWN_DESCRIPTION = `Spawn a subagent to handle a self-contained task and return its findings directly.
254324
-
254325
- The subagent is a fresh \`usable-chat\` agent running in YOUR current working directory, with the SAME model/provider and the same real filesystem + shell + MCP tools + skills. Use it to fan out isolated work \u2014 explore a folder, search, summarize a large file, draft something \u2014 without cluttering your own context. It returns its answer inline (no separate await step).
254499
+ cliSubagentTasks = /* @__PURE__ */ new Map();
254500
+ ASYNC_SPAWN_DESCRIPTION = `Start a background subagent for a self-contained task and return immediately.
254326
254501
 
254327
- Give it a complete, standalone \`prompt\` (it does not see your conversation). Keep tasks focused; it can read/run things on the user's machine.`;
254502
+ The task runs in this CLI process. Use list_subagents to inspect status and await_subagents to collect results. The subagent is a fresh \`usable-chat\` agent running in the current working directory, with the same provider/config and local tools.`;
254328
254503
  }
254329
254504
  });
254330
254505
 
@@ -269592,7 +269767,7 @@ var init_tui_markdown = __esm({
269592
269767
  // src/cli/warp-agent.ts
269593
269768
  import { openSync, writeSync } from "node:fs";
269594
269769
  import { basename as basename9 } from "node:path";
269595
- import { randomUUID as randomUUID6 } from "node:crypto";
269770
+ import { randomUUID as randomUUID7 } from "node:crypto";
269596
269771
  function isWarpAgentHost() {
269597
269772
  return !!process.env.WARP_CLI_AGENT_PROTOCOL_VERSION && process.platform !== "win32";
269598
269773
  }
@@ -269645,7 +269820,7 @@ var init_warp_agent = __esm({
269645
269820
  "use strict";
269646
269821
  WARP_AGENT_SLUG = "usable-chat";
269647
269822
  APP_NAME = "usable-chat";
269648
- sessionId = randomUUID6();
269823
+ sessionId = randomUUID7();
269649
269824
  }
269650
269825
  });
269651
269826
 
@@ -269758,6 +269933,7 @@ async function launchTui(options2) {
269758
269933
  const ui2 = new TUI(terminal);
269759
269934
  const editor = new Editor(ui2, editorTheme, { paddingX: 1 });
269760
269935
  editor.focused = true;
269936
+ const bangTools = createLocalTools();
269761
269937
  let items = [...initialHistory];
269762
269938
  let compactedPrefix = [];
269763
269939
  let busy = false;
@@ -269768,6 +269944,7 @@ async function launchTui(options2) {
269768
269944
  let exitArmed = false;
269769
269945
  let toolsExpanded = false;
269770
269946
  let abort = null;
269947
+ let lastBangCommand = null;
269771
269948
  const extStatus = /* @__PURE__ */ new Map();
269772
269949
  let picker = null;
269773
269950
  const rebuildPicker = () => {
@@ -270021,6 +270198,56 @@ Edit it, then /verify-extension ${arg.trim()} to check it loads, /trust (project
270021
270198
  async function submit(raw) {
270022
270199
  const value = raw.trim();
270023
270200
  if (!value || busy) return;
270201
+ if (value === "!!" || value.startsWith("!") && !value.startsWith("/")) {
270202
+ const command = value === "!!" ? lastBangCommand : value.slice(1).trim();
270203
+ if (!command) {
270204
+ sys(value === "!!" ? "No previous shell command." : "Usage: ! <command>", "error");
270205
+ return;
270206
+ }
270207
+ lastBangCommand = command;
270208
+ busy = true;
270209
+ push({ kind: "user", text: `! ${command}` });
270210
+ const toolId = `bang-${Date.now()}`;
270211
+ push({
270212
+ kind: "tool",
270213
+ id: toolId,
270214
+ name: "bash",
270215
+ args: short(JSON.stringify({ command })),
270216
+ argsFull: prettyJson({ command }),
270217
+ status: "running"
270218
+ });
270219
+ ui2.requestRender();
270220
+ try {
270221
+ const result = await bangTools.bash.execute({ command });
270222
+ const error41 = result && typeof result === "object" && "error" in result ? String(result.error) : void 0;
270223
+ items = items.map(
270224
+ (it7) => it7.kind === "tool" && it7.id === toolId ? {
270225
+ ...it7,
270226
+ status: error41 ? "error" : "done",
270227
+ result: error41 ? void 0 : previewValue(result),
270228
+ error: error41
270229
+ } : it7
270230
+ );
270231
+ } catch (err) {
270232
+ items = items.map(
270233
+ (it7) => it7.kind === "tool" && it7.id === toolId ? {
270234
+ ...it7,
270235
+ status: "error",
270236
+ error: err instanceof Error ? err.message : String(err)
270237
+ } : it7
270238
+ );
270239
+ } finally {
270240
+ busy = false;
270241
+ ui2.requestRender();
270242
+ if (autoSubmit) {
270243
+ setTimeout(() => {
270244
+ teardown();
270245
+ exit(0);
270246
+ }, 25);
270247
+ }
270248
+ }
270249
+ return;
270250
+ }
270024
270251
  if (value.startsWith("/")) {
270025
270252
  const [cmd, ...rest] = value.slice(1).split(/\s+/);
270026
270253
  await runSlash(cmd.toLowerCase(), rest.join(" "));
@@ -270298,6 +270525,7 @@ var init_tui2 = __esm({
270298
270525
  "use strict";
270299
270526
  init_dist8();
270300
270527
  init_tui_select();
270528
+ init_tools();
270301
270529
  init_tui_markdown();
270302
270530
  init_warp_agent();
270303
270531
  BANNER_LINES = [
@@ -270948,7 +271176,7 @@ init_tui_select();
270948
271176
  init_model_registry();
270949
271177
 
270950
271178
  // package.json
270951
- var version2 = "1.152.1";
271179
+ var version2 = "1.154.0";
270952
271180
 
270953
271181
  // src/adapters/cli/model-catalog.ts
270954
271182
  init_codex_auth();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usabledev/usable-chat",
3
- "version": "1.152.1",
3
+ "version": "1.154.0",
4
4
  "description": "usable-chat — terminal harness for usable-chat (headless + TUI)",
5
5
  "type": "module",
6
6
  "bin": {