@2kw/ai 6.3.0-dev.96 → 6.3.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.
- package/README.md +1 -1
- package/dist/agent-config/schema.js +4 -0
- package/dist/commands/agent-run.js +11 -5
- package/dist/commands/ai.js +1 -7
- package/dist/commands/billing.js +102 -0
- package/dist/commands/installations.js +46 -1
- package/dist/commands/knowledge.js +5 -0
- package/dist/commands/tracing.js +7 -5
- package/dist/lib/agent-decide.d.ts +27 -1
- package/dist/lib/agent-decide.js +98 -4
- package/dist/lib/agent-run.d.ts +2 -1
- package/dist/lib/agent-run.js +62 -55
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.js +5 -19
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +147 -0
- package/dist/lib/errors.js +3 -1
- package/package.json +1 -1
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
|
|
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,10 @@ 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",
|
|
26
|
+
// #1241: injected while the conversation's outputs are held; an authored copy is dropped.
|
|
27
|
+
"backbone.deliver": "it is added at run time while sandbox outputs are held",
|
|
24
28
|
web_search: "it has no executor and is skipped",
|
|
25
29
|
};
|
|
26
30
|
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,
|
|
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("
|
|
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 =
|
|
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
|
});
|
package/dist/commands/ai.js
CHANGED
|
@@ -125,13 +125,7 @@ export function makeAiCommand() {
|
|
|
125
125
|
if (opts.conversation)
|
|
126
126
|
body.conversation = opts.conversation;
|
|
127
127
|
const client = getClient(command);
|
|
128
|
-
const { data } = await withSpinner("Generating response...", () =>
|
|
129
|
-
// The wire `input` field also accepts a bare string (shorthand for
|
|
130
|
-
// a single user message; see ResponseItemInputDeserializer), which
|
|
131
|
-
// the generated type does not model — hence the cast, same as the
|
|
132
|
-
// untyped convert.ts source-conversion calls.
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
-
client.POST("/v1/responses", { body }));
|
|
128
|
+
const { data } = await withSpinner("Generating response...", () => client.POST("/v1/responses", { body }));
|
|
135
129
|
printResponseResult(data, command, opts.agent);
|
|
136
130
|
});
|
|
137
131
|
});
|
package/dist/commands/billing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getClient, runAction } from "../lib/client.js";
|
|
3
3
|
import { isJsonOutput } from "../lib/config.js";
|
|
4
|
+
import { CliUsageError } from "../lib/errors.js";
|
|
4
5
|
import { formatDetail, formatList } from "../lib/output.js";
|
|
5
6
|
import chalk from "chalk";
|
|
6
7
|
export function makeBillingCommand() {
|
|
@@ -133,6 +134,107 @@ export function makeBillingCommand() {
|
|
|
133
134
|
}
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
cmd.addCommand(makeOverageCommand());
|
|
138
|
+
cmd.addCommand(makeTopUpsCommand());
|
|
139
|
+
return cmd;
|
|
140
|
+
}
|
|
141
|
+
/** Whole euros from cents, e.g. 1234 → "€12.34". Amounts are net of VAT (#973 D-O). */
|
|
142
|
+
function euros(cents) {
|
|
143
|
+
return `€${((cents ?? 0) / 100).toFixed(2)}`;
|
|
144
|
+
}
|
|
145
|
+
function parseWhole(value, name, min, max) {
|
|
146
|
+
const n = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
147
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
148
|
+
throw new CliUsageError(`${name} must be a whole number between ${min} and ${max}, not '${value}'`);
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
|
152
|
+
/** `bb billing overage get|set` — the opt-in overage cap (#973, plan D-O). Reads need a member login. */
|
|
153
|
+
function makeOverageCommand() {
|
|
154
|
+
const cmd = new Command("overage").description("Show or set the opt-in overage cap (euros, net of VAT)");
|
|
155
|
+
cmd
|
|
156
|
+
.command("get")
|
|
157
|
+
.description("Show the overage setting and this period's overage (needs `bb auth login`)")
|
|
158
|
+
.action(async (_opts, command) => {
|
|
159
|
+
await runAction(command, async () => {
|
|
160
|
+
const client = getClient(command);
|
|
161
|
+
const { data } = await client.GET("/v1/billing/overage");
|
|
162
|
+
if (isJsonOutput(command)) {
|
|
163
|
+
console.log(JSON.stringify(data, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`Overage: ${data?.enabled ? chalk.green("on") : chalk.dim("off")}`);
|
|
167
|
+
console.log(`Cap: €${data?.capEuros ?? 0} (max €${data?.maxCapEuros ?? 0})`);
|
|
168
|
+
console.log(`Available: ${data?.available ? chalk.green("yes") : chalk.yellow(`no (${data?.unavailableReason ?? "unknown"})`)}`);
|
|
169
|
+
console.log(`Spent: ${euros(data?.spentCents)}`);
|
|
170
|
+
console.log(`Reserved: ${euros(data?.reservedCents)}`);
|
|
171
|
+
if (data?.periodStart || data?.periodEnd) {
|
|
172
|
+
console.log(chalk.dim(`Period: ${data?.periodStart?.slice(0, 10) ?? "?"} → ${data?.periodEnd?.slice(0, 10) ?? "?"}`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
cmd
|
|
177
|
+
.command("set")
|
|
178
|
+
.description("Turn overage on with a cap, or off (admin)")
|
|
179
|
+
.option("--cap <euros>", "Monthly cap in whole euros, 1-100; turns overage on")
|
|
180
|
+
.option("--off", "Turn overage off, keeping the stored cap")
|
|
181
|
+
.action(async (opts, command) => {
|
|
182
|
+
await runAction(command, async () => {
|
|
183
|
+
if (opts.off === Boolean(opts.cap)) {
|
|
184
|
+
throw new CliUsageError("Pass exactly one of --cap <euros> or --off");
|
|
185
|
+
}
|
|
186
|
+
// --off sends no cap: the backend keeps the stored one, and no read is needed (the read
|
|
187
|
+
// refuses API keys, the write admits an admin's).
|
|
188
|
+
const body = opts.off
|
|
189
|
+
? { enabled: false }
|
|
190
|
+
: { enabled: true, capEuros: parseWhole(String(opts.cap), "cap", 1, 100) };
|
|
191
|
+
const client = getClient(command);
|
|
192
|
+
const { data } = await client.PUT("/v1/billing/overage", { body });
|
|
193
|
+
formatDetail(data, command);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
return cmd;
|
|
197
|
+
}
|
|
198
|
+
/** `bb billing top-ups list|buy` — prepaid credit packs (#973, plan D-E). Never opens a browser. */
|
|
199
|
+
function makeTopUpsCommand() {
|
|
200
|
+
const cmd = new Command("top-ups").description("List or buy prepaid credit (packs of 1,000 credits at €15)");
|
|
201
|
+
cmd
|
|
202
|
+
.command("list")
|
|
203
|
+
.description("List unexpired prepaid credit (needs `bb auth login`)")
|
|
204
|
+
.action(async (_opts, command) => {
|
|
205
|
+
await runAction(command, async () => {
|
|
206
|
+
const client = getClient(command);
|
|
207
|
+
const { data } = await client.GET("/v1/billing/top-ups");
|
|
208
|
+
if (isJsonOutput(command)) {
|
|
209
|
+
console.log(JSON.stringify(data, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const rows = (data ?? []).map((lot) => ({
|
|
213
|
+
kind: lot.kind,
|
|
214
|
+
purchased: euros(lot.purchasedCents),
|
|
215
|
+
remaining: euros(lot.remainingCents),
|
|
216
|
+
expires: lot.expiresAt?.slice(0, 10),
|
|
217
|
+
}));
|
|
218
|
+
formatList(rows, command, ["kind", "purchased", "remaining", "expires"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
cmd
|
|
222
|
+
.command("buy")
|
|
223
|
+
.description("Start a Stripe Checkout for top-up packs and print its URL (admin)")
|
|
224
|
+
.requiredOption("--packs <n>", "Number of packs, 1-20")
|
|
225
|
+
.action(async (opts, command) => {
|
|
226
|
+
await runAction(command, async () => {
|
|
227
|
+
const packs = parseWhole(String(opts.packs), "packs", 1, 20);
|
|
228
|
+
const client = getClient(command);
|
|
229
|
+
const { data } = await client.POST("/v1/billing/top-ups", { body: { packs } });
|
|
230
|
+
if (isJsonOutput(command)) {
|
|
231
|
+
console.log(JSON.stringify(data, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
console.log(`Open this page to pay: ${data?.checkoutUrl}`);
|
|
235
|
+
console.log(chalk.dim(`Expires: ${data?.expiresAt ?? "?"}`));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
136
238
|
return cmd;
|
|
137
239
|
}
|
|
138
240
|
//# sourceMappingURL=billing.js.map
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getClient, runAction } from "../lib/client.js";
|
|
3
|
+
import { CliUsageError } from "../lib/errors.js";
|
|
3
4
|
import { formatPage, formatDetail, formatList, formatSuccess } from "../lib/output.js";
|
|
4
5
|
import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
6
|
+
const MIN_MONTHLY_PERCENT = 0;
|
|
7
|
+
const MAX_MONTHLY_PERCENT = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Validated inside the action rather than as commander's argument parser, so the refusal
|
|
10
|
+
* travels through `runAction` and exits 2 like every other usage error (see `parsePort` in
|
|
11
|
+
* connectors.ts).
|
|
12
|
+
*/
|
|
13
|
+
function parseMonthlyPercent(value) {
|
|
14
|
+
const percent = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
15
|
+
if (!Number.isInteger(percent) || percent < MIN_MONTHLY_PERCENT || percent > MAX_MONTHLY_PERCENT) {
|
|
16
|
+
throw new CliUsageError(`percent must be a whole number between ${MIN_MONTHLY_PERCENT} and ${MAX_MONTHLY_PERCENT}, not '${value}'`);
|
|
17
|
+
}
|
|
18
|
+
return percent;
|
|
19
|
+
}
|
|
5
20
|
export function makeInstallationsCommand() {
|
|
6
21
|
const cmd = new Command("installations").description("Manage surface installations");
|
|
7
22
|
const list = new Command("list").description("List installations");
|
|
@@ -73,7 +88,7 @@ export function makeInstallationsCommand() {
|
|
|
73
88
|
body.status = opts.status;
|
|
74
89
|
const { data } = await client.PUT("/v1/installations/{id}", {
|
|
75
90
|
params: { path: { id } },
|
|
76
|
-
body
|
|
91
|
+
body,
|
|
77
92
|
});
|
|
78
93
|
formatDetail(data, command);
|
|
79
94
|
});
|
|
@@ -172,6 +187,36 @@ export function makeInstallationsCommand() {
|
|
|
172
187
|
});
|
|
173
188
|
});
|
|
174
189
|
cmd.addCommand(keys);
|
|
190
|
+
const budget = new Command("budget").description("Manage an installation's monthly budget: a share of the organisation's pool");
|
|
191
|
+
budget
|
|
192
|
+
.command("get")
|
|
193
|
+
.description("Read an installation's current budget: percent, period, and usage")
|
|
194
|
+
.argument("<id>", "Installation ID")
|
|
195
|
+
.action(async (id, _opts, command) => {
|
|
196
|
+
await runAction(command, async () => {
|
|
197
|
+
const client = getClient(command);
|
|
198
|
+
const { data } = await client.GET("/v1/installations/{id}/budget", {
|
|
199
|
+
params: { path: { id } },
|
|
200
|
+
});
|
|
201
|
+
formatDetail(data, command);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
budget
|
|
205
|
+
.command("set")
|
|
206
|
+
.description("Set or change an installation's monthly budget share, 0-1000 percent")
|
|
207
|
+
.argument("<id>", "Installation ID")
|
|
208
|
+
.argument("<percent>", "Monthly percent of the organisation's pool, 0-1000")
|
|
209
|
+
.action(async (id, percent, _opts, command) => {
|
|
210
|
+
await runAction(command, async () => {
|
|
211
|
+
const client = getClient(command);
|
|
212
|
+
const { data } = await client.PUT("/v1/installations/{id}/budget", {
|
|
213
|
+
params: { path: { id } },
|
|
214
|
+
body: { monthlyPercent: parseMonthlyPercent(percent) },
|
|
215
|
+
});
|
|
216
|
+
formatDetail(data, command);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
cmd.addCommand(budget);
|
|
175
220
|
return cmd;
|
|
176
221
|
}
|
|
177
222
|
//# sourceMappingURL=installations.js.map
|
|
@@ -47,6 +47,7 @@ export function makeKnowledgeCommand() {
|
|
|
47
47
|
.option("--parent-chunk-size <n>", "Parent chunk size (hierarchical chunking)", parseInt)
|
|
48
48
|
.option("--reranker-provider <id>", "Reranker provider ID")
|
|
49
49
|
.option("--hybrid-search <bool>", "Enable hybrid search (true/false)")
|
|
50
|
+
.option("--text-search-language <name>", "Text-search language for the lexical leg (default simple)")
|
|
50
51
|
.action(async (opts, command) => {
|
|
51
52
|
await runAction(command, async () => {
|
|
52
53
|
const client = getClient(command);
|
|
@@ -64,6 +65,7 @@ export function makeKnowledgeCommand() {
|
|
|
64
65
|
parentChunkSize: opts.parentChunkSize,
|
|
65
66
|
rerankerProviderId: opts.rerankerProvider,
|
|
66
67
|
hybridSearchEnabled: opts.hybridSearch !== undefined ? opts.hybridSearch === "true" : undefined,
|
|
68
|
+
textSearchLanguage: opts.textSearchLanguage,
|
|
67
69
|
},
|
|
68
70
|
});
|
|
69
71
|
formatDetail(data, command);
|
|
@@ -83,6 +85,7 @@ export function makeKnowledgeCommand() {
|
|
|
83
85
|
.option("--parent-chunk-size <n>", "New parent chunk size", parseInt)
|
|
84
86
|
.option("--reranker-provider <id>", "New reranker provider ID")
|
|
85
87
|
.option("--hybrid-search <bool>", "Enable hybrid search (true/false)")
|
|
88
|
+
.option("--text-search-language <name>", "Change the text-search language (reindexes in the background); omit to leave it untouched")
|
|
86
89
|
.action(async (id, opts, command) => {
|
|
87
90
|
await runAction(command, async () => {
|
|
88
91
|
const client = getClient(command);
|
|
@@ -107,6 +110,8 @@ export function makeKnowledgeCommand() {
|
|
|
107
110
|
body.rerankerProviderId = opts.rerankerProvider;
|
|
108
111
|
if (opts.hybridSearch !== undefined)
|
|
109
112
|
body.hybridSearchEnabled = opts.hybridSearch === "true";
|
|
113
|
+
if (opts.textSearchLanguage)
|
|
114
|
+
body.textSearchLanguage = opts.textSearchLanguage;
|
|
110
115
|
const { data } = await client.PATCH("/v1/knowledge-bases/{id}", {
|
|
111
116
|
params: { path: { id } },
|
|
112
117
|
body: body,
|
package/dist/commands/tracing.js
CHANGED
|
@@ -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("<
|
|
105
|
-
sessionsGet.action(async (
|
|
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/{
|
|
109
|
-
params: { path: {
|
|
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
|
package/dist/lib/agent-decide.js
CHANGED
|
@@ -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
|
|
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 ? [...
|
|
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
|
package/dist/lib/agent-run.d.ts
CHANGED
|
@@ -28,7 +28,8 @@ export interface PendingToolCall {
|
|
|
28
28
|
/**
|
|
29
29
|
* A connector the run waits for the user to connect, allow or reconnect in chat.2kw.ai: an open
|
|
30
30
|
* `backbone:connector_auth_request` (#807 R12). The caller never answers the connect call; the
|
|
31
|
-
* continuation re-checks access itself.
|
|
31
|
+
* continuation re-checks access itself. Decoded by `connect-pause.ts` (#1086), whose `id` and
|
|
32
|
+
* `callId` the envelope does not publish.
|
|
32
33
|
*/
|
|
33
34
|
export interface PendingConnection {
|
|
34
35
|
serverLabel: string;
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { DEFAULT_CHAT_URL } from "./config.js";
|
|
3
|
+
import { connectCallIds, connectionsPhrase, pendingConnectionsOf } from "./connect-pause.js";
|
|
3
4
|
import { CliUsageError } from "./errors.js";
|
|
4
5
|
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
5
6
|
/** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
|
|
@@ -92,53 +93,32 @@ export function shellQuote(value) {
|
|
|
92
93
|
return value;
|
|
93
94
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
94
95
|
}
|
|
95
|
-
/** An open connector consent request; a continuation's projection carries another status. */
|
|
96
|
-
function pendingConnectionsOf(output) {
|
|
97
|
-
return output
|
|
98
|
-
.filter((i) => i.type === "backbone:connector_auth_request" && (i.status === undefined || i.status === "in_progress"))
|
|
99
|
-
.map((i) => ({
|
|
100
|
-
serverLabel: String(i.server_label),
|
|
101
|
-
host: String(i.host),
|
|
102
|
-
reason: String(i.reason),
|
|
103
|
-
...(Array.isArray(i.destinations) && i.destinations.length > 0
|
|
104
|
-
? { destinations: i.destinations.map((d) => String(d)) }
|
|
105
|
-
: {}),
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* The call ids a connect pause withholds from the caller: the call each request names, and every
|
|
110
|
-
* `mcp__<label>__connect` call for a pending connector (the model may call it more than once; one
|
|
111
|
-
* request stands for all of them).
|
|
112
|
-
*/
|
|
113
|
-
function connectCallIds(output, connections) {
|
|
114
|
-
const connectNames = new Set(connections.map((c) => `mcp__${c.serverLabel}__connect`));
|
|
115
|
-
const ids = new Set();
|
|
116
|
-
for (const item of output) {
|
|
117
|
-
if (item.type === "backbone:connector_auth_request" && (item.status === undefined || item.status === "in_progress")) {
|
|
118
|
-
ids.add(String(item.call_id));
|
|
119
|
-
}
|
|
120
|
-
else if (item.type === "function_call" && connectNames.has(String(item.name))) {
|
|
121
|
-
ids.add(String(item.call_id));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return ids;
|
|
125
|
-
}
|
|
126
|
-
function connectionPhrase(c) {
|
|
127
|
-
const target = `${c.serverLabel} (${c.host})`;
|
|
128
|
-
if (c.reason === "allow")
|
|
129
|
-
return `allow the agent to use ${target}`;
|
|
130
|
-
if (c.reason === "reconnect")
|
|
131
|
-
return `reconnect ${target}`;
|
|
132
|
-
return `connect ${target}`;
|
|
133
|
-
}
|
|
134
96
|
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
135
97
|
export function connectionsSentence(connections) {
|
|
136
|
-
const
|
|
137
|
-
const joined = phrases.length > 1 ? `${phrases.slice(0, -1).join(", ")} and ${phrases.at(-1)}` : phrases[0] ?? "";
|
|
98
|
+
const joined = connectionsPhrase(connections);
|
|
138
99
|
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
|
139
100
|
}
|
|
140
101
|
/** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
|
|
141
102
|
const THEN_RUN = ", then run: ";
|
|
103
|
+
/**
|
|
104
|
+
* The `decide` command that answers a relay pause (#671 D7): one `--output <callId>=@<file>` per pending call,
|
|
105
|
+
* plus `--approve-all` when approvals wait too. The call id is chosen by the model, so the suggested file name
|
|
106
|
+
* keeps only `[A-Za-z0-9_-]` (P2) and never repeats: it can be neither `../…` nor an absolute path.
|
|
107
|
+
*/
|
|
108
|
+
function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
109
|
+
const used = new Set();
|
|
110
|
+
const outputs = calls.map((c) => {
|
|
111
|
+
const safe = c.callId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
112
|
+
let file = safe;
|
|
113
|
+
// Compared without case: on Windows and macOS `callA.out` and `calla.out` are one file.
|
|
114
|
+
for (let n = 2; used.has(file.toLowerCase()); n++)
|
|
115
|
+
file = `${safe}-${n}`;
|
|
116
|
+
used.add(file.toLowerCase());
|
|
117
|
+
return ` --output ${shellQuote(`${c.callId}=@${file}.out`)}`;
|
|
118
|
+
});
|
|
119
|
+
return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
|
|
120
|
+
(withApprovals ? " --approve-all" : ""));
|
|
121
|
+
}
|
|
142
122
|
/**
|
|
143
123
|
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
144
124
|
* connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
|
|
@@ -185,8 +165,15 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
185
165
|
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
186
166
|
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
187
167
|
}));
|
|
188
|
-
|
|
189
|
-
const
|
|
168
|
+
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
169
|
+
const connectRequests = pendingConnectionsOf(output);
|
|
170
|
+
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
171
|
+
serverLabel,
|
|
172
|
+
host,
|
|
173
|
+
reason,
|
|
174
|
+
...(destinations ? { destinations } : {}),
|
|
175
|
+
}));
|
|
176
|
+
const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
|
|
190
177
|
const toolCalls = [];
|
|
191
178
|
const pendingToolCalls = [];
|
|
192
179
|
for (const item of output) {
|
|
@@ -225,8 +212,16 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
225
212
|
if (status === "requires_approval" && ref && responseId) {
|
|
226
213
|
next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
|
|
227
214
|
}
|
|
228
|
-
else if (status === "requires_tool_output" &&
|
|
229
|
-
//
|
|
215
|
+
else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
|
|
216
|
+
// The engine answers the connect call itself on the continuation, so connecting first is enough.
|
|
217
|
+
if (ref && responseId) {
|
|
218
|
+
const decide = relayDecideCommand(ref, responseId, pendingToolCalls, pendingApprovals.length > 0);
|
|
219
|
+
next = pendingConnections.length > 0
|
|
220
|
+
? `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}${decide}`
|
|
221
|
+
: decide;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
230
225
|
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
231
226
|
const resume = ref
|
|
232
227
|
? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
|
|
@@ -272,19 +267,32 @@ export function printRunText(env) {
|
|
|
272
267
|
console.log(chalk.dim(`\n── ${s(head.join(" · "))}`));
|
|
273
268
|
if (env.toolCalls.length)
|
|
274
269
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
270
|
+
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
271
|
+
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
272
|
+
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
273
|
+
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
274
|
+
if (a.reason)
|
|
275
|
+
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
276
|
+
});
|
|
275
277
|
if (env.status === "requires_approval") {
|
|
276
278
|
console.log(chalk.yellow("\nPaused for approval:"));
|
|
277
|
-
|
|
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
|
-
});
|
|
279
|
+
printApprovals();
|
|
284
280
|
if (env.next)
|
|
285
281
|
console.log(`\nDecide with:\n ${s(env.next)}`);
|
|
286
282
|
}
|
|
287
283
|
else if (env.status === "requires_tool_output") {
|
|
284
|
+
if (env.pendingToolCalls.length) {
|
|
285
|
+
console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
|
|
286
|
+
env.pendingToolCalls.forEach((c) => {
|
|
287
|
+
console.log(` - ${s(c.tool)} (${s(c.callId)})`);
|
|
288
|
+
console.log(chalk.dim(` ${s(JSON.stringify(c.arguments))}`));
|
|
289
|
+
});
|
|
290
|
+
// The printed command carries --approve-all, so show what it approves (#671 P3).
|
|
291
|
+
if (env.pendingApprovals.length) {
|
|
292
|
+
console.log(chalk.yellow("\nIt also waits for approval:"));
|
|
293
|
+
printApprovals();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
288
296
|
if (env.pendingConnections.length) {
|
|
289
297
|
console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
|
|
290
298
|
env.pendingConnections.forEach((c) => {
|
|
@@ -296,9 +304,8 @@ export function printRunText(env) {
|
|
|
296
304
|
if (env.next)
|
|
297
305
|
console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
|
|
298
306
|
}
|
|
299
|
-
if (env.pendingToolCalls.length) {
|
|
300
|
-
console.log(
|
|
301
|
-
env.pendingToolCalls.forEach((c) => console.log(` - ${s(c.tool)} (${s(c.callId)})`));
|
|
307
|
+
else if (env.pendingToolCalls.length && env.next) {
|
|
308
|
+
console.log(`\nAnswer with:\n ${s(env.next)}`);
|
|
302
309
|
}
|
|
303
310
|
if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
|
|
304
311
|
console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -52,12 +52,12 @@ export declare const DEFAULT_BASE_URL: string;
|
|
|
52
52
|
*/
|
|
53
53
|
export declare const DEFAULT_AUTH_URL: string;
|
|
54
54
|
export declare function defaultAuthUrlFor(baseUrl: string): string;
|
|
55
|
-
|
|
56
|
-
export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
55
|
+
export { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
57
56
|
/**
|
|
58
57
|
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
59
58
|
* (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
|
|
60
|
-
* does not know; a trailing slash is dropped so callers can append a path.
|
|
59
|
+
* does not know; a trailing slash is dropped so callers can append a path. The mapping itself
|
|
60
|
+
* is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
|
|
61
61
|
*/
|
|
62
62
|
export declare function chatUrlFor(baseUrl: string, env?: NodeJS.ProcessEnv): string;
|
|
63
63
|
declare const store: Conf<BackboneConfigStore>;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { chatOriginFor } from "./connect-pause.js";
|
|
4
5
|
/**
|
|
5
6
|
* Fallback base URL when the user has no context, env var, or local config.
|
|
6
7
|
* Override at runtime by setting AI_2KW_DEFAULT_BASE_URL (or the legacy
|
|
@@ -36,33 +37,18 @@ export function defaultAuthUrlFor(baseUrl) {
|
|
|
36
37
|
return DEFAULT_AUTH_URL;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
41
|
-
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
42
|
-
const CHAT_URL_BY_API_HOST = {
|
|
43
|
-
"api.2kw.ai": "https://chat.2kw.ai",
|
|
44
|
-
"api-dev.2kw.ai": "https://chat-dev.2kw.ai",
|
|
45
|
-
"backbone.manfred-kunze.dev": "https://chat.2kw.ai",
|
|
46
|
-
"localhost:8080": "http://localhost:3000",
|
|
47
|
-
"127.0.0.1:8080": "http://localhost:3000",
|
|
48
|
-
};
|
|
40
|
+
export { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
49
41
|
/**
|
|
50
42
|
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
51
43
|
* (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
|
|
52
|
-
* does not know; a trailing slash is dropped so callers can append a path.
|
|
44
|
+
* does not know; a trailing slash is dropped so callers can append a path. The mapping itself
|
|
45
|
+
* is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
|
|
53
46
|
*/
|
|
54
47
|
export function chatUrlFor(baseUrl, env = process.env) {
|
|
55
48
|
const override = env.AI_2KW_CHAT_URL?.trim();
|
|
56
49
|
if (override)
|
|
57
50
|
return override.replace(/\/+$/, "");
|
|
58
|
-
|
|
59
|
-
const host = new URL(baseUrl).host;
|
|
60
|
-
// hasOwn, as in defaultAuthUrlFor: a host named after an Object.prototype key must not resolve.
|
|
61
|
-
return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] : DEFAULT_CHAT_URL;
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return DEFAULT_CHAT_URL;
|
|
65
|
-
}
|
|
51
|
+
return chatOriginFor(baseUrl);
|
|
66
52
|
}
|
|
67
53
|
// One-shot flag so we only print the deprecation warning once per process,
|
|
68
54
|
// even if resolveConfig is called multiple times across commands.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
3
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
4
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
5
|
+
*
|
|
6
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
7
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
11
|
+
*
|
|
12
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
14
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
17
|
+
export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
18
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
19
|
+
export declare const CHAT_URL_BY_API_HOST: Readonly<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
22
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
23
|
+
*/
|
|
24
|
+
export declare function chatOriginFor(baseUrl: unknown): string;
|
|
25
|
+
/** What the user is asked to do: connect, allow the agent, or reconnect. */
|
|
26
|
+
export type ConnectReason = "connect" | "allow" | "reconnect";
|
|
27
|
+
export declare const CONNECT_REASONS: readonly ConnectReason[];
|
|
28
|
+
/**
|
|
29
|
+
* One open connect request. `id` (`cauth_<call id>`) and `callId` (the first connect call for
|
|
30
|
+
* this connector) are what a client needs to withhold the connect calls; the CLI and the MCP
|
|
31
|
+
* server drop both from what they publish.
|
|
32
|
+
*/
|
|
33
|
+
export interface PendingConnection {
|
|
34
|
+
id: string;
|
|
35
|
+
callId: string;
|
|
36
|
+
serverLabel: string;
|
|
37
|
+
/** The MCP server's host: what the user is asked to connect to. */
|
|
38
|
+
host: string;
|
|
39
|
+
reason: ConnectReason;
|
|
40
|
+
/** The egress difference an allow is asked again for (#807 R9); absent otherwise, never empty. */
|
|
41
|
+
destinations?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Enough of a connection to name it in a sentence. */
|
|
44
|
+
export interface ConnectTarget {
|
|
45
|
+
serverLabel: string;
|
|
46
|
+
host: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
50
|
+
export declare function connectToolName(serverLabel: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
53
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
54
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
55
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
56
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pendingConnectionsOf(output: unknown): PendingConnection[];
|
|
59
|
+
/**
|
|
60
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
61
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
62
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
63
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
64
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
65
|
+
*/
|
|
66
|
+
export declare function connectCallIds(output: unknown, connections: readonly Pick<PendingConnection, "callId" | "serverLabel">[]): Set<string>;
|
|
67
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
68
|
+
export declare function connectionPhrase(c: ConnectTarget): string;
|
|
69
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
70
|
+
export declare function connectionsPhrase(connections: readonly ConnectTarget[]): string;
|
|
71
|
+
//# sourceMappingURL=connect-pause.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
3
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
4
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
5
|
+
*
|
|
6
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
7
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
11
|
+
*
|
|
12
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
14
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
17
|
+
export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
18
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
19
|
+
export const CHAT_URL_BY_API_HOST = {
|
|
20
|
+
"api.2kw.ai": "https://chat.2kw.ai",
|
|
21
|
+
"api-dev.2kw.ai": "https://chat-dev.2kw.ai",
|
|
22
|
+
"backbone.manfred-kunze.dev": "https://chat.2kw.ai",
|
|
23
|
+
"localhost:8080": "http://localhost:3000",
|
|
24
|
+
"127.0.0.1:8080": "http://localhost:3000",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
28
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
29
|
+
*/
|
|
30
|
+
export function chatOriginFor(baseUrl) {
|
|
31
|
+
try {
|
|
32
|
+
const host = new URL(String(baseUrl ?? "")).host;
|
|
33
|
+
// hasOwn, not a bare index: a host named after an Object.prototype key must not resolve.
|
|
34
|
+
return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] ?? DEFAULT_CHAT_URL : DEFAULT_CHAT_URL;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return DEFAULT_CHAT_URL;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const CONNECT_REASONS = ["connect", "allow", "reconnect"];
|
|
41
|
+
const CONNECT_REQUEST = "backbone:connector_auth_request";
|
|
42
|
+
function itemsOf(output) {
|
|
43
|
+
if (!Array.isArray(output))
|
|
44
|
+
return [];
|
|
45
|
+
return output.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
46
|
+
}
|
|
47
|
+
function text(value) {
|
|
48
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
51
|
+
export function connectToolName(serverLabel) {
|
|
52
|
+
return `mcp__${serverLabel}__connect`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
56
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
57
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
58
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
59
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
60
|
+
*/
|
|
61
|
+
export function pendingConnectionsOf(output) {
|
|
62
|
+
const items = itemsOf(output);
|
|
63
|
+
const resolved = new Set();
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const id = text(item.id);
|
|
66
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
67
|
+
resolved.add(id);
|
|
68
|
+
}
|
|
69
|
+
const pending = new Map();
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
72
|
+
continue;
|
|
73
|
+
const id = text(item.id);
|
|
74
|
+
const callId = text(item.call_id);
|
|
75
|
+
const serverLabel = text(item.server_label);
|
|
76
|
+
const host = text(item.host);
|
|
77
|
+
if (!id || !callId || !serverLabel || !host || resolved.has(id) || pending.has(id))
|
|
78
|
+
continue;
|
|
79
|
+
const reason = text(item.reason);
|
|
80
|
+
const destinations = Array.isArray(item.destinations)
|
|
81
|
+
? item.destinations.filter((entry) => typeof entry === "string")
|
|
82
|
+
: [];
|
|
83
|
+
pending.set(id, {
|
|
84
|
+
id,
|
|
85
|
+
callId,
|
|
86
|
+
serverLabel,
|
|
87
|
+
host,
|
|
88
|
+
reason: reason && CONNECT_REASONS.includes(reason) ? reason : "connect",
|
|
89
|
+
...(destinations.length > 0 ? { destinations } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return [...pending.values()];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
96
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
97
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
98
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
99
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
100
|
+
*/
|
|
101
|
+
export function connectCallIds(output, connections) {
|
|
102
|
+
const names = new Set(connections.map((c) => connectToolName(c.serverLabel)));
|
|
103
|
+
const ids = new Set(connections.map((c) => c.callId));
|
|
104
|
+
const items = itemsOf(output);
|
|
105
|
+
// Every open connect request's own call id is withheld, even one pendingConnectionsOf could
|
|
106
|
+
// not fully describe (a missing server_label or host): the paired function_call must never be
|
|
107
|
+
// left for the caller to fabricate an output for (R7).
|
|
108
|
+
const resolved = new Set();
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const id = text(item.id);
|
|
111
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
112
|
+
resolved.add(id);
|
|
113
|
+
}
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
116
|
+
continue;
|
|
117
|
+
const id = text(item.id);
|
|
118
|
+
if (id && resolved.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
const callId = text(item.call_id);
|
|
121
|
+
if (callId)
|
|
122
|
+
ids.add(callId);
|
|
123
|
+
}
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const callId = text(item.call_id);
|
|
126
|
+
if (item.type === "function_call" && callId && typeof item.name === "string" && names.has(item.name))
|
|
127
|
+
ids.add(callId);
|
|
128
|
+
}
|
|
129
|
+
return ids;
|
|
130
|
+
}
|
|
131
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
132
|
+
export function connectionPhrase(c) {
|
|
133
|
+
const target = `${c.serverLabel} (${c.host})`;
|
|
134
|
+
if (c.reason === "allow")
|
|
135
|
+
return `allow the agent to use ${target}`;
|
|
136
|
+
if (c.reason === "reconnect")
|
|
137
|
+
return `reconnect ${target}`;
|
|
138
|
+
return `connect ${target}`;
|
|
139
|
+
}
|
|
140
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
141
|
+
export function connectionsPhrase(connections) {
|
|
142
|
+
const phrases = connections.map(connectionPhrase);
|
|
143
|
+
if (phrases.length < 2)
|
|
144
|
+
return phrases[0] ?? "";
|
|
145
|
+
return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=connect-pause.js.map
|
package/dist/lib/errors.js
CHANGED
|
@@ -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
|
|
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
|
|
3
|
+
"version": "6.3.0",
|
|
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",
|