@2kw/ai 6.3.0-dev.111 → 6.3.0-dev.116

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/README.md CHANGED
@@ -112,7 +112,7 @@ kubectl-style contexts switch between organizations and environments:
112
112
  | `providers` | Manage BYOK AI providers |
113
113
  | `analytics` | Usage analytics: spend, quality, providers, errors |
114
114
  | `billing` | Check subscription tier and usage limits |
115
- | `agents` | Run agents and decide approvals; apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
115
+ | `agents` | Run agents and answer paused runs (approvals and relayed tool calls); apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
116
116
  | `skills` | Import and export SKILL.md bundles; manage skill versions and labels |
117
117
  | `plugins` | Install plugin repositories, sync them, and inspect their reports |
118
118
  | `conversations` | Create and manage conversations and their items |
@@ -21,6 +21,8 @@ const oneOfLowercase = (...values) => ({ type: "string", enum: values });
21
21
  export const NON_AUTHORABLE_TOOL_TYPES = {
22
22
  "backbone.tool_search": "the planner adds it automatically",
23
23
  "backbone.skill_read": "it is added at run time when skills are pinned",
24
+ // #944: SkillsetPinner injects it for eligible callers and strips any stored copy (#779).
25
+ "backbone.skills_apply": "it is added at run time for callers allowed to author skills",
24
26
  web_search: "it has no executor and is skipped",
25
27
  };
26
28
  const BUILTIN_TYPES = [
@@ -6,7 +6,7 @@ import { BackboneApiError, CliUsageError } from "../lib/errors.js";
6
6
  import { withSpinner } from "../lib/output.js";
7
7
  import { resolveAgent, splitAgentRef, suggestAgents } from "../lib/agent-lookup.js";
8
8
  import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOption, printRunText, stripControl, } from "../lib/agent-run.js";
9
- import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, planDecisions, } from "../lib/agent-decide.js";
9
+ import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, parseToolAnswers, planContinuation, } from "../lib/agent-decide.js";
10
10
  import { PromptAbortedError, promptApprovals, readlineAsk } from "../lib/approval-prompt.js";
11
11
  import { withModelOverride } from "../lib/agent-models.js";
12
12
  import { readStdinInput } from "./ai.js";
@@ -171,9 +171,11 @@ export function makeAgentRunCommand() {
171
171
  });
172
172
  });
173
173
  }
174
+ /** Repeatable `--output` / `--fail`: a variadic `<v...>` would swallow the arguments that follow it. */
175
+ const collect = (value, previous = []) => [...previous, value];
174
176
  export function makeAgentDecideCommand() {
175
177
  return new Command("decide")
176
- .description("Decide every pending approval of a paused run and continue it")
178
+ .description("Answer a paused run: decide every pending approval and answer every relayed tool call in one call, then continue it")
177
179
  .argument("<agent>", "Agent name or id, optionally with @label and #model (use the same @label and #model the run used)")
178
180
  .requiredOption("--response <responseId>", "The paused response id (responseId in the run envelope)")
179
181
  .option("--approve <ids...>", "Approval ids to approve")
@@ -182,11 +184,15 @@ export function makeAgentDecideCommand() {
182
184
  .option("--reject-all", "Reject every pending approval of the response")
183
185
  .option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
184
186
  .option("--remember", "Also approve later calls of the same tool in this conversation")
187
+ .option("--output <callId=value>", "Answer a relayed tool call: <callId>=<text>, <callId>=@<file> or <callId>=@- for stdin; within one hour of the pause (repeatable)", collect)
188
+ .option("--fail <callId=message>", "Answer a relayed tool call as failed: <callId>=<message> (same @file / @- forms; repeatable)", collect)
185
189
  .option("--mode <mode>", MODE_OPTION_HELP)
186
190
  .option("--raw", "Print the untouched API response")
187
191
  .action(async (agentRef, opts, command) => {
188
192
  await runAction(command, async () => {
189
193
  const mode = parseModeOption(opts.mode);
194
+ // Syntax, duplicates and file reads are checked before any request (#671 D4).
195
+ const toolAnswers = parseToolAnswers(opts.output, opts.fail);
190
196
  const client = getClient(command);
191
197
  const { name, label, model } = splitAgentRef(agentRef);
192
198
  // Dropped silently, an empty override would continue the run on the default model.
@@ -194,15 +200,15 @@ export function makeAgentDecideCommand() {
194
200
  throw new CliUsageError(`Empty #model in '${agentRef}': use the model the run used, or drop the '#'.`);
195
201
  const agent = await resolveAgent(client, name);
196
202
  const pending = await fetchPendingApprovals(client, String(agent.id), opts.response);
197
- const decisions = planDecisions(pending, {
203
+ const decisions = planContinuation(pending, {
198
204
  approve: opts.approve,
199
205
  reject: opts.reject,
200
206
  approveAll: opts.approveAll,
201
207
  rejectAll: opts.rejectAll,
202
208
  reason: opts.reason,
203
209
  remember: opts.remember,
204
- }, opts.response);
205
- const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode));
210
+ }, toolAnswers, opts.response);
211
+ const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode, toolAnswers));
206
212
  await finishRun(command, client, { name, label, model, mode }, result, { raw: !!opts.raw, interactive: false });
207
213
  });
208
214
  });
