@2kw/ai 7.0.0-dev.4 → 7.0.0-dev.8
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.
|
@@ -124,7 +124,7 @@ async function finishRun(command, client, target, first, opts) {
|
|
|
124
124
|
}
|
|
125
125
|
export function makeAgentRunCommand() {
|
|
126
126
|
return new Command("run")
|
|
127
|
-
.description("Run an agent on a task (exit 3 = paused for approval, 4 = needs client tool output
|
|
127
|
+
.description("Run an agent on a task (exit 3 = paused for approval, 4 = needs client tool output, a connector connected in chat or your answer in chat, 5 = incomplete)")
|
|
128
128
|
.argument("<agent>", "Agent name or id, optionally with @label and #model")
|
|
129
129
|
.argument("[input]", "Task text (or use --input-file, or pipe it on stdin); optional with --continue")
|
|
130
130
|
.option("--input-file <path>", "Read the task text from a file")
|
package/dist/commands/billing.js
CHANGED
|
@@ -4,6 +4,8 @@ import { isJsonOutput } from "../lib/config.js";
|
|
|
4
4
|
import { CliUsageError } from "../lib/errors.js";
|
|
5
5
|
import { formatDetail, formatList } from "../lib/output.js";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
+
/** What human output shows for a null tier: an organisation without a plan (#1356). */
|
|
8
|
+
const NO_PLAN = "no plan";
|
|
7
9
|
export function makeBillingCommand() {
|
|
8
10
|
const cmd = new Command("billing").description("Billing and subscription management");
|
|
9
11
|
cmd
|
|
@@ -23,7 +25,9 @@ export function makeBillingCommand() {
|
|
|
23
25
|
await runAction(command, async () => {
|
|
24
26
|
const client = getClient(command);
|
|
25
27
|
const { data } = await client.GET("/v1/billing/tier");
|
|
26
|
-
|
|
28
|
+
// A null name is an organisation without a plan (#1356); --json keeps the raw null.
|
|
29
|
+
const shown = data && data.name == null && !isJsonOutput(command) ? { ...data, name: NO_PLAN } : data;
|
|
30
|
+
formatDetail(shown, command);
|
|
27
31
|
});
|
|
28
32
|
});
|
|
29
33
|
cmd
|
|
@@ -37,7 +41,7 @@ export function makeBillingCommand() {
|
|
|
37
41
|
console.log(JSON.stringify(data, null, 2));
|
|
38
42
|
}
|
|
39
43
|
else {
|
|
40
|
-
console.log(chalk.cyan(`Tier: ${data?.tier ??
|
|
44
|
+
console.log(chalk.cyan(`Tier: ${data?.tier ?? NO_PLAN}`));
|
|
41
45
|
console.log();
|
|
42
46
|
if (data?.resources?.length) {
|
|
43
47
|
console.log(chalk.bold("Resources:"));
|
package/dist/lib/agent-run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { components } from "../generated/openapi.js";
|
|
2
|
+
import { type InputMode } from "./connect-pause.js";
|
|
2
3
|
type AnyRecord = Record<string, any>;
|
|
3
4
|
export type RunStatus = "completed" | "requires_approval" | "requires_tool_output" | "incomplete";
|
|
4
5
|
/** A built-in SkillsApply approval's preview (#781, spec §6); the generated type, fields all optional. */
|
|
@@ -39,6 +40,19 @@ export interface PendingConnection {
|
|
|
39
40
|
/** The egress difference an allow is re-asked for; absent otherwise. */
|
|
40
41
|
destinations?: string[];
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A connector's question the run waits on (#1320, connectors spec §12): an open
|
|
45
|
+
* `backbone:input_request`. Only the user who started the run answers it, and only in chat, at
|
|
46
|
+
* `answerUrl`; the envelope never carries the question, its form or its link (D7).
|
|
47
|
+
*/
|
|
48
|
+
export interface PendingInput {
|
|
49
|
+
inputRequestId: string;
|
|
50
|
+
serverLabel: string;
|
|
51
|
+
tool: string;
|
|
52
|
+
/** `form`, `url`, `mixed` (both) or `unknown`: what the user does in chat. */
|
|
53
|
+
mode: InputMode;
|
|
54
|
+
answerUrl: string;
|
|
55
|
+
}
|
|
42
56
|
/** The end user's conversation mode (epic &59); the server matches these three values exactly. */
|
|
43
57
|
export type ConversationMode = "plan" | "ask" | "auto";
|
|
44
58
|
export declare const CONVERSATION_MODES: readonly ConversationMode[];
|
|
@@ -72,6 +86,7 @@ export interface RunEnvelope {
|
|
|
72
86
|
pendingApprovals: PendingApproval[];
|
|
73
87
|
pendingToolCalls: PendingToolCall[];
|
|
74
88
|
pendingConnections: PendingConnection[];
|
|
89
|
+
pendingInputs: PendingInput[];
|
|
75
90
|
incompleteReason: string | null;
|
|
76
91
|
usage: {
|
|
77
92
|
inputTokens: number;
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -1,6 +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
|
+
import { answerUrlFor, connectCallIds, connectionsPhrase, inputLabelsPhrase, pendingConnectionsOf, pendingInputsOf, } from "./connect-pause.js";
|
|
4
4
|
import { CliUsageError } from "./errors.js";
|
|
5
5
|
import { previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
6
6
|
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
@@ -182,6 +182,7 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
182
182
|
"function_call_output",
|
|
183
183
|
"backbone:approval_request",
|
|
184
184
|
"backbone:connector_auth_request",
|
|
185
|
+
"backbone:input_request",
|
|
185
186
|
"backbone:tool_image",
|
|
186
187
|
"mcp_approval_request",
|
|
187
188
|
"mcp_call",
|
|
@@ -230,7 +231,22 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
230
231
|
reason,
|
|
231
232
|
...(destinations ? { destinations } : {}),
|
|
232
233
|
}));
|
|
233
|
-
|
|
234
|
+
// A connector's question (#1320): answered in chat only, so the envelope names where, never what (D7).
|
|
235
|
+
const inputRequests = pendingInputsOf(output);
|
|
236
|
+
const answerUrl = answerUrlFor(chatUrl, result?.conversation?.id ?? null);
|
|
237
|
+
const pendingInputs = inputRequests.map(({ id, serverLabel, tool, mode }) => ({
|
|
238
|
+
inputRequestId: id,
|
|
239
|
+
serverLabel,
|
|
240
|
+
tool,
|
|
241
|
+
mode,
|
|
242
|
+
answerUrl,
|
|
243
|
+
}));
|
|
244
|
+
const withheldCallIds = new Set([
|
|
245
|
+
...pendingApprovals.map((a) => a.callId),
|
|
246
|
+
...connectCallIds(output, connectRequests),
|
|
247
|
+
// An awaiting call has no function_call item (spec §5); withheld all the same, never the caller's to answer.
|
|
248
|
+
...inputRequests.map((i) => i.callId),
|
|
249
|
+
]);
|
|
234
250
|
const toolCalls = [];
|
|
235
251
|
const pendingToolCalls = [];
|
|
236
252
|
for (const item of output) {
|
|
@@ -254,9 +270,10 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
254
270
|
status = "incomplete";
|
|
255
271
|
break;
|
|
256
272
|
case "requires_action":
|
|
257
|
-
// A connect pause keeps exit 4 (§8, CLI agents D14): the CLI cannot answer it either
|
|
273
|
+
// A connect pause keeps exit 4 (§8, CLI agents D14): the CLI cannot answer it either, nor an
|
|
274
|
+
// input pause, which outranks every other (#1320, P91).
|
|
258
275
|
status =
|
|
259
|
-
pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
276
|
+
pendingInputs.length > 0 || pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
260
277
|
? "requires_tool_output"
|
|
261
278
|
: "requires_approval";
|
|
262
279
|
break;
|
|
@@ -266,7 +283,14 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
266
283
|
const responseId = result?.id ?? null;
|
|
267
284
|
const ref = agentRef ?? agent;
|
|
268
285
|
let next = null;
|
|
269
|
-
if (
|
|
286
|
+
if (pendingInputs.length > 0) {
|
|
287
|
+
// Only the requester's continuation resolves the response (spec D9), and only chat sends one: no
|
|
288
|
+
// command here can continue it, whatever else waits (#1320, P91).
|
|
289
|
+
const labels = inputLabelsPhrase(pendingInputs);
|
|
290
|
+
const needs = new Set(pendingInputs.map((i) => i.serverLabel)).size > 1 ? "need" : "needs";
|
|
291
|
+
next = `${labels} ${needs} your input: answer in chat at ${answerUrl}. The run continues there.`;
|
|
292
|
+
}
|
|
293
|
+
else if (status === "requires_approval" && ref && responseId) {
|
|
270
294
|
next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
|
|
271
295
|
}
|
|
272
296
|
else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
|
|
@@ -310,6 +334,7 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
310
334
|
pendingApprovals,
|
|
311
335
|
pendingToolCalls,
|
|
312
336
|
pendingConnections,
|
|
337
|
+
pendingInputs,
|
|
313
338
|
incompleteReason: result?.incomplete_details?.reason ?? null,
|
|
314
339
|
usage: result?.usage
|
|
315
340
|
? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
|
|
@@ -317,6 +342,13 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
317
342
|
next,
|
|
318
343
|
};
|
|
319
344
|
}
|
|
345
|
+
/** What the user does in chat for each input mode, in {@link printRunText}'s words. */
|
|
346
|
+
const INPUT_MODE_WORDS = {
|
|
347
|
+
form: "form",
|
|
348
|
+
url: "link",
|
|
349
|
+
mixed: "form and link",
|
|
350
|
+
unknown: "question",
|
|
351
|
+
};
|
|
320
352
|
/** Human-readable rendering of an envelope on stdout. Every value printed is passed through {@link stripControl}. */
|
|
321
353
|
export function printRunText(env) {
|
|
322
354
|
const s = stripControl;
|
|
@@ -351,6 +383,21 @@ export function printRunText(env) {
|
|
|
351
383
|
if (env.next)
|
|
352
384
|
console.log(`\nDecide with:\n ${s(env.next)}`);
|
|
353
385
|
}
|
|
386
|
+
else if (env.status === "requires_tool_output" && env.pendingInputs.length) {
|
|
387
|
+
// A connector's question outranks every other pause (#1320, P91): it is answered in chat, and that
|
|
388
|
+
// one answer also carries the rest, so no command is printed for anything else.
|
|
389
|
+
console.log(chalk.yellow("\nPaused: the agent needs your answer in chat:"));
|
|
390
|
+
env.pendingInputs.forEach((i) => console.log(` - ${s(i.serverLabel)}: ${s(i.tool)} (${INPUT_MODE_WORDS[i.mode]})`));
|
|
391
|
+
const others = [
|
|
392
|
+
env.pendingApprovals.length ? `${env.pendingApprovals.length} approval(s)` : null,
|
|
393
|
+
env.pendingConnections.length ? `${env.pendingConnections.length} connection(s)` : null,
|
|
394
|
+
env.pendingToolCalls.length ? `${env.pendingToolCalls.length} client-side tool call(s)` : null,
|
|
395
|
+
].filter(Boolean);
|
|
396
|
+
if (others.length)
|
|
397
|
+
console.log(chalk.dim(` It also waits for ${others.join(", ")}: answer them in chat too.`));
|
|
398
|
+
if (env.next)
|
|
399
|
+
console.log(`\n${s(env.next)}`);
|
|
400
|
+
}
|
|
354
401
|
else if (env.status === "requires_tool_output") {
|
|
355
402
|
if (env.pendingToolCalls.length) {
|
|
356
403
|
console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
8
|
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
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.
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it. The input pause
|
|
11
|
+
* (#1320, connectors spec §12) lives here too, with its own fixture `input-pause-cases.json`.
|
|
11
12
|
*
|
|
12
13
|
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
14
|
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
@@ -68,4 +69,37 @@ export declare function connectCallIds(output: unknown, connections: readonly Pi
|
|
|
68
69
|
export declare function connectionPhrase(c: ConnectTarget): string;
|
|
69
70
|
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
70
71
|
export declare function connectionsPhrase(connections: readonly ConnectTarget[]): string;
|
|
72
|
+
/**
|
|
73
|
+
* What an open input request asks for: a form, a page to open, both (`mixed`), or neither of the
|
|
74
|
+
* modes this client knows (`unknown`). It names no URL: the question stays in chat (D7).
|
|
75
|
+
*/
|
|
76
|
+
export type InputMode = "form" | "url" | "mixed" | "unknown";
|
|
77
|
+
/**
|
|
78
|
+
* One open `backbone:input_request` (#1320, connectors spec §12): a connector's question that only
|
|
79
|
+
* the requester can answer, and only in chat. `id` is the item's `inreq_…` id. Its message, schema
|
|
80
|
+
* and URL are never read: no assistant may see them (D7).
|
|
81
|
+
*/
|
|
82
|
+
export interface PendingInput {
|
|
83
|
+
id: string;
|
|
84
|
+
callId: string;
|
|
85
|
+
serverLabel: string;
|
|
86
|
+
/** The tool's own name, without the `mcp__<label>__` prefix. */
|
|
87
|
+
tool: string;
|
|
88
|
+
mode: InputMode;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The questions a response's `output` still waits on, in order, by the chat's own rule
|
|
92
|
+
* (`surface/src/core/chat.ts`, so a link never points at a question the chat would not show): an
|
|
93
|
+
* `in_progress` request whose id no other-status projection and no `backbone:input_response`
|
|
94
|
+
* resolved, and only the newest open round of its call. An item without a non-empty `id`,
|
|
95
|
+
* `call_id`, `server_label` or `name`, or without one request entry that has a `key`, is skipped.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pendingInputsOf(output: unknown): PendingInput[];
|
|
98
|
+
/**
|
|
99
|
+
* Where the user answers: the chat's slug-less conversation link (`<chat>/c/<id>`, the path
|
|
100
|
+
* `surface/src/hosts/web/routes.ts` `answerPath` builds), or the chat itself without a conversation.
|
|
101
|
+
*/
|
|
102
|
+
export declare function answerUrlFor(chatOrigin: string, conversationId: string | null | undefined): string;
|
|
103
|
+
/** "erp", "erp and crm", "erp, crm and drive": each connector that asks, once; empty for none. */
|
|
104
|
+
export declare function inputLabelsPhrase(inputs: readonly Pick<PendingInput, "serverLabel">[]): string;
|
|
71
105
|
//# sourceMappingURL=connect-pause.d.ts.map
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
8
|
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
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.
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it. The input pause
|
|
11
|
+
* (#1320, connectors spec §12) lives here too, with its own fixture `input-pause-cases.json`.
|
|
11
12
|
*
|
|
12
13
|
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
14
|
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
@@ -144,4 +145,77 @@ export function connectionsPhrase(connections) {
|
|
|
144
145
|
return phrases[0] ?? "";
|
|
145
146
|
return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
|
|
146
147
|
}
|
|
148
|
+
const INPUT_REQUEST = "backbone:input_request";
|
|
149
|
+
const INPUT_RESPONSE = "backbone:input_response";
|
|
150
|
+
function roundOf(value) {
|
|
151
|
+
return typeof value === "number" && Number.isInteger(value) ? value : 1;
|
|
152
|
+
}
|
|
153
|
+
function inputModeOf(requests) {
|
|
154
|
+
const modes = new Set(requests.map((entry) => entry.mode).filter((mode) => mode === "form" || mode === "url"));
|
|
155
|
+
if (modes.size === 2)
|
|
156
|
+
return "mixed";
|
|
157
|
+
if (modes.has("form"))
|
|
158
|
+
return "form";
|
|
159
|
+
return modes.has("url") ? "url" : "unknown";
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The questions a response's `output` still waits on, in order, by the chat's own rule
|
|
163
|
+
* (`surface/src/core/chat.ts`, so a link never points at a question the chat would not show): an
|
|
164
|
+
* `in_progress` request whose id no other-status projection and no `backbone:input_response`
|
|
165
|
+
* resolved, and only the newest open round of its call. An item without a non-empty `id`,
|
|
166
|
+
* `call_id`, `server_label` or `name`, or without one request entry that has a `key`, is skipped.
|
|
167
|
+
*/
|
|
168
|
+
export function pendingInputsOf(output) {
|
|
169
|
+
const items = itemsOf(output);
|
|
170
|
+
const resolved = new Set();
|
|
171
|
+
const newestRound = new Map();
|
|
172
|
+
for (const item of items) {
|
|
173
|
+
if (item.type === INPUT_REQUEST) {
|
|
174
|
+
const id = text(item.id);
|
|
175
|
+
if (id && item.status !== "in_progress")
|
|
176
|
+
resolved.add(id);
|
|
177
|
+
const callId = text(item.call_id);
|
|
178
|
+
if (callId && item.status === "in_progress") {
|
|
179
|
+
newestRound.set(callId, Math.max(newestRound.get(callId) ?? 0, roundOf(item.round)));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (item.type === INPUT_RESPONSE) {
|
|
183
|
+
const id = text(item.input_request_id);
|
|
184
|
+
if (id)
|
|
185
|
+
resolved.add(id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const pending = new Map();
|
|
189
|
+
for (const item of items) {
|
|
190
|
+
if (item.type !== INPUT_REQUEST || item.status !== "in_progress")
|
|
191
|
+
continue;
|
|
192
|
+
const id = text(item.id);
|
|
193
|
+
const callId = text(item.call_id);
|
|
194
|
+
const serverLabel = text(item.server_label);
|
|
195
|
+
const tool = text(item.name);
|
|
196
|
+
if (!id || !callId || !serverLabel || !tool || resolved.has(id) || pending.has(id))
|
|
197
|
+
continue;
|
|
198
|
+
if (roundOf(item.round) !== newestRound.get(callId))
|
|
199
|
+
continue;
|
|
200
|
+
const requests = itemsOf(item.requests).filter((entry) => text(entry.key) !== undefined);
|
|
201
|
+
if (requests.length === 0)
|
|
202
|
+
continue;
|
|
203
|
+
pending.set(id, { id, callId, serverLabel, tool, mode: inputModeOf(requests) });
|
|
204
|
+
}
|
|
205
|
+
return [...pending.values()];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Where the user answers: the chat's slug-less conversation link (`<chat>/c/<id>`, the path
|
|
209
|
+
* `surface/src/hosts/web/routes.ts` `answerPath` builds), or the chat itself without a conversation.
|
|
210
|
+
*/
|
|
211
|
+
export function answerUrlFor(chatOrigin, conversationId) {
|
|
212
|
+
return conversationId ? `${chatOrigin}/c/${encodeURIComponent(conversationId)}` : `${chatOrigin}/`;
|
|
213
|
+
}
|
|
214
|
+
/** "erp", "erp and crm", "erp, crm and drive": each connector that asks, once; empty for none. */
|
|
215
|
+
export function inputLabelsPhrase(inputs) {
|
|
216
|
+
const labels = [...new Set(inputs.map((input) => input.serverLabel))];
|
|
217
|
+
if (labels.length < 2)
|
|
218
|
+
return labels[0] ?? "";
|
|
219
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
220
|
+
}
|
|
147
221
|
//# sourceMappingURL=connect-pause.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.8",
|
|
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",
|