@2kw/ai 6.3.0 → 6.4.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/dist/agent-config/schema.d.ts +31 -0
- package/dist/agent-config/schema.js +17 -0
- package/dist/agent-config/template.js +2 -0
- package/dist/commands/ai.js +2 -2
- package/dist/commands/convert.js +13 -29
- package/dist/commands/experiments.js +20 -3
- package/dist/commands/knowledge.js +3 -3
- package/dist/commands/skills.js +6 -8
- package/dist/commands/transcribe.js +9 -27
- package/dist/lib/agent-run.d.ts +3 -0
- package/dist/lib/agent-run.js +86 -15
- package/dist/lib/approval-prompt.js +11 -4
- package/package.json +1 -1
|
@@ -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[];
|
|
@@ -25,6 +25,8 @@ export const NON_AUTHORABLE_TOOL_TYPES = {
|
|
|
25
25
|
"backbone.skills_apply": "it is added at run time for callers allowed to author skills",
|
|
26
26
|
// #1241: injected while the conversation's outputs are held; an authored copy is dropped.
|
|
27
27
|
"backbone.deliver": "it is added at run time while sandbox outputs are held",
|
|
28
|
+
// #1244: injected wherever backbone.bash is advertised; an authored copy is dropped.
|
|
29
|
+
"backbone.view_image": "it is added at run time wherever the sandboxed shell is",
|
|
28
30
|
web_search: "it has no executor and is skipped",
|
|
29
31
|
};
|
|
30
32
|
const BUILTIN_TYPES = [
|
|
@@ -33,6 +35,7 @@ const BUILTIN_TYPES = [
|
|
|
33
35
|
"backbone.document_convert",
|
|
34
36
|
"backbone.todo",
|
|
35
37
|
"backbone.skill",
|
|
38
|
+
"backbone.bash",
|
|
36
39
|
"function",
|
|
37
40
|
];
|
|
38
41
|
const typeIs = (t) => ({ required: ["type"], properties: { type: { const: t } } });
|
|
@@ -159,6 +162,19 @@ export const agentSchema = {
|
|
|
159
162
|
skills: { type: "array", minItems: 1, maxItems: 20, uniqueItems: true, items: { type: "string", pattern: SKILL_REF } },
|
|
160
163
|
},
|
|
161
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
|
+
},
|
|
162
178
|
functionTool: {
|
|
163
179
|
type: "object",
|
|
164
180
|
additionalProperties: false,
|
|
@@ -192,6 +208,7 @@ export const agentSchema = {
|
|
|
192
208
|
{ if: typeIs("backbone.document_convert"), then: { $ref: "#/$defs/documentConvertTool" } },
|
|
193
209
|
{ if: typeIs("backbone.todo"), then: { $ref: "#/$defs/todoTool" } },
|
|
194
210
|
{ if: typeIs("backbone.skill"), then: { $ref: "#/$defs/skillTool" } },
|
|
211
|
+
{ if: typeIs("backbone.bash"), then: { $ref: "#/$defs/bashTool" } },
|
|
195
212
|
{ if: typeIs("function"), then: { $ref: "#/$defs/functionTool" } },
|
|
196
213
|
{
|
|
197
214
|
// The validator reports these with the reason from NON_AUTHORABLE_TOOL_TYPES; this keeps the published schema honest.
|
package/dist/commands/ai.js
CHANGED
|
@@ -106,11 +106,11 @@ export function makeAiCommand() {
|
|
|
106
106
|
.option("--agent <id>", "Invoke a stored agent by id or name, optionally with @label")
|
|
107
107
|
.option("--model <id>", "Model in provider/model format; with --agent, one of the agent's models to run this request on")
|
|
108
108
|
.option("--conversation <id>", "Conversation ID this response belongs to")
|
|
109
|
-
.option("--stream", "Stream the response (not supported by the
|
|
109
|
+
.option("--stream", "Stream the response (not supported by the CLI yet)")
|
|
110
110
|
.action(async (input, opts, command) => {
|
|
111
111
|
await runAction(command, async () => {
|
|
112
112
|
if (opts.stream) {
|
|
113
|
-
throw new CliUsageError("Streaming is not supported by the
|
|
113
|
+
throw new CliUsageError("Streaming is not supported by the CLI yet. Run without --stream.");
|
|
114
114
|
}
|
|
115
115
|
const model = resolveModel(opts);
|
|
116
116
|
const inputText = input ?? readStdinInput();
|
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:"));
|
|
@@ -37,7 +37,7 @@ export function makeKnowledgeCommand() {
|
|
|
37
37
|
.description("Create a knowledge base and its first configuration version")
|
|
38
38
|
.requiredOption("-n, --name <name>", "Knowledge base name")
|
|
39
39
|
.requiredOption("--slug <slug>", "Unique slug")
|
|
40
|
-
.requiredOption("--embedding-provider <id>", "Embedding provider ID")
|
|
40
|
+
.requiredOption("--embedding-provider <id>", "Embedding provider: a provider ID, or builtin for the platform's built-in models (text-embedding-3-small, text-embedding-3-large; charged per input token)")
|
|
41
41
|
.requiredOption("--embedding-model <model>", "Embedding model")
|
|
42
42
|
.requiredOption("--embedding-dim <n>", "Embedding dimension", parseInt)
|
|
43
43
|
.option("-d, --description <text>", "Description")
|
|
@@ -77,7 +77,7 @@ export function makeKnowledgeCommand() {
|
|
|
77
77
|
.argument("<id>", "Knowledge base ID")
|
|
78
78
|
.option("-n, --name <name>", "New name")
|
|
79
79
|
.option("-d, --description <text>", "New description")
|
|
80
|
-
.option("--embedding-provider <id>", "New embedding provider ID")
|
|
80
|
+
.option("--embedding-provider <id>", "New embedding provider ID, or builtin. Re-embeds nothing: stored vectors stay comparable only if the new provider serves the same model at the same width; the safe move is a new knowledge base with the documents uploaded again")
|
|
81
81
|
.option("--embedding-model <model>", "New embedding model")
|
|
82
82
|
.option("--chunking-strategy <strategy>", "New chunking strategy (AUTO, FIXED, HIERARCHICAL, CUSTOM)")
|
|
83
83
|
.option("--chunk-size <n>", "New chunk size", parseInt)
|
|
@@ -178,7 +178,7 @@ export function makeKnowledgeCommand() {
|
|
|
178
178
|
cmd
|
|
179
179
|
.command("embedding-models")
|
|
180
180
|
.description("List embedding models a provider serves that fit a provisioned dimension")
|
|
181
|
-
.argument("<providerId>", "Provider ID")
|
|
181
|
+
.argument("<providerId>", "Provider ID, or builtin for the platform's built-in embedding models")
|
|
182
182
|
.action(async (providerId, _opts, command) => {
|
|
183
183
|
await runAction(command, async () => {
|
|
184
184
|
const client = getClient(command);
|
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
|
@@ -116,6 +116,9 @@ export declare function parseAgentModel(model?: string): {
|
|
|
116
116
|
* clipboard. Newlines and tabs stay. JSON output is never passed through this.
|
|
117
117
|
*/
|
|
118
118
|
export declare function stripControl(s: string): string;
|
|
119
|
+
export declare function hasInvisible(s: string): boolean;
|
|
120
|
+
/** Replaces each hidden code point with a visible `<U+XXXX>`, so approving a diff means having seen it. */
|
|
121
|
+
export declare function revealInvisible(s: string): string;
|
|
119
122
|
export declare function shellQuote(value: string): string;
|
|
120
123
|
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
121
124
|
export declare function connectionsSentence(connections: PendingConnection[]): string;
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { DEFAULT_CHAT_URL } from "./config.js";
|
|
3
3
|
import { connectCallIds, connectionsPhrase, pendingConnectionsOf } 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,27 @@ 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.
|
|
176
|
+
* `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
|
|
125
177
|
*/
|
|
126
178
|
const DECODED_ITEM_TYPES = new Set([
|
|
127
179
|
"message",
|
|
@@ -130,6 +182,8 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
130
182
|
"function_call_output",
|
|
131
183
|
"backbone:approval_request",
|
|
132
184
|
"backbone:connector_auth_request",
|
|
185
|
+
"backbone:tool_image",
|
|
186
|
+
"mcp_approval_request",
|
|
133
187
|
"mcp_call",
|
|
134
188
|
"mcp_list_tools",
|
|
135
189
|
]);
|
|
@@ -153,18 +207,21 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
153
207
|
String(i.call_id),
|
|
154
208
|
i.status === "incomplete" ? "incomplete" : "completed",
|
|
155
209
|
]));
|
|
156
|
-
const pendingApprovals =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
210
|
+
const pendingApprovals = [
|
|
211
|
+
...output
|
|
212
|
+
// Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
|
|
213
|
+
.filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
|
|
214
|
+
.map((i) => ({
|
|
215
|
+
approvalId: String(i.id),
|
|
216
|
+
callId: String(i.call_id),
|
|
217
|
+
tool: String(i.tool),
|
|
218
|
+
arguments: parseArguments(i.arguments),
|
|
219
|
+
policyClass: String(i.policy_class),
|
|
220
|
+
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
221
|
+
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
222
|
+
})),
|
|
223
|
+
...output.filter((i) => i.type === "mcp_approval_request").map(connectorApproval),
|
|
224
|
+
];
|
|
168
225
|
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
169
226
|
const connectRequests = pendingConnectionsOf(output);
|
|
170
227
|
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
@@ -221,6 +278,13 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
221
278
|
: decide;
|
|
222
279
|
}
|
|
223
280
|
}
|
|
281
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingApprovals.length > 0 && ref && responseId) {
|
|
282
|
+
// A continuation must decide every pending approval (400 incomplete_tool_outputs otherwise), so
|
|
283
|
+
// `--continue` would fail; decide answers them and the engine re-checks access itself (#949 review).
|
|
284
|
+
next =
|
|
285
|
+
`${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}` +
|
|
286
|
+
`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)} --approve-all`;
|
|
287
|
+
}
|
|
224
288
|
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
225
289
|
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
226
290
|
const resume = ref
|
|
@@ -269,8 +333,15 @@ export function printRunText(env) {
|
|
|
269
333
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
270
334
|
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
271
335
|
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
272
|
-
//
|
|
273
|
-
|
|
336
|
+
// A SkillsApply change reads as its diff (#1028), like the interactive prompt; an incomplete
|
|
337
|
+
// preview keeps the arguments, the one place a member can read what would persist.
|
|
338
|
+
if (a.preview)
|
|
339
|
+
for (const line of previewLines(a.preview))
|
|
340
|
+
console.log(chalk.dim(` ${s(line)}`));
|
|
341
|
+
if (!a.preview || previewIsIncomplete(a.preview)) {
|
|
342
|
+
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
343
|
+
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
344
|
+
}
|
|
274
345
|
if (a.reason)
|
|
275
346
|
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
276
347
|
});
|
|
@@ -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):"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.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",
|