@@ -77,6 +77,7 @@ export function makeTracingCommand() {
77
77
  const sessions = new Command("sessions").description("Session-level trace views (spans grouped by exporter-stamped session id)");
78
78
  const sessionsList = new Command("list").description("List trace sessions");
79
79
  sessionsList.option("--search <query>", "Free-text search across session id and name");
80
+ sessionsList.option("--session-id <sessionId>", "Exact match on the session.id the exporter stamped (slashes allowed)");
80
81
  sessionsList.option("--from <iso>", "Start of time range (ISO-8601)");
81
82
  sessionsList.option("--to <iso>", "End of time range (ISO-8601)");
82
83
  sessionsList.option("--page <n>", "Page index (0-based)", "0");
@@ -88,6 +89,7 @@ export function makeTracingCommand() {
88
89
  params: {
89
90
  query: {
90
91
  search: opts.search,
92
+ sessionId: opts.sessionId,
91
93
  from: opts.from,
92
94
  to: opts.to,
93
95
  page: parseInt(opts.page, 10),
@@ -96,17 +98,17 @@ export function makeTracingCommand() {
96
98
  },
97
99
  });
98
100
  const page = data;
99
- formatList((page?.content ?? []), command, ["sessionId", "name", "startTime", "durationMs", "turnCount", "errorCount", "costUsd"]);
101
+ formatList((page?.content ?? []), command, ["id", "sessionId", "name", "startTime", "durationMs", "turnCount", "errorCount", "costUsd"]);
100
102
  });
101
103
  });
102
104
  sessions.addCommand(sessionsList);
103
105
  const sessionsGet = new Command("get").description("Get all spans of a session by id, sorted chronologically");
