@2kw/ai 6.4.0-dev.7 → 7.0.0-dev.10
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.d.ts +31 -0
- package/dist/agent-config/schema.js +15 -0
- package/dist/agent-config/template.js +2 -0
- package/dist/commands/agent-run.js +1 -1
- package/dist/commands/analytics.js +1 -3
- package/dist/commands/billing.js +6 -2
- package/dist/commands/convert.js +13 -29
- package/dist/commands/experiments.js +20 -3
- package/dist/commands/files.js +7 -6
- package/dist/commands/providers.js +3 -12
- package/dist/commands/skills.js +6 -8
- package/dist/commands/transcribe.js +9 -27
- package/dist/lib/agent-run.d.ts +18 -0
- package/dist/lib/agent-run.js +136 -20
- package/dist/lib/approval-prompt.js +11 -4
- package/dist/lib/connect-pause.d.ts +35 -1
- package/dist/lib/connect-pause.js +75 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ kubectl-style contexts switch between organizations and environments:
|
|
|
110
110
|
| `tracing` | Inspect request traces and tracing settings |
|
|
111
111
|
| `memory` | Your agent memory files (list, read, write, delete, forget); member totals and erasure for admins |
|
|
112
112
|
| `providers` | Manage BYOK AI providers |
|
|
113
|
-
| `analytics` | Usage analytics:
|
|
113
|
+
| `analytics` | Usage analytics: operations, tokens, quality, providers, errors |
|
|
114
114
|
| `billing` | Check subscription tier and usage limits |
|
|
115
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 |
|
|
@@ -221,6 +221,25 @@ export declare const agentSchema: {
|
|
|
221
221
|
};
|
|
222
222
|
};
|
|
223
223
|
};
|
|
224
|
+
readonly bashTool: {
|
|
225
|
+
readonly type: "object";
|
|
226
|
+
readonly additionalProperties: false;
|
|
227
|
+
readonly required: readonly ["type"];
|
|
228
|
+
readonly properties: {
|
|
229
|
+
readonly type: {
|
|
230
|
+
readonly const: "backbone.bash";
|
|
231
|
+
};
|
|
232
|
+
readonly approval: {
|
|
233
|
+
readonly enum: readonly ["sandbox", "pause"];
|
|
234
|
+
};
|
|
235
|
+
readonly outputs: {
|
|
236
|
+
readonly enum: readonly ["immediate", "held"];
|
|
237
|
+
};
|
|
238
|
+
readonly annotations: {
|
|
239
|
+
readonly $ref: "#/$defs/annotations";
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
};
|
|
224
243
|
readonly functionTool: {
|
|
225
244
|
readonly type: "object";
|
|
226
245
|
readonly additionalProperties: false;
|
|
@@ -335,6 +354,18 @@ export declare const agentSchema: {
|
|
|
335
354
|
readonly then: {
|
|
336
355
|
readonly $ref: "#/$defs/skillTool";
|
|
337
356
|
};
|
|
357
|
+
}, {
|
|
358
|
+
readonly if: {
|
|
359
|
+
required: string[];
|
|
360
|
+
properties: {
|
|
361
|
+
type: {
|
|
362
|
+
const: string;
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
readonly then: {
|
|
367
|
+
readonly $ref: "#/$defs/bashTool";
|
|
368
|
+
};
|
|
338
369
|
}, {
|
|
339
370
|
readonly if: {
|
|
340
371
|
required: string[];
|
|
@@ -35,6 +35,7 @@ const BUILTIN_TYPES = [
|
|
|
35
35
|
"backbone.document_convert",
|
|
36
36
|
"backbone.todo",
|
|
37
37
|
"backbone.skill",
|
|
38
|
+
"backbone.bash",
|
|
38
39
|
"function",
|
|
39
40
|
];
|
|
40
41
|
const typeIs = (t) => ({ required: ["type"], properties: { type: { const: t } } });
|
|
@@ -161,6 +162,19 @@ export const agentSchema = {
|
|
|
161
162
|
skills: { type: "array", minItems: 1, maxItems: 20, uniqueItems: true, items: { type: "string", pattern: SKILL_REF } },
|
|
162
163
|
},
|
|
163
164
|
},
|
|
165
|
+
// The envelope BashToolEntryValidator accepts: no name, no requiresConfirmation, and none of
|
|
166
|
+
// the egress of the withdrawn per-agent network design (#925).
|
|
167
|
+
bashTool: {
|
|
168
|
+
type: "object",
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
required: ["type"],
|
|
171
|
+
properties: {
|
|
172
|
+
type: { const: "backbone.bash" },
|
|
173
|
+
approval: { enum: ["sandbox", "pause"] },
|
|
174
|
+
outputs: { enum: ["immediate", "held"] },
|
|
175
|
+
annotations: { $ref: "#/$defs/annotations" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
164
178
|
functionTool: {
|
|
165
179
|
type: "object",
|
|
166
180
|
additionalProperties: false,
|
|
@@ -194,6 +208,7 @@ export const agentSchema = {
|
|
|
194
208
|
{ if: typeIs("backbone.document_convert"), then: { $ref: "#/$defs/documentConvertTool" } },
|
|
195
209
|
{ if: typeIs("backbone.todo"), then: { $ref: "#/$defs/todoTool" } },
|
|
196
210
|
{ if: typeIs("backbone.skill"), then: { $ref: "#/$defs/skillTool" } },
|
|
211
|
+
{ if: typeIs("backbone.bash"), then: { $ref: "#/$defs/bashTool" } },
|
|
197
212
|
{ if: typeIs("function"), then: { $ref: "#/$defs/functionTool" } },
|
|
198
213
|
{
|
|
199
214
|
// The validator reports these with the reason from NON_AUTHORABLE_TOOL_TYPES; this keeps the published schema honest.
|
|
@@ -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")
|
|
@@ -8,7 +8,7 @@ function addDateOptions(cmd) {
|
|
|
8
8
|
}
|
|
9
9
|
export function makeAnalyticsCommand() {
|
|
10
10
|
const cmd = new Command("analytics").description("Organization analytics");
|
|
11
|
-
const summary = new Command("summary").description("Cross-surface summary (total ops,
|
|
11
|
+
const summary = new Command("summary").description("Cross-surface summary (total ops, tokens, p95, per-surface breakdown)");
|
|
12
12
|
addDateOptions(summary);
|
|
13
13
|
summary.action(async (opts, command) => {
|
|
14
14
|
await runAction(command, async () => {
|
|
@@ -66,7 +66,6 @@ export function makeAnalyticsCommand() {
|
|
|
66
66
|
"successfulExtractions",
|
|
67
67
|
"successRate",
|
|
68
68
|
"totalTokens",
|
|
69
|
-
"totalCost",
|
|
70
69
|
"avgDurationMs",
|
|
71
70
|
]);
|
|
72
71
|
});
|
|
@@ -89,7 +88,6 @@ export function makeAnalyticsCommand() {
|
|
|
89
88
|
"successfulExtractions",
|
|
90
89
|
"successRate",
|
|
91
90
|
"totalTokens",
|
|
92
|
-
"totalCost",
|
|
93
91
|
"avgDurationMs",
|
|
94
92
|
]);
|
|
95
93
|
});
|
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/commands/convert.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { getClient,
|
|
5
|
-
import {
|
|
6
|
-
import { BackboneApiError } from "../lib/errors.js";
|
|
4
|
+
import { getClient, runAction } from "../lib/client.js";
|
|
5
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
7
6
|
import { formatDetail, withSpinner } from "../lib/output.js";
|
|
8
7
|
import { fileToBlob, getMimeType } from "../lib/multipart.js";
|
|
9
8
|
function mapFormat(format) {
|
|
@@ -136,34 +135,19 @@ function buildFormData(paths, opts) {
|
|
|
136
135
|
formData.append("options", new Blob([JSON.stringify(pipelineOptions)], { type: "application/json" }));
|
|
137
136
|
return formData;
|
|
138
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* POST the multipart body through the typed client. The generated type describes `files` as string[],
|
|
140
|
+
* which FormData cannot match, so the placeholder body satisfies the type and bodySerializer sends the
|
|
141
|
+
* real parts; the client's auth and error middleware apply.
|
|
142
|
+
*/
|
|
139
143
|
async function multipartConvert(command, formData, endpoint, pipeline) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: `${baseUrl}${endpoint}`;
|
|
146
|
-
const res = await fetch(url, {
|
|
147
|
-
method: "POST",
|
|
148
|
-
headers: { Authorization: authHeader },
|
|
149
|
-
body: formData,
|
|
144
|
+
const client = getClient(command);
|
|
145
|
+
const { data } = await client.POST(endpoint, {
|
|
146
|
+
params: { query: { pipeline } },
|
|
147
|
+
body: { files: [] },
|
|
148
|
+
bodySerializer: () => formData,
|
|
150
149
|
});
|
|
151
|
-
|
|
152
|
-
let body;
|
|
153
|
-
try {
|
|
154
|
-
body = await res.json();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
body = {
|
|
158
|
-
error: res.statusText,
|
|
159
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
160
|
-
status: res.status,
|
|
161
|
-
timestamp: new Date().toISOString(),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
throw new BackboneApiError(body);
|
|
165
|
-
}
|
|
166
|
-
return (await res.json());
|
|
150
|
+
return data;
|
|
167
151
|
}
|
|
168
152
|
export function makeConvertCommand() {
|
|
169
153
|
const cmd = new Command("convert").description("Convert documents");
|
|
@@ -16,8 +16,18 @@ function formatComparisonCell(result) {
|
|
|
16
16
|
return chalk.red("ERROR");
|
|
17
17
|
const scores = result.scores ?? [];
|
|
18
18
|
if (scores.length > 0) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// A SKIP carries a placeholder 0.0 that is not a rating, and a comment-only
|
|
20
|
+
// score has no number; neither takes part in the average (#693).
|
|
21
|
+
const rated = scores.filter((s) => s.label !== "SKIP" && s.score != null);
|
|
22
|
+
if (rated.length > 0) {
|
|
23
|
+
return (rated.reduce((sum, s) => sum + s.score, 0) / rated.length).toFixed(2);
|
|
24
|
+
}
|
|
25
|
+
// A label-only verdict (#1163) has no number; show its label, as the console does.
|
|
26
|
+
const verdict = scores.find((s) => s.label && s.label !== "SKIP");
|
|
27
|
+
if (verdict)
|
|
28
|
+
return String(verdict.label);
|
|
29
|
+
if (scores.every((s) => s.label === "SKIP"))
|
|
30
|
+
return chalk.dim("skipped");
|
|
21
31
|
}
|
|
22
32
|
return result.durationMs != null ? `${result.durationMs}ms` : chalk.dim("done");
|
|
23
33
|
}
|
|
@@ -392,8 +402,14 @@ export function makeExperimentsCommand() {
|
|
|
392
402
|
console.log(chalk.dim(`Page ${(page.number ?? 0) + 1} of ${page.totalPages ?? 0} (${page.totalElements ?? 0} total)`));
|
|
393
403
|
}
|
|
394
404
|
if (aggregates.length > 0) {
|
|
405
|
+
const skippedOf = (agg) => Object.entries((agg.skippedEvaluatorCounts ?? {}))
|
|
406
|
+
.filter(([, n]) => n > 0)
|
|
407
|
+
.map(([evaluatorId, n]) => `${evaluatorId}: ${n}`)
|
|
408
|
+
.join(", ");
|
|
409
|
+
// Only when something was skipped, so a clean run keeps its narrow table (#693).
|
|
410
|
+
const showSkipped = aggregates.some((agg) => skippedOf(agg) !== "");
|
|
395
411
|
const aggTable = new Table({
|
|
396
|
-
head: ["Variant", "Items", "Avg Duration", "Avg Tokens", "Total Cost"].map((h) => chalk.cyan(h)),
|
|
412
|
+
head: ["Variant", "Items", "Avg Duration", "Avg Tokens", "Total Cost", ...(showSkipped ? ["Skipped"] : [])].map((h) => chalk.cyan(h)),
|
|
397
413
|
wordWrap: true,
|
|
398
414
|
});
|
|
399
415
|
for (const agg of aggregates) {
|
|
@@ -403,6 +419,7 @@ export function makeExperimentsCommand() {
|
|
|
403
419
|
agg.avgDurationMs != null ? `${Math.round(agg.avgDurationMs)}ms` : "—",
|
|
404
420
|
agg.avgTokens != null ? Math.round(agg.avgTokens) : "—",
|
|
405
421
|
agg.totalCost != null ? `$${agg.totalCost}` : "—",
|
|
422
|
+
...(showSkipped ? [skippedOf(agg) || "—"] : []),
|
|
406
423
|
]);
|
|
407
424
|
}
|
|
408
425
|
console.log(chalk.bold("\nAggregates:"));
|
package/dist/commands/files.js
CHANGED
|
@@ -8,19 +8,20 @@ import { formatPage, formatDetail, formatSuccess, withSpinner } from "../lib/out
|
|
|
8
8
|
import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
9
9
|
import { fileToBlob } from "../lib/multipart.js";
|
|
10
10
|
/**
|
|
11
|
-
* Upload a single file with purpose=agent_input|knowledge.
|
|
12
|
-
* describes the "file" part as a binary string,
|
|
13
|
-
* so the placeholder body satisfies the type and
|
|
14
|
-
* real
|
|
11
|
+
* Upload a single file with purpose=agent_input|knowledge. Both are multipart
|
|
12
|
+
* form parts. The generated type describes the "file" part as a binary string,
|
|
13
|
+
* which FormData cannot match, so the placeholder body satisfies the type and
|
|
14
|
+
* bodySerializer sends the real parts.
|
|
15
15
|
*/
|
|
16
16
|
async function uploadFile(command, filePath, purpose) {
|
|
17
17
|
const { blob, filename } = fileToBlob(filePath);
|
|
18
18
|
const formData = new FormData();
|
|
19
19
|
formData.append("file", blob, filename);
|
|
20
|
+
formData.append("purpose", purpose);
|
|
20
21
|
const client = getClient(command);
|
|
21
22
|
const { data } = await client.POST("/v1/files", {
|
|
22
|
-
|
|
23
|
-
body: { file: "" },
|
|
23
|
+
// The server validates the purpose and answers 400 for an unknown one, so the CLI does not narrow it.
|
|
24
|
+
body: { file: "", purpose: purpose },
|
|
24
25
|
bodySerializer: () => formData,
|
|
25
26
|
});
|
|
26
27
|
return data;
|
|
@@ -14,7 +14,7 @@ export function makeProvidersCommand() {
|
|
|
14
14
|
const { data } = await client.GET("/v1/providers", {
|
|
15
15
|
params: { query: { search: opts.search, ...paginationParams(opts) } },
|
|
16
16
|
});
|
|
17
|
-
formatPage(data, command, ["id", "name", "
|
|
17
|
+
formatPage(data, command, ["id", "name", "provider", "createdAt"]);
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
cmd.addCommand(list);
|
|
@@ -36,7 +36,7 @@ export function makeProvidersCommand() {
|
|
|
36
36
|
.description("Create a new provider")
|
|
37
37
|
.requiredOption("-n, --name <name>", "Provider name")
|
|
38
38
|
.requiredOption("--type <type>", "Provider type (openai, azure_openai, anthropic, xai, mistral, ollama)")
|
|
39
|
-
.
|
|
39
|
+
.option("--provider-api-key <key>", "API key for the AI provider (not needed for ollama)")
|
|
40
40
|
.option("-c, --config <json>", "Provider config as JSON")
|
|
41
41
|
.action(async (opts, command) => {
|
|
42
42
|
await runAction(command, async () => {
|
|
@@ -54,7 +54,6 @@ export function makeProvidersCommand() {
|
|
|
54
54
|
.argument("<id>", "Provider ID")
|
|
55
55
|
.option("-n, --name <name>", "New name")
|
|
56
56
|
.option("--type <type>", "New type")
|
|
57
|
-
.option("--enabled <bool>", "Enable/disable")
|
|
58
57
|
.option("-c, --config <json>", "New config as JSON")
|
|
59
58
|
.action(async (id, opts, command) => {
|
|
60
59
|
await runAction(command, async () => {
|
|
@@ -64,8 +63,6 @@ export function makeProvidersCommand() {
|
|
|
64
63
|
body.name = opts.name;
|
|
65
64
|
if (opts.type)
|
|
66
65
|
body.provider = opts.type.toUpperCase();
|
|
67
|
-
if (opts.enabled !== undefined)
|
|
68
|
-
body.enabled = opts.enabled === "true";
|
|
69
66
|
if (opts.config)
|
|
70
67
|
body.config = JSON.parse(opts.config);
|
|
71
68
|
const { data } = await client.PATCH("/v1/providers/{id}", {
|
|
@@ -95,13 +92,7 @@ export function makeProvidersCommand() {
|
|
|
95
92
|
await runAction(command, async () => {
|
|
96
93
|
const client = getClient(command);
|
|
97
94
|
const { data } = await client.GET("/v1/providers/all");
|
|
98
|
-
formatList(data, command, [
|
|
99
|
-
"id",
|
|
100
|
-
"name",
|
|
101
|
-
"provider",
|
|
102
|
-
"enabled",
|
|
103
|
-
"createdAt",
|
|
104
|
-
]);
|
|
95
|
+
formatList(data, command, ["id", "name", "provider", "createdAt"]);
|
|
105
96
|
});
|
|
106
97
|
});
|
|
107
98
|
cmd
|
package/dist/commands/skills.js
CHANGED
|
@@ -149,18 +149,16 @@ export function makeSkillsCommand() {
|
|
|
149
149
|
}
|
|
150
150
|
({ blob, filename } = fileToBlob(path));
|
|
151
151
|
}
|
|
152
|
-
const config = resolveConfig(command);
|
|
153
|
-
const authHeader = await resolveAuthHeader(config);
|
|
154
152
|
const formData = new FormData();
|
|
155
153
|
formData.append("file", blob, filename);
|
|
154
|
+
const client = getClient(command);
|
|
156
155
|
const data = await withSpinner("Importing skill...", async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// Placeholder body for the generated type; bodySerializer sends the real part.
|
|
157
|
+
const result = await client.POST("/v1/skills/import", {
|
|
158
|
+
body: { file: "" },
|
|
159
|
+
bodySerializer: () => formData,
|
|
161
160
|
});
|
|
162
|
-
|
|
163
|
-
return res.json();
|
|
161
|
+
return result.data;
|
|
164
162
|
});
|
|
165
163
|
formatDetail(data, command);
|
|
166
164
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { getClient } from "../lib/client.js";
|
|
3
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
4
4
|
import { handleError } from "../lib/errors.js";
|
|
5
5
|
import { formatDetail, withSpinner } from "../lib/output.js";
|
|
6
6
|
import { fileToBlob } from "../lib/multipart.js";
|
|
7
|
-
import { BackboneApiError } from "../lib/errors.js";
|
|
8
7
|
export function makeTranscribeCommand() {
|
|
9
8
|
const cmd = new Command("transcribe")
|
|
10
9
|
.description("Transcribe an audio file")
|
|
@@ -17,8 +16,6 @@ export function makeTranscribeCommand() {
|
|
|
17
16
|
.action(async (file, opts, command) => {
|
|
18
17
|
const json = isJsonOutput(command);
|
|
19
18
|
try {
|
|
20
|
-
const config = resolveConfig(command);
|
|
21
|
-
const authHeader = await resolveAuthHeader(config);
|
|
22
19
|
const { blob, filename } = fileToBlob(file);
|
|
23
20
|
const formData = new FormData();
|
|
24
21
|
formData.append("file", blob, filename);
|
|
@@ -31,30 +28,15 @@ export function makeTranscribeCommand() {
|
|
|
31
28
|
formData.append("response_format", opts.format);
|
|
32
29
|
if (opts.temperature)
|
|
33
30
|
formData.append("temperature", opts.temperature);
|
|
34
|
-
const
|
|
35
|
-
const url = `${baseUrl}/v1/audio/transcriptions`;
|
|
31
|
+
const client = getClient(command);
|
|
36
32
|
const data = await withSpinner("Transcribing...", async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
body:
|
|
33
|
+
// The generated type describes "file" as a binary string, which FormData cannot match;
|
|
34
|
+
// the placeholder satisfies it and bodySerializer sends the real parts.
|
|
35
|
+
const result = await client.POST("/v1/audio/transcriptions", {
|
|
36
|
+
body: { file: "", model: opts.model },
|
|
37
|
+
bodySerializer: () => formData,
|
|
41
38
|
});
|
|
42
|
-
|
|
43
|
-
let body;
|
|
44
|
-
try {
|
|
45
|
-
body = await res.json();
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
body = {
|
|
49
|
-
error: res.statusText,
|
|
50
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
51
|
-
status: res.status,
|
|
52
|
-
timestamp: new Date().toISOString(),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
throw new BackboneApiError(body);
|
|
56
|
-
}
|
|
57
|
-
return res.json();
|
|
39
|
+
return result.data;
|
|
58
40
|
});
|
|
59
41
|
formatDetail(data, command);
|
|
60
42
|
}
|
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;
|
|
@@ -116,6 +131,9 @@ export declare function parseAgentModel(model?: string): {
|
|
|
116
131
|
* clipboard. Newlines and tabs stay. JSON output is never passed through this.
|
|
117
132
|
*/
|
|
118
133
|
export declare function stripControl(s: string): string;
|
|
134
|
+
export declare function hasInvisible(s: string): boolean;
|
|
135
|
+
/** Replaces each hidden code point with a visible `<U+XXXX>`, so approving a diff means having seen it. */
|
|
136
|
+
export declare function revealInvisible(s: string): string;
|
|
119
137
|
export declare function shellQuote(value: string): string;
|
|
120
138
|
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
121
139
|
export declare function connectionsSentence(connections: PendingConnection[]): string;
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
5
6
|
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
6
7
|
/** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
|
|
7
8
|
export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
|
|
@@ -88,6 +89,39 @@ const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
|
88
89
|
export function stripControl(s) {
|
|
89
90
|
return s.replace(CONTROL_CHARS, "");
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Code points that print as nothing, or reorder what surrounds them, yet reach the model: Unicode
|
|
94
|
+
* tags, zero-width characters, directional marks, embeddings, overrides and isolates, the word
|
|
95
|
+
* joiner and the byte-order mark (#1027). As numbers, not an escaped regex, so no tool can fold
|
|
96
|
+
* them into the source. Kept in step with `surface/src/core/skills-apply-preview.ts`.
|
|
97
|
+
*/
|
|
98
|
+
const INVISIBLE_RANGES = [
|
|
99
|
+
[0xe0000, 0xe007f],
|
|
100
|
+
[0x200b, 0x200f],
|
|
101
|
+
[0x202a, 0x202e],
|
|
102
|
+
[0x2060, 0x2060],
|
|
103
|
+
[0x2066, 0x2069],
|
|
104
|
+
[0x061c, 0x061c],
|
|
105
|
+
[0xfeff, 0xfeff],
|
|
106
|
+
];
|
|
107
|
+
function isInvisible(codePoint) {
|
|
108
|
+
return INVISIBLE_RANGES.some(([low, high]) => codePoint >= low && codePoint <= high);
|
|
109
|
+
}
|
|
110
|
+
export function hasInvisible(s) {
|
|
111
|
+
for (const char of s)
|
|
112
|
+
if (isInvisible(char.codePointAt(0)))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/** Replaces each hidden code point with a visible `<U+XXXX>`, so approving a diff means having seen it. */
|
|
117
|
+
export function revealInvisible(s) {
|
|
118
|
+
let out = "";
|
|
119
|
+
for (const char of s) {
|
|
120
|
+
const codePoint = char.codePointAt(0);
|
|
121
|
+
out += isInvisible(codePoint) ? `<U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}>` : char;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
91
125
|
export function shellQuote(value) {
|
|
92
126
|
if (/^[A-Za-z0-9._@/:-]+$/.test(value))
|
|
93
127
|
return value;
|
|
@@ -119,9 +153,26 @@ function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
|
119
153
|
return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
|
|
120
154
|
(withApprovals ? " --approve-all" : ""));
|
|
121
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
|
|
158
|
+
* is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
|
|
159
|
+
* id, policy class or status, and exists only while the decision is open, so its id doubles as the call id
|
|
160
|
+
* and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
|
|
161
|
+
*/
|
|
162
|
+
function connectorApproval(i) {
|
|
163
|
+
return {
|
|
164
|
+
approvalId: String(i.id),
|
|
165
|
+
callId: String(i.id),
|
|
166
|
+
tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
|
|
167
|
+
arguments: parseArguments(i.arguments),
|
|
168
|
+
policyClass: "unknown",
|
|
169
|
+
reason: null,
|
|
170
|
+
preview: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
122
173
|
/**
|
|
123
174
|
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
124
|
-
* connector calls. A pause on anything else
|
|
175
|
+
* connector calls. A pause on anything else is named.
|
|
125
176
|
* `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
|
|
126
177
|
*/
|
|
127
178
|
const DECODED_ITEM_TYPES = new Set([
|
|
@@ -131,7 +182,9 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
131
182
|
"function_call_output",
|
|
132
183
|
"backbone:approval_request",
|
|
133
184
|
"backbone:connector_auth_request",
|
|
185
|
+
"backbone:input_request",
|
|
134
186
|
"backbone:tool_image",
|
|
187
|
+
"mcp_approval_request",
|
|
135
188
|
"mcp_call",
|
|
136
189
|
"mcp_list_tools",
|
|
137
190
|
]);
|
|
@@ -155,18 +208,21 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
155
208
|
String(i.call_id),
|
|
156
209
|
i.status === "incomplete" ? "incomplete" : "completed",
|
|
157
210
|
]));
|
|
158
|
-
const pendingApprovals =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
211
|
+
const pendingApprovals = [
|
|
212
|
+
...output
|
|
213
|
+
// Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
|
|
214
|
+
.filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
|
|
215
|
+
.map((i) => ({
|
|
216
|
+
approvalId: String(i.id),
|
|
217
|
+
callId: String(i.call_id),
|
|
218
|
+
tool: String(i.tool),
|
|
219
|
+
arguments: parseArguments(i.arguments),
|
|
220
|
+
policyClass: String(i.policy_class),
|
|
221
|
+
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
222
|
+
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
223
|
+
})),
|
|
224
|
+
...output.filter((i) => i.type === "mcp_approval_request").map(connectorApproval),
|
|
225
|
+
];
|
|
170
226
|
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
171
227
|
const connectRequests = pendingConnectionsOf(output);
|
|
172
228
|
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
@@ -175,7 +231,22 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
175
231
|
reason,
|
|
176
232
|
...(destinations ? { destinations } : {}),
|
|
177
233
|
}));
|
|
178
|
-
|
|
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
|
+
]);
|
|
179
250
|
const toolCalls = [];
|
|
180
251
|
const pendingToolCalls = [];
|
|
181
252
|
for (const item of output) {
|
|
@@ -199,9 +270,10 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
199
270
|
status = "incomplete";
|
|
200
271
|
break;
|
|
201
272
|
case "requires_action":
|
|
202
|
-
// 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).
|
|
203
275
|
status =
|
|
204
|
-
pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
276
|
+
pendingInputs.length > 0 || pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
205
277
|
? "requires_tool_output"
|
|
206
278
|
: "requires_approval";
|
|
207
279
|
break;
|
|
@@ -211,7 +283,14 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
211
283
|
const responseId = result?.id ?? null;
|
|
212
284
|
const ref = agentRef ?? agent;
|
|
213
285
|
let next = null;
|
|
214
|
-
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) {
|
|
215
294
|
next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
|
|
216
295
|
}
|
|
217
296
|
else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
|
|
@@ -223,6 +302,13 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
223
302
|
: decide;
|
|
224
303
|
}
|
|
225
304
|
}
|
|
305
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingApprovals.length > 0 && ref && responseId) {
|
|
306
|
+
// A continuation must decide every pending approval (400 incomplete_tool_outputs otherwise), so
|
|
307
|
+
// `--continue` would fail; decide answers them and the engine re-checks access itself (#949 review).
|
|
308
|
+
next =
|
|
309
|
+
`${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}` +
|
|
310
|
+
`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)} --approve-all`;
|
|
311
|
+
}
|
|
226
312
|
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
227
313
|
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
228
314
|
const resume = ref
|
|
@@ -248,6 +334,7 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
248
334
|
pendingApprovals,
|
|
249
335
|
pendingToolCalls,
|
|
250
336
|
pendingConnections,
|
|
337
|
+
pendingInputs,
|
|
251
338
|
incompleteReason: result?.incomplete_details?.reason ?? null,
|
|
252
339
|
usage: result?.usage
|
|
253
340
|
? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
|
|
@@ -255,6 +342,13 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
255
342
|
next,
|
|
256
343
|
};
|
|
257
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
|
+
};
|
|
258
352
|
/** Human-readable rendering of an envelope on stdout. Every value printed is passed through {@link stripControl}. */
|
|
259
353
|
export function printRunText(env) {
|
|
260
354
|
const s = stripControl;
|
|
@@ -271,8 +365,15 @@ export function printRunText(env) {
|
|
|
271
365
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
272
366
|
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
273
367
|
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
274
|
-
//
|
|
275
|
-
|
|
368
|
+
// A SkillsApply change reads as its diff (#1028), like the interactive prompt; an incomplete
|
|
369
|
+
// preview keeps the arguments, the one place a member can read what would persist.
|
|
370
|
+
if (a.preview)
|
|
371
|
+
for (const line of previewLines(a.preview))
|
|
372
|
+
console.log(chalk.dim(` ${s(line)}`));
|
|
373
|
+
if (!a.preview || previewIsIncomplete(a.preview)) {
|
|
374
|
+
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
375
|
+
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
376
|
+
}
|
|
276
377
|
if (a.reason)
|
|
277
378
|
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
278
379
|
});
|
|
@@ -282,6 +383,21 @@ export function printRunText(env) {
|
|
|
282
383
|
if (env.next)
|
|
283
384
|
console.log(`\nDecide with:\n ${s(env.next)}`);
|
|
284
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
|
+
}
|
|
285
401
|
else if (env.status === "requires_tool_output") {
|
|
286
402
|
if (env.pendingToolCalls.length) {
|
|
287
403
|
console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { stripControl } from "./agent-run.js";
|
|
3
|
+
import { hasInvisible, revealInvisible, stripControl } from "./agent-run.js";
|
|
4
4
|
import { REMEMBER_NOTE, previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
5
5
|
/** Raised when the prompt's input closes (EOF, Ctrl+C, Ctrl+D) before an answer arrives. */
|
|
6
6
|
export class PromptAbortedError extends Error {
|
|
@@ -50,9 +50,11 @@ export function readlineAsk(input = process.stdin, output = process.stderr) {
|
|
|
50
50
|
}
|
|
51
51
|
export async function promptApprovals(pending, ask, write = (line) => console.error(line)) {
|
|
52
52
|
const answers = new Map();
|
|
53
|
-
const s = stripControl;
|
|
53
|
+
const s = (value) => revealInvisible(stripControl(value));
|
|
54
54
|
for (const [index, a] of pending.entries()) {
|
|
55
|
-
|
|
55
|
+
// A connector approval carries no policy class (#949): "unknown" may be destructive, so it is never
|
|
56
|
+
// offered a standing grant here; the server drops one for a destructive row anyway.
|
|
57
|
+
const destructive = ["destructive", "unknown"].includes(a.policyClass.toLowerCase());
|
|
56
58
|
write(chalk.yellow(`\nApproval ${index + 1}/${pending.length}: ${s(a.tool)} [${s(a.policyClass)}]`));
|
|
57
59
|
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
58
60
|
const request = () => write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
|
|
@@ -60,7 +62,12 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
|
|
|
60
62
|
// A SkillsApply change reads as its diff (#782, spec §10). When the preview leaves changes
|
|
61
63
|
// out, the full arguments follow it: this prompt never runs with --json, so it is the only
|
|
62
64
|
// place the member can read what would persist (spec D7).
|
|
63
|
-
|
|
65
|
+
const lines = previewLines(a.preview);
|
|
66
|
+
// Hidden characters are shown as <U+XXXX> below; the warning says why (#1027, spec D7).
|
|
67
|
+
if (lines.some(hasInvisible)) {
|
|
68
|
+
write(chalk.yellow("Hidden characters found: shown as <U+XXXX>. A model reads them; you would not see them. Check each marked line before you approve."));
|
|
69
|
+
}
|
|
70
|
+
for (const line of lines)
|
|
64
71
|
write(colourPreviewLine(s(line)));
|
|
65
72
|
if (previewIsIncomplete(a.preview)) {
|
|
66
73
|
write(chalk.dim("Full request (the preview leaves changes out):"));
|
|
@@ -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": "
|
|
3
|
+
"version": "7.0.0-dev.10",
|
|
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",
|