@2kw/ai 6.3.0-dev.14 → 6.3.0-dev.142
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 +4 -1
- package/dist/agent-config/schema.d.ts +6 -3
- package/dist/agent-config/schema.js +19 -4
- package/dist/agent-config/template.js +1 -1
- package/dist/commands/agent-apply.js +1 -1
- package/dist/commands/agent-policy.d.ts +2 -1
- package/dist/commands/agent-policy.js +9 -2
- package/dist/commands/agent-run.d.ts +5 -0
- package/dist/commands/agent-run.js +47 -21
- package/dist/commands/agents.js +18 -5
- package/dist/commands/ai.js +3 -9
- package/dist/commands/auth.js +6 -1
- package/dist/commands/billing.js +107 -3
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +16 -2
- package/dist/commands/conversations.js +16 -0
- package/dist/commands/datasets.js +17 -15
- package/dist/commands/experiments.js +36 -26
- package/dist/commands/files.js +10 -28
- package/dist/commands/installations.js +46 -1
- package/dist/commands/knowledge-documents.js +24 -32
- package/dist/commands/knowledge.js +5 -0
- package/dist/commands/memory.d.ts +10 -0
- package/dist/commands/memory.js +132 -0
- package/dist/commands/prompts.js +9 -8
- package/dist/commands/schemas.js +15 -6
- package/dist/commands/skill-versions.js +27 -1
- package/dist/commands/skills.js +31 -1
- package/dist/commands/tracing.js +12 -17
- package/dist/index.js +2 -0
- package/dist/lib/agent-decide.d.ts +29 -2
- package/dist/lib/agent-decide.js +100 -3
- package/dist/lib/agent-run.d.ts +50 -3
- package/dist/lib/agent-run.js +168 -16
- package/dist/lib/approval-prompt.js +31 -1
- package/dist/lib/client.d.ts +8 -0
- package/dist/lib/client.js +18 -1
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +26 -1
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +147 -0
- package/dist/lib/errors.js +3 -1
- package/dist/lib/overlay.d.ts +10 -0
- package/dist/lib/overlay.js +20 -0
- package/dist/lib/skills-apply-preview.d.ts +19 -0
- package/dist/lib/skills-apply-preview.js +94 -0
- package/dist/lib/tracing-settings.d.ts +26 -0
- package/dist/lib/tracing-settings.js +25 -0
- package/package.json +1 -1
package/dist/lib/agent-run.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { DEFAULT_CHAT_URL } from "./config.js";
|
|
3
|
+
import { connectCallIds, connectionsPhrase, pendingConnectionsOf } from "./connect-pause.js";
|
|
4
|
+
import { CliUsageError } from "./errors.js";
|
|
5
|
+
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
6
|
+
/** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
|
|
7
|
+
export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
|
|
8
|
+
"auto (the operator's policy as written). Without it, the conversation keeps its mode";
|
|
9
|
+
function isConversationMode(value) {
|
|
10
|
+
return typeof value === "string" && CONVERSATION_MODES.includes(value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The `--mode` value, checked before any request is sent: anything but the three exact values is a
|
|
14
|
+
* usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
|
|
15
|
+
*/
|
|
16
|
+
export function parseModeOption(raw) {
|
|
17
|
+
if (raw === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (isConversationMode(raw))
|
|
20
|
+
return raw;
|
|
21
|
+
throw new CliUsageError(`Invalid --mode '${raw}': use plan, ask or auto.`);
|
|
22
|
+
}
|
|
23
|
+
/** The `backbone:mode` input item (S1 D7); always appended last. */
|
|
24
|
+
export function modeItem(mode) {
|
|
25
|
+
return { type: "backbone:mode", mode };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
|
|
29
|
+
* and any value other than the three read as null, which also means "none set".
|
|
30
|
+
*/
|
|
31
|
+
export function responseMode(result) {
|
|
32
|
+
const raw = result?.conversation_mode;
|
|
33
|
+
return isConversationMode(raw) ? raw : null;
|
|
34
|
+
}
|
|
2
35
|
export const EXIT_CODES = {
|
|
3
36
|
completed: 0,
|
|
4
37
|
requires_approval: 3,
|
|
@@ -60,7 +93,59 @@ export function shellQuote(value) {
|
|
|
60
93
|
return value;
|
|
61
94
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
62
95
|
}
|
|
63
|
-
|
|
96
|
+
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
97
|
+
export function connectionsSentence(connections) {
|
|
98
|
+
const joined = connectionsPhrase(connections);
|
|
99
|
+
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
|
100
|
+
}
|
|
101
|
+
/** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
|
|
102
|
+
const THEN_RUN = ", then run: ";
|
|
103
|
+
/**
|
|
104
|
+
* The `decide` command that answers a relay pause (#671 D7): one `--output <callId>=@<file>` per pending call,
|
|
105
|
+
* plus `--approve-all` when approvals wait too. The call id is chosen by the model, so the suggested file name
|
|
106
|
+
* keeps only `[A-Za-z0-9_-]` (P2) and never repeats: it can be neither `../…` nor an absolute path.
|
|
107
|
+
*/
|
|
108
|
+
function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
109
|
+
const used = new Set();
|
|
110
|
+
const outputs = calls.map((c) => {
|
|
111
|
+
const safe = c.callId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
112
|
+
let file = safe;
|
|
113
|
+
// Compared without case: on Windows and macOS `callA.out` and `calla.out` are one file.
|
|
114
|
+
for (let n = 2; used.has(file.toLowerCase()); n++)
|
|
115
|
+
file = `${safe}-${n}`;
|
|
116
|
+
used.add(file.toLowerCase());
|
|
117
|
+
return ` --output ${shellQuote(`${c.callId}=@${file}.out`)}`;
|
|
118
|
+
});
|
|
119
|
+
return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
|
|
120
|
+
(withApprovals ? " --approve-all" : ""));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
124
|
+
* connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
|
|
125
|
+
* `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
|
|
126
|
+
*/
|
|
127
|
+
const DECODED_ITEM_TYPES = new Set([
|
|
128
|
+
"message",
|
|
129
|
+
"reasoning",
|
|
130
|
+
"function_call",
|
|
131
|
+
"function_call_output",
|
|
132
|
+
"backbone:approval_request",
|
|
133
|
+
"backbone:connector_auth_request",
|
|
134
|
+
"backbone:tool_image",
|
|
135
|
+
"mcp_call",
|
|
136
|
+
"mcp_list_tools",
|
|
137
|
+
]);
|
|
138
|
+
function undecodedPause(output) {
|
|
139
|
+
const types = [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
|
|
140
|
+
return types.length > 0
|
|
141
|
+
? `This CLI cannot show or answer ${types.join(", ")}.`
|
|
142
|
+
: "This CLI cannot tell what the run waits for.";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @param chatUrl the chat web host of the API the run went to ({@link chatUrlFor}); a connect pause's
|
|
146
|
+
* `next` sends the user to its Connectors page.
|
|
147
|
+
*/
|
|
148
|
+
export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
64
149
|
const output = result?.output ?? [];
|
|
65
150
|
const { agent, version } = parseAgentModel(result?.model);
|
|
66
151
|
// call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
|
|
@@ -80,8 +165,17 @@ export function buildRunEnvelope(result, agentRef) {
|
|
|
80
165
|
arguments: parseArguments(i.arguments),
|
|
81
166
|
policyClass: String(i.policy_class),
|
|
82
167
|
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
168
|
+
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
83
169
|
}));
|
|
84
|
-
|
|
170
|
+
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
171
|
+
const connectRequests = pendingConnectionsOf(output);
|
|
172
|
+
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
173
|
+
serverLabel,
|
|
174
|
+
host,
|
|
175
|
+
reason,
|
|
176
|
+
...(destinations ? { destinations } : {}),
|
|
177
|
+
}));
|
|
178
|
+
const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
|
|
85
179
|
const toolCalls = [];
|
|
86
180
|
const pendingToolCalls = [];
|
|
87
181
|
for (const item of output) {
|
|
@@ -105,19 +199,46 @@ export function buildRunEnvelope(result, agentRef) {
|
|
|
105
199
|
status = "incomplete";
|
|
106
200
|
break;
|
|
107
201
|
case "requires_action":
|
|
108
|
-
|
|
202
|
+
// A connect pause keeps exit 4 (§8, CLI agents D14): the CLI cannot answer it either.
|
|
203
|
+
status =
|
|
204
|
+
pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
205
|
+
? "requires_tool_output"
|
|
206
|
+
: "requires_approval";
|
|
109
207
|
break;
|
|
110
208
|
default:
|
|
111
209
|
throw new Error(`Unexpected response status: ${result?.status}`);
|
|
112
210
|
}
|
|
113
211
|
const responseId = result?.id ?? null;
|
|
114
212
|
const ref = agentRef ?? agent;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
213
|
+
let next = null;
|
|
214
|
+
if (status === "requires_approval" && ref && responseId) {
|
|
215
|
+
next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
|
|
216
|
+
}
|
|
217
|
+
else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
|
|
218
|
+
// The engine answers the connect call itself on the continuation, so connecting first is enough.
|
|
219
|
+
if (ref && responseId) {
|
|
220
|
+
const decide = relayDecideCommand(ref, responseId, pendingToolCalls, pendingApprovals.length > 0);
|
|
221
|
+
next = pendingConnections.length > 0
|
|
222
|
+
? `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}${decide}`
|
|
223
|
+
: decide;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
227
|
+
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
228
|
+
const resume = ref
|
|
229
|
+
? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
|
|
230
|
+
: `, then run the agent again with --continue ${shellQuote(responseId)}`;
|
|
231
|
+
next = `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${resume}`;
|
|
232
|
+
}
|
|
233
|
+
else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
|
|
234
|
+
// Paused on nothing the envelope decodes: say so, rather than exit 4 without a word.
|
|
235
|
+
next =
|
|
236
|
+
`${undecodedPause(output)} Response ${responseId} stays paused: answer it in ${chatUrl}/ ` +
|
|
237
|
+
"or from a client that continues it by previous_response_id.";
|
|
238
|
+
}
|
|
118
239
|
return {
|
|
119
240
|
status,
|
|
120
|
-
mode:
|
|
241
|
+
mode: responseMode(result),
|
|
121
242
|
agent,
|
|
122
243
|
version,
|
|
123
244
|
responseId,
|
|
@@ -126,6 +247,7 @@ export function buildRunEnvelope(result, agentRef) {
|
|
|
126
247
|
toolCalls,
|
|
127
248
|
pendingApprovals,
|
|
128
249
|
pendingToolCalls,
|
|
250
|
+
pendingConnections,
|
|
129
251
|
incompleteReason: result?.incomplete_details?.reason ?? null,
|
|
130
252
|
usage: result?.usage
|
|
131
253
|
? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
|
|
@@ -147,21 +269,51 @@ export function printRunText(env) {
|
|
|
147
269
|
console.log(chalk.dim(`\n── ${s(head.join(" · "))}`));
|
|
148
270
|
if (env.toolCalls.length)
|
|
149
271
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
272
|
+
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
273
|
+
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
274
|
+
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
275
|
+
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
276
|
+
if (a.reason)
|
|
277
|
+
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
278
|
+
});
|
|
150
279
|
if (env.status === "requires_approval") {
|
|
151
280
|
console.log(chalk.yellow("\nPaused for approval:"));
|
|
152
|
-
|
|
153
|
-
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
154
|
-
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
155
|
-
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
156
|
-
if (a.reason)
|
|
157
|
-
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
158
|
-
});
|
|
281
|
+
printApprovals();
|
|
159
282
|
if (env.next)
|
|
160
283
|
console.log(`\nDecide with:\n ${s(env.next)}`);
|
|
161
284
|
}
|
|
162
285
|
else if (env.status === "requires_tool_output") {
|
|
163
|
-
|
|
164
|
-
|
|
286
|
+
if (env.pendingToolCalls.length) {
|
|
287
|
+
console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
|
|
288
|
+
env.pendingToolCalls.forEach((c) => {
|
|
289
|
+
console.log(` - ${s(c.tool)} (${s(c.callId)})`);
|
|
290
|
+
console.log(chalk.dim(` ${s(JSON.stringify(c.arguments))}`));
|
|
291
|
+
});
|
|
292
|
+
// The printed command carries --approve-all, so show what it approves (#671 P3).
|
|
293
|
+
if (env.pendingApprovals.length) {
|
|
294
|
+
console.log(chalk.yellow("\nIt also waits for approval:"));
|
|
295
|
+
printApprovals();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (env.pendingConnections.length) {
|
|
299
|
+
console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
|
|
300
|
+
env.pendingConnections.forEach((c) => {
|
|
301
|
+
console.log(` - ${s(connectionsSentence([c]))}`);
|
|
302
|
+
if (c.destinations?.length)
|
|
303
|
+
console.log(chalk.dim(` new destinations: ${s(c.destinations.join(", "))}`));
|
|
304
|
+
});
|
|
305
|
+
// The continue command on its own line, ready to copy.
|
|
306
|
+
if (env.next)
|
|
307
|
+
console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
|
|
308
|
+
}
|
|
309
|
+
else if (env.pendingToolCalls.length && env.next) {
|
|
310
|
+
console.log(`\nAnswer with:\n ${s(env.next)}`);
|
|
311
|
+
}
|
|
312
|
+
if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
|
|
313
|
+
console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
|
|
314
|
+
if (env.next)
|
|
315
|
+
console.log(` ${s(env.next)}`);
|
|
316
|
+
}
|
|
165
317
|
}
|
|
166
318
|
else if (env.status === "incomplete") {
|
|
167
319
|
console.log(chalk.yellow(`\nIncomplete: ${s(env.incompleteReason ?? "unknown reason")}`));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { stripControl } from "./agent-run.js";
|
|
4
|
+
import { REMEMBER_NOTE, previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
4
5
|
/** Raised when the prompt's input closes (EOF, Ctrl+C, Ctrl+D) before an answer arrives. */
|
|
5
6
|
export class PromptAbortedError extends Error {
|
|
6
7
|
constructor() {
|
|
@@ -54,7 +55,23 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
|
|
|
54
55
|
const destructive = a.policyClass.toLowerCase() === "destructive";
|
|
55
56
|
write(chalk.yellow(`\nApproval ${index + 1}/${pending.length}: ${s(a.tool)} [${s(a.policyClass)}]`));
|
|
56
57
|
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
57
|
-
write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
|
|
58
|
+
const request = () => write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
|
|
59
|
+
if (a.preview) {
|
|
60
|
+
// A SkillsApply change reads as its diff (#782, spec §10). When the preview leaves changes
|
|
61
|
+
// out, the full arguments follow it: this prompt never runs with --json, so it is the only
|
|
62
|
+
// place the member can read what would persist (spec D7).
|
|
63
|
+
for (const line of previewLines(a.preview))
|
|
64
|
+
write(colourPreviewLine(s(line)));
|
|
65
|
+
if (previewIsIncomplete(a.preview)) {
|
|
66
|
+
write(chalk.dim("Full request (the preview leaves changes out):"));
|
|
67
|
+
request();
|
|
68
|
+
}
|
|
69
|
+
if (!destructive)
|
|
70
|
+
write(chalk.dim(REMEMBER_NOTE));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
request();
|
|
74
|
+
}
|
|
58
75
|
if (a.reason)
|
|
59
76
|
write(chalk.dim(`reason: ${s(a.reason)}`));
|
|
60
77
|
const question = destructive ? "[a]pprove / [r]eject? " : "[a]pprove / [r]eject / approve and [R]emember? ";
|
|
@@ -81,4 +98,17 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
|
|
|
81
98
|
}
|
|
82
99
|
return answers;
|
|
83
100
|
}
|
|
101
|
+
/** Diff lines sit indented six spaces under their file; only those are coloured. */
|
|
102
|
+
function colourPreviewLine(line) {
|
|
103
|
+
if (!line.startsWith(" "))
|
|
104
|
+
return line;
|
|
105
|
+
const body = line.slice(6);
|
|
106
|
+
if (body.startsWith("+"))
|
|
107
|
+
return chalk.green(line);
|
|
108
|
+
if (body.startsWith("-"))
|
|
109
|
+
return chalk.red(line);
|
|
110
|
+
if (body.startsWith("@@"))
|
|
111
|
+
return chalk.cyan(line);
|
|
112
|
+
return chalk.dim(line);
|
|
113
|
+
}
|
|
84
114
|
//# sourceMappingURL=approval-prompt.js.map
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export declare function apiKeyAuthMiddleware(config: ApiKeyConfig): Middleware;
|
|
|
39
39
|
* fires and cannot be replayed safely.
|
|
40
40
|
*/
|
|
41
41
|
export declare function sessionAuthMiddleware(config: SessionConfig, deps?: SessionAuthDeps): Middleware;
|
|
42
|
+
/** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
|
|
43
|
+
export declare const MEMORY_HEADER = "X-Backbone-Memory";
|
|
44
|
+
/**
|
|
45
|
+
* Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
|
|
46
|
+
* and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
|
|
47
|
+
* it; the server reads it only there.
|
|
48
|
+
*/
|
|
49
|
+
export declare const memoryHeaderMiddleware: Middleware;
|
|
42
50
|
/**
|
|
43
51
|
* The middleware stack for a resolved config, in registration order.
|
|
44
52
|
*
|
package/dist/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createClient from "openapi-fetch";
|
|
2
|
-
import { resolveConfig, isJsonOutput } from "./config.js";
|
|
2
|
+
import { resolveConfig, isJsonOutput, memoryEnabled } from "./config.js";
|
|
3
3
|
import { ensureJwt } from "./auth-session.js";
|
|
4
4
|
import { BackboneApiError, handleError } from "./errors.js";
|
|
5
5
|
/**
|
|
@@ -85,6 +85,21 @@ export function sessionAuthMiddleware(config, deps = {}) {
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
/** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
|
|
89
|
+
export const MEMORY_HEADER = "X-Backbone-Memory";
|
|
90
|
+
/**
|
|
91
|
+
* Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
|
|
92
|
+
* and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
|
|
93
|
+
* it; the server reads it only there.
|
|
94
|
+
*/
|
|
95
|
+
export const memoryHeaderMiddleware = {
|
|
96
|
+
onRequest({ request }) {
|
|
97
|
+
if (request.method === "POST" && new URL(request.url).pathname.endsWith("/v1/responses")) {
|
|
98
|
+
request.headers.set(MEMORY_HEADER, "enabled");
|
|
99
|
+
}
|
|
100
|
+
return request;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
88
103
|
/**
|
|
89
104
|
* The middleware stack for a resolved config, in registration order.
|
|
90
105
|
*
|
|
@@ -125,6 +140,8 @@ export function getClient(command) {
|
|
|
125
140
|
baseUrl: config.baseUrl.replace(/\/+$/, ""),
|
|
126
141
|
});
|
|
127
142
|
client.use(...buildMiddleware(config));
|
|
143
|
+
if (memoryEnabled())
|
|
144
|
+
client.use(memoryHeaderMiddleware);
|
|
128
145
|
return client;
|
|
129
146
|
}
|
|
130
147
|
/**
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ContextEntry {
|
|
|
14
14
|
cachedJwt?: string;
|
|
15
15
|
/** Epoch millis when cachedJwt expires. */
|
|
16
16
|
cachedJwtExp?: number;
|
|
17
|
+
/** Send `X-Backbone-Memory: enabled` on responses calls (`2kw config set memory true`, #721). */
|
|
18
|
+
memory?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface BackboneConfigStore {
|
|
19
21
|
activeContext: string;
|
|
@@ -50,11 +52,25 @@ export declare const DEFAULT_BASE_URL: string;
|
|
|
50
52
|
*/
|
|
51
53
|
export declare const DEFAULT_AUTH_URL: string;
|
|
52
54
|
export declare function defaultAuthUrlFor(baseUrl: string): string;
|
|
55
|
+
export { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
56
|
+
/**
|
|
57
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
58
|
+
* (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
|
|
59
|
+
* does not know; a trailing slash is dropped so callers can append a path. The mapping itself
|
|
60
|
+
* is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
|
|
61
|
+
*/
|
|
62
|
+
export declare function chatUrlFor(baseUrl: string, env?: NodeJS.ProcessEnv): string;
|
|
53
63
|
declare const store: Conf<BackboneConfigStore>;
|
|
54
64
|
export { store };
|
|
55
65
|
export declare function validateContextName(name: string): void;
|
|
56
66
|
export declare function getActiveContextName(): string;
|
|
57
67
|
export declare function getActiveContext(): ContextEntry | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
|
|
70
|
+
* set (`true` on, anything else off), so CI can switch it without touching the store;
|
|
71
|
+
* otherwise the active context's `memory` key decides. Off by default.
|
|
72
|
+
*/
|
|
73
|
+
export declare function memoryEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
58
74
|
export declare function getAllContexts(): Record<string, ContextEntry>;
|
|
59
75
|
export declare function getContextCount(): number;
|
|
60
76
|
export declare function setContext(name: string, entry: ContextEntry): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { chatOriginFor } from "./connect-pause.js";
|
|
4
5
|
/**
|
|
5
6
|
* Fallback base URL when the user has no context, env var, or local config.
|
|
6
7
|
* Override at runtime by setting AI_2KW_DEFAULT_BASE_URL (or the legacy
|
|
@@ -9,7 +10,7 @@ import { resolve } from "node:path";
|
|
|
9
10
|
*/
|
|
10
11
|
export const DEFAULT_BASE_URL = process.env.AI_2KW_DEFAULT_BASE_URL ??
|
|
11
12
|
process.env.BACKBONE_DEFAULT_BASE_URL ??
|
|
12
|
-
"https://
|
|
13
|
+
"https://api.2kw.ai";
|
|
13
14
|
/**
|
|
14
15
|
* Fallback auth-service URL used when the API host is not one we recognise.
|
|
15
16
|
* Override at runtime with AI_2KW_DEFAULT_AUTH_URL.
|
|
@@ -36,6 +37,19 @@ export function defaultAuthUrlFor(baseUrl) {
|
|
|
36
37
|
return DEFAULT_AUTH_URL;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
export { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
41
|
+
/**
|
|
42
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
43
|
+
* (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
|
|
44
|
+
* does not know; a trailing slash is dropped so callers can append a path. The mapping itself
|
|
45
|
+
* is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
|
|
46
|
+
*/
|
|
47
|
+
export function chatUrlFor(baseUrl, env = process.env) {
|
|
48
|
+
const override = env.AI_2KW_CHAT_URL?.trim();
|
|
49
|
+
if (override)
|
|
50
|
+
return override.replace(/\/+$/, "");
|
|
51
|
+
return chatOriginFor(baseUrl);
|
|
52
|
+
}
|
|
39
53
|
// One-shot flag so we only print the deprecation warning once per process,
|
|
40
54
|
// even if resolveConfig is called multiple times across commands.
|
|
41
55
|
let legacyEnvWarned = false;
|
|
@@ -142,6 +156,17 @@ export function getActiveContext() {
|
|
|
142
156
|
const contexts = store.get("contexts") ?? {};
|
|
143
157
|
return contexts[name];
|
|
144
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
|
|
161
|
+
* set (`true` on, anything else off), so CI can switch it without touching the store;
|
|
162
|
+
* otherwise the active context's `memory` key decides. Off by default.
|
|
163
|
+
*/
|
|
164
|
+
export function memoryEnabled(env = process.env) {
|
|
165
|
+
const fromEnv = env.AI_2KW_MEMORY;
|
|
166
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
167
|
+
return fromEnv === "true";
|
|
168
|
+
return getActiveContext()?.memory === true;
|
|
169
|
+
}
|
|
145
170
|
export function getAllContexts() {
|
|
146
171
|
return store.get("contexts") ?? {};
|
|
147
172
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
3
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
4
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
5
|
+
*
|
|
6
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
7
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
11
|
+
*
|
|
12
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
14
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
17
|
+
export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
18
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
19
|
+
export declare const CHAT_URL_BY_API_HOST: Readonly<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
22
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
23
|
+
*/
|
|
24
|
+
export declare function chatOriginFor(baseUrl: unknown): string;
|
|
25
|
+
/** What the user is asked to do: connect, allow the agent, or reconnect. */
|
|
26
|
+
export type ConnectReason = "connect" | "allow" | "reconnect";
|
|
27
|
+
export declare const CONNECT_REASONS: readonly ConnectReason[];
|
|
28
|
+
/**
|
|
29
|
+
* One open connect request. `id` (`cauth_<call id>`) and `callId` (the first connect call for
|
|
30
|
+
* this connector) are what a client needs to withhold the connect calls; the CLI and the MCP
|
|
31
|
+
* server drop both from what they publish.
|
|
32
|
+
*/
|
|
33
|
+
export interface PendingConnection {
|
|
34
|
+
id: string;
|
|
35
|
+
callId: string;
|
|
36
|
+
serverLabel: string;
|
|
37
|
+
/** The MCP server's host: what the user is asked to connect to. */
|
|
38
|
+
host: string;
|
|
39
|
+
reason: ConnectReason;
|
|
40
|
+
/** The egress difference an allow is asked again for (#807 R9); absent otherwise, never empty. */
|
|
41
|
+
destinations?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Enough of a connection to name it in a sentence. */
|
|
44
|
+
export interface ConnectTarget {
|
|
45
|
+
serverLabel: string;
|
|
46
|
+
host: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
50
|
+
export declare function connectToolName(serverLabel: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
53
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
54
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
55
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
56
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pendingConnectionsOf(output: unknown): PendingConnection[];
|
|
59
|
+
/**
|
|
60
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
61
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
62
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
63
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
64
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
65
|
+
*/
|
|
66
|
+
export declare function connectCallIds(output: unknown, connections: readonly Pick<PendingConnection, "callId" | "serverLabel">[]): Set<string>;
|
|
67
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
68
|
+
export declare function connectionPhrase(c: ConnectTarget): string;
|
|
69
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
70
|
+
export declare function connectionsPhrase(connections: readonly ConnectTarget[]): string;
|
|
71
|
+
//# sourceMappingURL=connect-pause.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
3
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
4
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
5
|
+
*
|
|
6
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
7
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
11
|
+
*
|
|
12
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
14
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
17
|
+
export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
18
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
19
|
+
export const CHAT_URL_BY_API_HOST = {
|
|
20
|
+
"api.2kw.ai": "https://chat.2kw.ai",
|
|
21
|
+
"api-dev.2kw.ai": "https://chat-dev.2kw.ai",
|
|
22
|
+
"backbone.manfred-kunze.dev": "https://chat.2kw.ai",
|
|
23
|
+
"localhost:8080": "http://localhost:3000",
|
|
24
|
+
"127.0.0.1:8080": "http://localhost:3000",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
28
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
29
|
+
*/
|
|
30
|
+
export function chatOriginFor(baseUrl) {
|
|
31
|
+
try {
|
|
32
|
+
const host = new URL(String(baseUrl ?? "")).host;
|
|
33
|
+
// hasOwn, not a bare index: a host named after an Object.prototype key must not resolve.
|
|
34
|
+
return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] ?? DEFAULT_CHAT_URL : DEFAULT_CHAT_URL;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return DEFAULT_CHAT_URL;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const CONNECT_REASONS = ["connect", "allow", "reconnect"];
|
|
41
|
+
const CONNECT_REQUEST = "backbone:connector_auth_request";
|
|
42
|
+
function itemsOf(output) {
|
|
43
|
+
if (!Array.isArray(output))
|
|
44
|
+
return [];
|
|
45
|
+
return output.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
46
|
+
}
|
|
47
|
+
function text(value) {
|
|
48
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
51
|
+
export function connectToolName(serverLabel) {
|
|
52
|
+
return `mcp__${serverLabel}__connect`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
56
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
57
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
58
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
59
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
60
|
+
*/
|
|
61
|
+
export function pendingConnectionsOf(output) {
|
|
62
|
+
const items = itemsOf(output);
|
|
63
|
+
const resolved = new Set();
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const id = text(item.id);
|
|
66
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
67
|
+
resolved.add(id);
|
|
68
|
+
}
|
|
69
|
+
const pending = new Map();
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
72
|
+
continue;
|
|
73
|
+
const id = text(item.id);
|
|
74
|
+
const callId = text(item.call_id);
|
|
75
|
+
const serverLabel = text(item.server_label);
|
|
76
|
+
const host = text(item.host);
|
|
77
|
+
if (!id || !callId || !serverLabel || !host || resolved.has(id) || pending.has(id))
|
|
78
|
+
continue;
|
|
79
|
+
const reason = text(item.reason);
|
|
80
|
+
const destinations = Array.isArray(item.destinations)
|
|
81
|
+
? item.destinations.filter((entry) => typeof entry === "string")
|
|
82
|
+
: [];
|
|
83
|
+
pending.set(id, {
|
|
84
|
+
id,
|
|
85
|
+
callId,
|
|
86
|
+
serverLabel,
|
|
87
|
+
host,
|
|
88
|
+
reason: reason && CONNECT_REASONS.includes(reason) ? reason : "connect",
|
|
89
|
+
...(destinations.length > 0 ? { destinations } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return [...pending.values()];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
96
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
97
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
98
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
99
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
100
|
+
*/
|
|
101
|
+
export function connectCallIds(output, connections) {
|
|
102
|
+
const names = new Set(connections.map((c) => connectToolName(c.serverLabel)));
|
|
103
|
+
const ids = new Set(connections.map((c) => c.callId));
|
|
104
|
+
const items = itemsOf(output);
|
|
105
|
+
// Every open connect request's own call id is withheld, even one pendingConnectionsOf could
|
|
106
|
+
// not fully describe (a missing server_label or host): the paired function_call must never be
|
|
107
|
+
// left for the caller to fabricate an output for (R7).
|
|
108
|
+
const resolved = new Set();
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const id = text(item.id);
|
|
111
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
112
|
+
resolved.add(id);
|
|
113
|
+
}
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
116
|
+
continue;
|
|
117
|
+
const id = text(item.id);
|
|
118
|
+
if (id && resolved.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
const callId = text(item.call_id);
|
|
121
|
+
if (callId)
|
|
122
|
+
ids.add(callId);
|
|
123
|
+
}
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const callId = text(item.call_id);
|
|
126
|
+
if (item.type === "function_call" && callId && typeof item.name === "string" && names.has(item.name))
|
|
127
|
+
ids.add(callId);
|
|
128
|
+
}
|
|
129
|
+
return ids;
|
|
130
|
+
}
|
|
131
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
132
|
+
export function connectionPhrase(c) {
|
|
133
|
+
const target = `${c.serverLabel} (${c.host})`;
|
|
134
|
+
if (c.reason === "allow")
|
|
135
|
+
return `allow the agent to use ${target}`;
|
|
136
|
+
if (c.reason === "reconnect")
|
|
137
|
+
return `reconnect ${target}`;
|
|
138
|
+
return `connect ${target}`;
|
|
139
|
+
}
|
|
140
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
141
|
+
export function connectionsPhrase(connections) {
|
|
142
|
+
const phrases = connections.map(connectionPhrase);
|
|
143
|
+
if (phrases.length < 2)
|
|
144
|
+
return phrases[0] ?? "";
|
|
145
|
+
return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=connect-pause.js.map
|
package/dist/lib/errors.js
CHANGED
|
@@ -95,7 +95,9 @@ const PENDING_APPROVALS_HINT = "The approval was already decided or its response
|
|
|
95
95
|
const CODE_HINTS = {
|
|
96
96
|
approval_hmac_mismatch: PENDING_APPROVALS_HINT,
|
|
97
97
|
unknown_approval_id: PENDING_APPROVALS_HINT,
|
|
98
|
-
incomplete_tool_outputs: "Every pending approval of a paused response must be
|
|
98
|
+
incomplete_tool_outputs: "Every released tool call and every pending approval of a paused response must be answered in one call. " +
|
|
99
|
+
"Check pendingToolCalls and pendingApprovals in the run envelope.",
|
|
100
|
+
unknown_tool_output: "That call id was not released by this response. Use the responseId of the latest envelope.",
|
|
99
101
|
conversation_agent_mismatch: "This conversation belongs to another agent. Start a new conversation or run the agent that owns it.",
|
|
100
102
|
};
|
|
101
103
|
/** The most specific hint for an API error: gateway code, then known messages, then the status table. */
|