104
- sessionsGet.argument("<sessionId>", "Session id");
105
- sessionsGet.action(async (sessionId, _opts, command) => {
106
+ sessionsGet.argument("<id>", "Session id: the id column of `sessions list` (find one by session.id with --session-id)");
107
+ sessionsGet.action(async (id, _opts, command) => {
106
108
  await runAction(command, async () => {
107
109
  const client = getClient(command);
108
- const { data } = await client.GET("/v1/traces/sessions/{sessionId}", {
109
- params: { path: { sessionId } },
110
+ const { data } = await client.GET("/v1/traces/sessions/{id}", {
111
+ params: { path: { id } },
110
112
  });
111
113
  formatDetail(data, command);
112
114
  });
@@ -22,21 +22,47 @@ export interface PromptAnswer {
22
22
  reason?: string;
23
23
  remember?: boolean;
24
24
  }
25
+ /** One relayed tool call answered by `--output` or `--fail` (#671 D2, D3). */
26
+ export interface ToolAnswer {
27
+ callId: string;
28
+ /** Sent verbatim as the item's `output` string. */
29
+ output: string;
30
+ failed: boolean;
31
+ }
32
+ export interface ToolAnswerIo {
33
+ readFile: (path: string) => string;
34
+ readStdin: () => string;
35
+ }
36
+ /**
37
+ * `--output` and `--fail` values as `<callId>=<value>`, split on the first `=`. `@<path>` reads a file and
38
+ * `@-` reads stdin, both verbatim. Only syntax is checked here: without stored state the CLI cannot list the
39
+ * calls a response released, so completeness is the server's check (#671 D4). Every check runs before any read.
40
+ */
41
+ export declare function parseToolAnswers(outputs?: string[], fails?: string[], io?: ToolAnswerIo): ToolAnswer[];
25
42
  export declare function fetchPendingApprovals(client: ApiClient, agentId: string, responseId: string): Promise<ApprovalRow[]>;
26
43
  /**
27
44
  * The backend refuses a continuation that leaves any pending approval of the paused
28
45
  * response undecided (400 incomplete_tool_outputs), so the whole set is checked here first.
29
46
  */
30
47
  export declare function planDecisions(pending: ApprovalRow[], opts: DecideOptions, responseId: string): Decision[];
48
+ /**
49
+ * The decisions a `decide` call sends beside its tool answers (#671 D5). A pure relay pause has no pending
50
+ * approval, so the approval flags are optional there: `--approve-all` / `--reject-all` add nothing (P1) and
51
+ * only explicit ids are refused. A mixed pause needs its approvals decided in the same call.
52
+ */
53
+ export declare function planContinuation(pending: ApprovalRow[], opts: DecideOptions, toolAnswers: ToolAnswer[], responseId: string): Decision[];
31
54
  export declare function decisionsFromAnswers(pending: ApprovalRow[], answers: Map<string, PromptAnswer>): Decision[];
32
55
  /** Same item shape as n8n Decide Approval (#660). */
33
56
  export declare function toApprovalItems(decisions: Decision[]): Record<string, string>[];
57
+ /** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
58
+ export declare function toToolOutputItems(answers: ToolAnswer[]): Record<string, string | boolean>[];
34
59
  /**
35
60
  * Continues on `agent/<agentId>[@<label>][#<model>]` with the label and model override the run used: the
36
61
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
37
62
  * the run's or the continuation drops to another version or the agent's default model.
38
63
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
39
64
  * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
65
+ * Tool answers go first, in flag order (#671 D6): the server replays accepted outputs ahead of other items.
40
66
  */
41
- export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode): Promise<ResponsesResult>;
67
+ export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode, toolAnswers?: ToolAnswer[]): Promise<ResponsesResult>;
42
68
  //# sourceMappingURL=agent-decide.d.ts.map
@@ -1,6 +1,69 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { modeItem } from "./agent-run.js";
2
3
  import { CliUsageError } from "./errors.js";
3
4
  import { paginationParams } from "./pagination.js";
5
+ const defaultIo = {
6
+ readFile: (path) => readFileSync(path, "utf-8"),
7
+ // Not readStdinInput: that one trims, and a tool result is sent as read (P5).
8
+ readStdin: () => readFileSync(0, "utf-8"),
9
+ };
10
+ /**
11
+ * `--output` and `--fail` values as `<callId>=<value>`, split on the first `=`. `@<path>` reads a file and
12
+ * `@-` reads stdin, both verbatim. Only syntax is checked here: without stored state the CLI cannot list the
13
+ * calls a response released, so completeness is the server's check (#671 D4). Every check runs before any read.
14
+ */
15
+ export function parseToolAnswers(outputs = [], fails = [], io = defaultIo) {
16
+ const entries = [
17
+ ...outputs.map((raw) => ({ raw, flag: "--output", noun: "value", failed: false })),
18
+ ...fails.map((raw) => ({ raw, flag: "--fail", noun: "message", failed: true })),
19
+ ].map(({ raw, flag, noun, failed }) => {
20
+ const eq = raw.indexOf("=");
21
+ if (eq < 0)
22
+ throw new CliUsageError(`Invalid ${flag} '${raw}': use <callId>=<${noun}>.`);
23
+ const callId = raw.slice(0, eq);
24
+ if (!callId)
25
+ throw new CliUsageError(`Invalid ${flag} '${raw}': the call id is empty.`);
26
+ return { callId, value: raw.slice(eq + 1), failed };
27
+ });
28
+ const seen = new Map();
29
+ for (const e of entries) {
30
+ const before = seen.get(e.callId);
31
+ if (before === undefined) {
32
+ seen.set(e.callId, e.failed);
33
+ }
34
+ else if (before === e.failed) {
35
+ throw new CliUsageError(`Tool call ${e.callId} is answered more than once.`);
36
+ }
37
+ else {
38
+ throw new CliUsageError(`Tool call ${e.callId} is answered by both --output and --fail.`);
39
+ }
40
+ }
41
+ if (entries.filter((e) => e.value === "@-").length > 1) {
42
+ throw new CliUsageError("Only one --output or --fail value can read stdin (@-).");
43
+ }
44
+ return entries.map(({ callId, value, failed }) => ({ callId, output: readAnswerValue(value, io), failed }));
45
+ }
46
+ function readAnswerValue(value, io) {
47
+ if (value === "@-") {
48
+ try {
49
+ return io.readStdin();
50
+ }
51
+ catch (err) {
52
+ // A closed or non-blocking stdin (EAGAIN, EOF) is a usage error like an unreadable file.
53
+ throw new CliUsageError(`stdin: cannot read (${err.code ?? err.message})`);
54
+ }
55
+ }
56
+ if (!value.startsWith("@"))
57
+ return value;
58
+ const path = value.slice(1);
59
+ try {
60
+ return io.readFile(path);
61
+ }
62
+ catch (err) {
63
+ const code = err.code;
64
+ throw new CliUsageError(`${path}: cannot read file (${code ?? err.message})`);
65
+ }
66
+ }
4
67
  const MAX_PAGES = 50;
5
68
  export async function fetchPendingApprovals(client, agentId, responseId) {
6
69
  const rows = [];
@@ -41,7 +104,8 @@ export function planDecisions(pending, opts, responseId) {
41
104
  throw new CliUsageError("Use either --approve-all / --reject-all or explicit --approve / --reject ids, not both.");
42
105
  }
43
106
  if (!usesAll && approve.length === 0 && reject.length === 0) {
44
- throw new CliUsageError("Choose a decision: --approve <ids...>, --reject <ids...>, --approve-all or --reject-all.");
107
+ throw new CliUsageError("Choose a decision: --approve <ids...>, --reject <ids...>, --approve-all or --reject-all " +
108
+ "(and answer relayed tool calls with --output / --fail).");
45
109
  }
46
110
  const choice = new Map();
47
111
  if (usesAll) {
@@ -84,6 +148,26 @@ export function planDecisions(pending, opts, responseId) {
84
148
  };
85
149
  });
86
150
  }
151
+ /**
152
+ * The decisions a `decide` call sends beside its tool answers (#671 D5). A pure relay pause has no pending
153
+ * approval, so the approval flags are optional there: `--approve-all` / `--reject-all` add nothing (P1) and
154
+ * only explicit ids are refused. A mixed pause needs its approvals decided in the same call.
155
+ */
156
+ export function planContinuation(pending, opts, toolAnswers, responseId) {
157
+ if (toolAnswers.length === 0)
158
+ return planDecisions(pending, opts, responseId);
159
+ const ids = [...(opts.approve ?? []), ...(opts.reject ?? [])];
160
+ if (pending.length === 0) {
161
+ if (ids.length)
162
+ throw new CliUsageError(`Not pending on ${responseId}: ${ids.join(", ")}`);
163
+ return [];
164
+ }
165
+ if (!opts.approveAll && !opts.rejectAll && ids.length === 0) {
166
+ throw new CliUsageError(`${responseId} also waits for approvals: every relayed tool call and every pending approval must be answered ` +
167
+ `in one call. Undecided: ${pending.map((r) => r.id).join(", ")} (add --approve-all, --reject-all or the ids).`);
168
+ }
169
+ return planDecisions(pending, opts, responseId);
170
+ }
87
171
  export function decisionsFromAnswers(pending, answers) {
88
172
  return pending.map((r) => {
89
173
  const answer = answers.get(String(r.id));
@@ -109,20 +193,30 @@ export function toApprovalItems(decisions) {
109
193
  ...(d.remember ? { remember: d.remember } : {}),
110
194
  }));
111
195
  }
196
+ /** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
197
+ export function toToolOutputItems(answers) {
198
+ return answers.map((a) => ({
199
+ type: "function_call_output",
200
+ call_id: a.callId,
201
+ output: a.output,
202
+ ...(a.failed ? { failed: true } : {}),
203
+ }));
204
+ }
112
205
  /**
113
206
  * Continues on `agent/<agentId>[@<label>][#<model>]` with the label and model override the run used: the
114
207
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
115
208
  * the run's or the continuation drops to another version or the agent's default model.
116
209
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
117
210
  * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
211
+ * Tool answers go first, in flag order (#671 D6): the server replays accepted outputs ahead of other items.
118
212
  */
119
- export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode) {
120
- const approvals = toApprovalItems(decisions);
213
+ export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode, toolAnswers = []) {
214
+ const items = [...toToolOutputItems(toolAnswers), ...toApprovalItems(decisions)];
121
215
  const body = {
122
216
  model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
123
217
  previous_response_id: responseId,
124
218
  stream: false,
125
- input: mode ? [...approvals, modeItem(mode)] : approvals,
219
+ input: mode ? [...items, modeItem(mode)] : items,
126
220
  };
127
221
  // The generated body type does not model backbone: extension items, hence the cast.
128
222
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -139,6 +139,25 @@ export function connectionsSentence(connections) {
139
139
  }
140
140
  /** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
141
141
  const THEN_RUN = ", then run: ";
142
+ /**
143
+ * The `decide` command that answers a relay pause (#671 D7): one `--output <callId>=@<file>` per pending call,
144
+ * plus `--approve-all` when approvals wait too. The call id is chosen by the model, so the suggested file name
145
+ * keeps only `[A-Za-z0-9_-]` (P2) and never repeats: it can be neither `../…` nor an absolute path.
146
+ */
147
+ function relayDecideCommand(ref, responseId, calls, withApprovals) {
148
+ const used = new Set();
149
+ const outputs = calls.map((c) => {
150
+ const safe = c.callId.replace(/[^A-Za-z0-9_-]/g, "_");
151
+ let file = safe;
152
+ // Compared without case: on Windows and macOS `callA.out` and `calla.out` are one file.
153
+ for (let n = 2; used.has(file.toLowerCase()); n++)
154
+ file = `${safe}-${n}`;
155
+ used.add(file.toLowerCase());
156
+ return ` --output ${shellQuote(`${c.callId}=@${file}.out`)}`;
157
+ });
158
+ return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
159
+ (withApprovals ? " --approve-all" : ""));
160
+ }
142
161
  /**
143
162
  * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
144
163
  * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
@@ -225,8 +244,16 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
225
244
  if (status === "requires_approval" && ref && responseId) {
226
245
  next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
227
246
  }
228
- else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
229
- // Not with a relayed call beside it: a continuation without that call's output is refused.
247
+ else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
248
+ // The engine answers the connect call itself on the continuation, so connecting first is enough.
249
+ if (ref && responseId) {
250
+ const decide = relayDecideCommand(ref, responseId, pendingToolCalls, pendingApprovals.length > 0);
251
+ next = pendingConnections.length > 0
252
+ ? `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}${decide}`
253
+ : decide;
254
+ }
255
+ }
256
+ else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
230
257
  // `agents run --continue` needs no input: the continuation re-checks access itself.
231
258
  const resume = ref
232
259
  ? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
@@ -272,19 +299,32 @@ export function printRunText(env) {
272
299
  console.log(chalk.dim(`\n── ${s(head.join(" · "))}`));
273
300
  if (env.toolCalls.length)
274
301
  console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
302
+ const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
303
+ console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
304
+ // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
305
+ console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
306
+ if (a.reason)
307
+ console.log(chalk.dim(` reason: ${s(a.reason)}`));
308
+ });
275
309
  if (env.status === "requires_approval") {
276
310
  console.log(chalk.yellow("\nPaused for approval:"));
277
- env.pendingApprovals.forEach((a, i) => {
278
- console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
279
- // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
280
- console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
281
- if (a.reason)
282
- console.log(chalk.dim(` reason: ${s(a.reason)}`));
283
- });
311
+ printApprovals();
284
312
  if (env.next)
285
313
  console.log(`\nDecide with:\n ${s(env.next)}`);
286
314
  }
287
315
  else if (env.status === "requires_tool_output") {
316
+ if (env.pendingToolCalls.length) {
317
+ console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
318
+ env.pendingToolCalls.forEach((c) => {
319
+ console.log(` - ${s(c.tool)} (${s(c.callId)})`);
320
+ console.log(chalk.dim(` ${s(JSON.stringify(c.arguments))}`));
321
+ });
322
+ // The printed command carries --approve-all, so show what it approves (#671 P3).
323
+ if (env.pendingApprovals.length) {
324
+ console.log(chalk.yellow("\nIt also waits for approval:"));
325
+ printApprovals();
326
+ }
327
+ }
288
328
  if (env.pendingConnections.length) {
289
329
  console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
290
330
  env.pendingConnections.forEach((c) => {
@@ -296,9 +336,8 @@ export function printRunText(env) {
296
336
  if (env.next)
297
337
  console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
298
338
  }
299
- if (env.pendingToolCalls.length) {
300
- console.log(chalk.yellow("\nPaused: the agent needs client-side tool output this CLI cannot provide:"));
301
- env.pendingToolCalls.forEach((c) => console.log(` - ${s(c.tool)} (${s(c.callId)})`));
339
+ else if (env.pendingToolCalls.length && env.next) {
340
+ console.log(`\nAnswer with:\n ${s(env.next)}`);
302
341
  }
303
342
  if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
304
343
  console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
@@ -95,7 +95,9 @@ const PENDING_APPROVALS_HINT = "The approval was already decided or its response
95
95
  const CODE_HINTS = {
96
96
  approval_hmac_mismatch: PENDING_APPROVALS_HINT,
97
97
  unknown_approval_id: PENDING_APPROVALS_HINT,
98
- incomplete_tool_outputs: "Every pending approval of a paused response must be decided in one call. Use --approve-all or --reject-all, or name every id.",
98
+ incomplete_tool_outputs: "Every released tool call and every pending approval of a paused response must be answered in one call. " +
99
+ "Check pendingToolCalls and pendingApprovals in the run envelope.",
100
+ unknown_tool_output: "That call id was not released by this response. Use the responseId of the latest envelope.",
99
101
  conversation_agent_mismatch: "This conversation belongs to another agent. Start a new conversation or run the agent that owns it.",
100
102
  };
101
103
  /** The most specific hint for an API error: gateway code, then known messages, then the status table. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.111",
3
+ "version": "6.3.0-dev.116",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",