@2kw/ai-mcp-server 6.3.0-dev.12 → 6.3.0-dev.127
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 +3 -1
- package/dist/client.d.ts +9 -2
- package/dist/client.js +15 -1
- package/dist/index.js +5 -53
- package/dist/lib/agent-decide.d.ts +94 -0
- package/dist/lib/agent-decide.js +216 -0
- package/dist/lib/agent-run.d.ts +125 -0
- package/dist/lib/agent-run.js +259 -0
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +149 -0
- package/dist/lib/overlay.d.ts +10 -0
- package/dist/lib/overlay.js +20 -0
- package/dist/tools/agents.js +118 -4
- package/dist/tools/ai-gateway.js +25 -11
- package/dist/tools/conversations.js +26 -0
- package/dist/tools/datasets.js +10 -15
- package/dist/tools/experiments.js +24 -28
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +190 -0
- package/dist/tools/knowledge.js +41 -23
- package/dist/tools/memory.d.ts +9 -0
- package/dist/tools/memory.js +43 -0
- package/dist/tools/prompts.js +10 -9
- package/dist/tools/schemas.js +14 -7
- package/dist/tools/tracing.js +21 -8
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Get an API key from your [2kw.ai dashboard](https://2kw.ai) — paid plans start
|
|
|
42
42
|
|----------|----------|---------|-------------|
|
|
43
43
|
| `AI_2KW_API_KEY` | Yes | — | API key for the 2kw.ai platform (legacy alias: `BACKBONE_API_KEY`) |
|
|
44
44
|
| `AI_2KW_BASE_URL` | No | 2kw.ai cloud API | Point at a self-hosted deployment (legacy alias: `BACKBONE_BASE_URL`) |
|
|
45
|
+
| `AI_2KW_MEMORY` | No | off | `true` makes agent runs through `2kw_create_response` and `2kw_decide_agent_approvals` opt into agent memory (`X-Backbone-Memory: enabled`) |
|
|
45
46
|
| `MCP_TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` |
|
|
46
47
|
| `MCP_HTTP_PORT` | No | `3100` | Port for HTTP transport |
|
|
47
48
|
|
|
@@ -56,10 +57,11 @@ Get an API key from your [2kw.ai dashboard](https://2kw.ai) — paid plans start
|
|
|
56
57
|
| **Schemas** | CRUD, versioning, labels, validation, and testing against sample documents |
|
|
57
58
|
| **Prompts** | Versioned prompt management, labels, compilation, testing |
|
|
58
59
|
| **Datasets & experiments** | Build datasets, run experiments, manage evaluators, record scores |
|
|
59
|
-
| **Agents** | CRUD, versioning, labels, tool-approval history, and tool-catalog sync history |
|
|
60
|
+
| **Agents** | CRUD, versioning, labels, runs with the full result envelope, approval decisions, tool-approval history, and tool-catalog sync history |
|
|
60
61
|
| **Conversations** | Create/inspect/delete conversations and replay their items for the responses API |
|
|
61
62
|
| **Knowledge base** | CRUD, document upload/attach/versioning, hybrid search, and citation resolution |
|
|
62
63
|
| **Files** | Upload, list, download, and delete files (agent input or knowledge documents) |
|
|
64
|
+
| **Memory** | List, read, write and delete your own agent memory files, forget everything; admins see member totals and erase a member |
|
|
63
65
|
| **Observability** | Analytics (spend, quality, providers, errors), traces, billing limits |
|
|
64
66
|
| **API docs** | Browse the platform API documentation by section |
|
|
65
67
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import createClient from "openapi-fetch";
|
|
1
|
+
import createClient, { type Middleware } from "openapi-fetch";
|
|
2
2
|
import type { paths } from "./generated/openapi.js";
|
|
3
3
|
export type ApiClient = ReturnType<typeof createClient<paths>> & {
|
|
4
4
|
/** Exposed config for raw fetch calls (e.g. multipart uploads). */
|
|
@@ -7,8 +7,15 @@ export type ApiClient = ReturnType<typeof createClient<paths>> & {
|
|
|
7
7
|
apiKey: string;
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
|
+
/** The per-request opt-in to agent memory for API-key runs (agent memory spec §3.3, D6, D7). */
|
|
11
|
+
export declare const MEMORY_HEADER = "X-Backbone-Memory";
|
|
12
|
+
/** Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses` only, the call that starts or resumes a run. */
|
|
13
|
+
export declare const memoryHeaderMiddleware: Middleware;
|
|
10
14
|
/**
|
|
11
15
|
* Create a typed openapi-fetch client with Bearer auth and error middleware.
|
|
16
|
+
* `memory: true` opts agent runs into agent memory (see {@link memoryHeaderMiddleware}).
|
|
12
17
|
*/
|
|
13
|
-
export declare function createApiClient(baseUrl: string, apiKey: string
|
|
18
|
+
export declare function createApiClient(baseUrl: string, apiKey: string, options?: {
|
|
19
|
+
memory?: boolean;
|
|
20
|
+
}): ApiClient;
|
|
14
21
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
CHANGED
|
@@ -34,10 +34,22 @@ const errorMiddleware = {
|
|
|
34
34
|
});
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
+
/** The per-request opt-in to agent memory for API-key runs (agent memory spec §3.3, D6, D7). */
|
|
38
|
+
export const MEMORY_HEADER = "X-Backbone-Memory";
|
|
39
|
+
/** Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses` only, the call that starts or resumes a run. */
|
|
40
|
+
export const memoryHeaderMiddleware = {
|
|
41
|
+
onRequest({ request }) {
|
|
42
|
+
if (request.method === "POST" && new URL(request.url).pathname.endsWith("/v1/responses")) {
|
|
43
|
+
request.headers.set(MEMORY_HEADER, "enabled");
|
|
44
|
+
}
|
|
45
|
+
return request;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
37
48
|
/**
|
|
38
49
|
* Create a typed openapi-fetch client with Bearer auth and error middleware.
|
|
50
|
+
* `memory: true` opts agent runs into agent memory (see {@link memoryHeaderMiddleware}).
|
|
39
51
|
*/
|
|
40
|
-
export function createApiClient(baseUrl, apiKey) {
|
|
52
|
+
export function createApiClient(baseUrl, apiKey, options = {}) {
|
|
41
53
|
const client = createClient({
|
|
42
54
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
43
55
|
headers: {
|
|
@@ -45,6 +57,8 @@ export function createApiClient(baseUrl, apiKey) {
|
|
|
45
57
|
},
|
|
46
58
|
});
|
|
47
59
|
client.use(errorMiddleware);
|
|
60
|
+
if (options.memory)
|
|
61
|
+
client.use(memoryHeaderMiddleware);
|
|
48
62
|
// Expose config for raw fetch calls (multipart uploads)
|
|
49
63
|
const enriched = client;
|
|
50
64
|
enriched._config = { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
|
package/dist/index.js
CHANGED
|
@@ -6,31 +6,7 @@ import { createServer } from "node:http";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { createApiClient } from "./client.js";
|
|
8
8
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
9
|
-
import
|
|
10
|
-
import * as extraction from "./tools/extraction.js";
|
|
11
|
-
import * as aiGateway from "./tools/ai-gateway.js";
|
|
12
|
-
import * as transcription from "./tools/transcription.js";
|
|
13
|
-
import * as schemas from "./tools/schemas.js";
|
|
14
|
-
import * as schemaLabels from "./tools/schema-labels.js";
|
|
15
|
-
import * as schemaVersions from "./tools/schema-versions.js";
|
|
16
|
-
import * as schemaTesting from "./tools/schema-testing.js";
|
|
17
|
-
import * as datasets from "./tools/datasets.js";
|
|
18
|
-
import * as experiments from "./tools/experiments.js";
|
|
19
|
-
import * as evaluators from "./tools/evaluators.js";
|
|
20
|
-
import * as prompts from "./tools/prompts.js";
|
|
21
|
-
import * as skills from "./tools/skills.js";
|
|
22
|
-
import * as plugins from "./tools/plugins.js";
|
|
23
|
-
import * as providers from "./tools/providers.js";
|
|
24
|
-
import * as billing from "./tools/billing.js";
|
|
25
|
-
import * as scores from "./tools/scores.js";
|
|
26
|
-
import * as annotationQueues from "./tools/annotation-queues.js";
|
|
27
|
-
import * as tracing from "./tools/tracing.js";
|
|
28
|
-
import * as analytics from "./tools/analytics.js";
|
|
29
|
-
import * as docs from "./tools/docs.js";
|
|
30
|
-
import * as agents from "./tools/agents.js";
|
|
31
|
-
import * as conversations from "./tools/conversations.js";
|
|
32
|
-
import * as knowledge from "./tools/knowledge.js";
|
|
33
|
-
import * as files from "./tools/files.js";
|
|
9
|
+
import { registerAllTools } from "./tools/index.js";
|
|
34
10
|
// Read the canonical KW_* env var; fall back to legacy BACKBONE_* for
|
|
35
11
|
// backwards compatibility. Warn once on stderr when the legacy name supplied
|
|
36
12
|
// the value.
|
|
@@ -61,43 +37,19 @@ function requireEnv(canonical, legacy) {
|
|
|
61
37
|
}
|
|
62
38
|
const DEFAULT_BASE_URL = process.env.AI_2KW_DEFAULT_BASE_URL ??
|
|
63
39
|
process.env.BACKBONE_DEFAULT_BASE_URL ??
|
|
64
|
-
"https://
|
|
40
|
+
"https://api.2kw.ai";
|
|
65
41
|
async function main() {
|
|
66
42
|
const apiKey = requireEnv("AI_2KW_API_KEY", "BACKBONE_API_KEY");
|
|
67
43
|
const baseUrl = readEnv("AI_2KW_BASE_URL", "BACKBONE_BASE_URL") ?? DEFAULT_BASE_URL;
|
|
68
44
|
const transport = process.env.MCP_TRANSPORT ?? "stdio";
|
|
69
45
|
const httpPort = parseInt(process.env.MCP_HTTP_PORT ?? "3100", 10);
|
|
70
|
-
|
|
46
|
+
// Opt API-key runs into agent memory (X-Backbone-Memory: enabled on responses calls, #721).
|
|
47
|
+
const client = createApiClient(baseUrl, apiKey, { memory: process.env.AI_2KW_MEMORY === "true" });
|
|
71
48
|
const server = new McpServer({
|
|
72
49
|
name: "2kw",
|
|
73
50
|
version: pkg.version,
|
|
74
51
|
});
|
|
75
|
-
|
|
76
|
-
conversion.register(server, client);
|
|
77
|
-
extraction.register(server, client);
|
|
78
|
-
aiGateway.register(server, client);
|
|
79
|
-
transcription.register(server, client);
|
|
80
|
-
schemas.register(server, client);
|
|
81
|
-
schemaLabels.register(server, client);
|
|
82
|
-
schemaVersions.register(server, client);
|
|
83
|
-
schemaTesting.register(server, client);
|
|
84
|
-
datasets.register(server, client);
|
|
85
|
-
experiments.register(server, client);
|
|
86
|
-
evaluators.register(server, client);
|
|
87
|
-
prompts.register(server, client);
|
|
88
|
-
skills.register(server, client);
|
|
89
|
-
plugins.register(server, client);
|
|
90
|
-
providers.register(server, client);
|
|
91
|
-
billing.register(server, client);
|
|
92
|
-
scores.register(server, client);
|
|
93
|
-
annotationQueues.register(server, client);
|
|
94
|
-
tracing.register(server, client);
|
|
95
|
-
analytics.register(server, client);
|
|
96
|
-
docs.register(server, client, baseUrl, apiKey);
|
|
97
|
-
agents.register(server, client);
|
|
98
|
-
conversations.register(server, client);
|
|
99
|
-
knowledge.register(server, client);
|
|
100
|
-
files.register(server, client);
|
|
52
|
+
registerAllTools(server, { client, baseUrl, apiKey });
|
|
101
53
|
if (transport === "http") {
|
|
102
54
|
const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
103
55
|
const httpServer = createServer(async (req, res) => {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ApiClient } from "../client.js";
|
|
2
|
+
import type { components } from "../generated/openapi.js";
|
|
3
|
+
import { type ConversationMode, type ResponsesResult } from "./agent-run.js";
|
|
4
|
+
/**
|
|
5
|
+
* Decide the approvals a paused agent run is waiting for (#667; spec 2026-09-14-cli-agents-design.md
|
|
6
|
+
* §4.2, D10, D11). Mirrors `cli/src/lib/agent-decide.ts` and `agent-lookup.ts`, with a per-approval
|
|
7
|
+
* input instead of the CLI's flags.
|
|
8
|
+
*/
|
|
9
|
+
export type ApprovalRow = components["schemas"]["ToolApprovalDTO"];
|
|
10
|
+
export interface DecisionInput {
|
|
11
|
+
approvalId: string;
|
|
12
|
+
decision: "approve" | "reject";
|
|
13
|
+
reason?: string;
|
|
14
|
+
remember?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface DecideInput {
|
|
17
|
+
decisions?: DecisionInput[];
|
|
18
|
+
decideAll?: "approve" | "reject";
|
|
19
|
+
/** Only with `decideAll`: the reason stored on every decision. */
|
|
20
|
+
reason?: string;
|
|
21
|
+
/** Only with `decideAll`: remember every approval for the conversation. */
|
|
22
|
+
remember?: boolean;
|
|
23
|
+
/** One answer per relayed tool call of the paused response (#1254 M1 to M4). */
|
|
24
|
+
outputs?: ToolOutputInput[];
|
|
25
|
+
}
|
|
26
|
+
/** One relayed tool call answered by `outputs` (#1254 M2, M3). */
|
|
27
|
+
export interface ToolOutputInput {
|
|
28
|
+
callId: string;
|
|
29
|
+
/** Sent verbatim as the item's `output` string. */
|
|
30
|
+
output: string;
|
|
31
|
+
failed?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The `function_call_output` wire item (#480), byte for byte what the CLI and surface send. */
|
|
34
|
+
export interface ToolOutputItem {
|
|
35
|
+
type: "function_call_output";
|
|
36
|
+
call_id: string;
|
|
37
|
+
output: string;
|
|
38
|
+
failed?: true;
|
|
39
|
+
}
|
|
40
|
+
/** The #660 wire item, byte for byte what n8n Decide Approval and `bb agents decide` send. */
|
|
41
|
+
export interface ApprovalItem {
|
|
42
|
+
type: "backbone:approval_response";
|
|
43
|
+
approval_id: string;
|
|
44
|
+
decision: "approve" | "reject";
|
|
45
|
+
hmac: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
remember?: "conversation";
|
|
48
|
+
}
|
|
49
|
+
/** `name[@label][#model]`, split like the server: the model at the last `#`, then the label at the last `@`. */
|
|
50
|
+
export declare function splitAgentRef(ref: string): {
|
|
51
|
+
name: string;
|
|
52
|
+
label: string | undefined;
|
|
53
|
+
model: string | undefined;
|
|
54
|
+
};
|
|
55
|
+
/** A UUID is taken as the id; a name is matched exactly (the REST API only offers a substring `search`). */
|
|
56
|
+
export declare function resolveAgentId(client: ApiClient, name: string): Promise<string>;
|
|
57
|
+
export declare function fetchPendingApprovals(client: ApiClient, agentId: string, responseId: string): Promise<ApprovalRow[]>;
|
|
58
|
+
/**
|
|
59
|
+
* The backend refuses a continuation that leaves any pending approval of the paused response
|
|
60
|
+
* undecided (400 incomplete_tool_outputs, D10), so the whole set is checked here before sending.
|
|
61
|
+
*/
|
|
62
|
+
export declare function planDecisions(pending: ApprovalRow[], input: DecideInput, responseId: string): ApprovalItem[];
|
|
63
|
+
/**
|
|
64
|
+
* Refuses the same relayed call answered twice, before any request (#1254 M4). Completeness against
|
|
65
|
+
* the response's released calls is the server's check: without stored state this server cannot list them.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkToolOutputs(outputs: ToolOutputInput[]): void;
|
|
68
|
+
/** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
|
|
69
|
+
export declare function toToolOutputItems(outputs: ToolOutputInput[]): ToolOutputItem[];
|
|
70
|
+
/**
|
|
71
|
+
* The decisions and tool outputs a continuation sends (#1254 M5). A pure relay pause has no pending
|
|
72
|
+
* approval, so the approval input is optional: `decideAll` adds nothing (P1) and only `decisions` naming
|
|
73
|
+
* an id is refused. A mixed pause needs its approvals decided in the same call as `outputs`. Without
|
|
74
|
+
* `outputs` this delegates to {@link planDecisions} unchanged, approval-only behaviour included.
|
|
75
|
+
*/
|
|
76
|
+
export declare function planContinuation(pending: ApprovalRow[], input: DecideInput, responseId: string): {
|
|
77
|
+
outputs: ToolOutputItem[];
|
|
78
|
+
approvals: ApprovalItem[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
|
|
82
|
+
* instructions and model from this reference, so label and model must be the run's (D11). Never the
|
|
83
|
+
* echoed `name@<versionNumber>` — it would 404 as a label.
|
|
84
|
+
* With `mode`, the `backbone:mode` item follows the decisions (#656).
|
|
85
|
+
* `outputs` (#1254 M6) go first: the server replays accepted outputs ahead of other items, so the
|
|
86
|
+
* wire matches what is persisted.
|
|
87
|
+
*/
|
|
88
|
+
export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, items: ApprovalItem[], label?: string, model?: string, mode?: ConversationMode, outputs?: ToolOutputItem[]): Promise<ResponsesResult>;
|
|
89
|
+
/**
|
|
90
|
+
* `formatErrorForMcp`, with a hint appended for the two relay error codes (#1254 spec §3): every
|
|
91
|
+
* other tool keeps the plain text, since `formatErrorForMcp` itself is shared by every tool.
|
|
92
|
+
*/
|
|
93
|
+
export declare function decideErrorText(error: unknown): string;
|
|
94
|
+
//# sourceMappingURL=agent-decide.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { BackboneApiError, formatErrorForMcp } from "../errors.js";
|
|
2
|
+
import { modeItem } from "./agent-run.js";
|
|
3
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const PAGE_SIZE = 100;
|
|
5
|
+
const MAX_PAGES = 50;
|
|
6
|
+
/** `name[@label][#model]`, split like the server: the model at the last `#`, then the label at the last `@`. */
|
|
7
|
+
export function splitAgentRef(ref) {
|
|
8
|
+
const hash = ref.lastIndexOf("#");
|
|
9
|
+
const model = hash >= 0 ? ref.slice(hash + 1) : undefined;
|
|
10
|
+
const base = hash >= 0 ? ref.slice(0, hash) : ref;
|
|
11
|
+
const at = base.lastIndexOf("@");
|
|
12
|
+
if (at <= 0)
|
|
13
|
+
return { name: base, label: undefined, model };
|
|
14
|
+
return { name: base.slice(0, at), label: base.slice(at + 1), model };
|
|
15
|
+
}
|
|
16
|
+
/** A UUID is taken as the id; a name is matched exactly (the REST API only offers a substring `search`). */
|
|
17
|
+
export async function resolveAgentId(client, name) {
|
|
18
|
+
if (UUID.test(name))
|
|
19
|
+
return name;
|
|
20
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
21
|
+
const { data } = await client.GET("/v1/agents", {
|
|
22
|
+
// A stable sort: offset paging over an unordered result can skip rows between pages.
|
|
23
|
+
params: { query: { search: name, pageable: { page, size: PAGE_SIZE, sort: ["id,asc"] } } },
|
|
24
|
+
});
|
|
25
|
+
// Typed loosely, as 2kw_list_agents does: the generated page type of this endpoint is not a PageAgentDTO.
|
|
26
|
+
const pageData = data;
|
|
27
|
+
const content = pageData?.content ?? [];
|
|
28
|
+
const hit = content.find((a) => a.name === name);
|
|
29
|
+
if (hit?.id)
|
|
30
|
+
return String(hit.id);
|
|
31
|
+
if (pageData?.last !== false || content.length === 0)
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Agent '${name}' not found. List agents with 2kw_list_agents.`);
|
|
35
|
+
}
|
|
36
|
+
export async function fetchPendingApprovals(client, agentId, responseId) {
|
|
37
|
+
const rows = [];
|
|
38
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
39
|
+
const { data } = await client.GET("/v1/agents/{agentId}/approvals", {
|
|
40
|
+
params: { path: { agentId }, query: { status: "pending", pageable: { page, size: PAGE_SIZE } } },
|
|
41
|
+
});
|
|
42
|
+
const content = data?.content ?? [];
|
|
43
|
+
rows.push(...content.filter((r) => r.responseId === responseId));
|
|
44
|
+
if (data?.last !== false || content.length === 0)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The backend refuses a continuation that leaves any pending approval of the paused response
|
|
51
|
+
* undecided (400 incomplete_tool_outputs, D10), so the whole set is checked here before sending.
|
|
52
|
+
*/
|
|
53
|
+
export function planDecisions(pending, input, responseId) {
|
|
54
|
+
if (pending.length === 0) {
|
|
55
|
+
throw new Error(`No pending approvals for ${responseId} (already decided or superseded). A run waiting for client-side tool ` +
|
|
56
|
+
"output is answered with `outputs`.");
|
|
57
|
+
}
|
|
58
|
+
const hasList = input.decisions !== undefined && input.decisions.length > 0;
|
|
59
|
+
if (hasList === (input.decideAll !== undefined)) {
|
|
60
|
+
throw new Error("Give either `decisions` or `decideAll` (exactly one): one decision per pending approval, or one for all. " +
|
|
61
|
+
"Answer relayed tool calls with `outputs`.");
|
|
62
|
+
}
|
|
63
|
+
const chosen = new Map();
|
|
64
|
+
if (input.decideAll !== undefined) {
|
|
65
|
+
for (const r of pending) {
|
|
66
|
+
chosen.set(String(r.id), { approvalId: String(r.id), decision: input.decideAll, reason: input.reason, remember: input.remember });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const twice = new Set();
|
|
71
|
+
for (const d of input.decisions) {
|
|
72
|
+
if (chosen.has(d.approvalId))
|
|
73
|
+
twice.add(d.approvalId);
|
|
74
|
+
chosen.set(d.approvalId, d);
|
|
75
|
+
}
|
|
76
|
+
if (twice.size)
|
|
77
|
+
throw new Error(`Approvals decided more than once: ${[...twice].join(", ")}`);
|
|
78
|
+
const pendingIds = new Set(pending.map((r) => String(r.id)));
|
|
79
|
+
const foreign = [...chosen.keys()].filter((id) => !pendingIds.has(id));
|
|
80
|
+
if (foreign.length)
|
|
81
|
+
throw new Error(`Not pending on ${responseId}: ${foreign.join(", ")}`);
|
|
82
|
+
const undecided = [...pendingIds].filter((id) => !chosen.has(id));
|
|
83
|
+
if (undecided.length) {
|
|
84
|
+
throw new Error(`Every pending approval of a response must be decided in one call. Undecided: ${undecided.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return pending.map((r) => {
|
|
88
|
+
const id = String(r.id);
|
|
89
|
+
const d = chosen.get(id);
|
|
90
|
+
if (d.remember && d.decision !== "approve") {
|
|
91
|
+
throw new Error(`\`remember\` applies only to an approve: ${id}`);
|
|
92
|
+
}
|
|
93
|
+
if (d.remember && (r.policyClass ?? "").toUpperCase() === "DESTRUCTIVE") {
|
|
94
|
+
throw new Error(`\`remember\` cannot be used on a destructive tool call: ${id}`);
|
|
95
|
+
}
|
|
96
|
+
if (!r.hmac)
|
|
97
|
+
throw new Error(`Approval ${id} carries no hmac and cannot be decided here.`);
|
|
98
|
+
return {
|
|
99
|
+
type: "backbone:approval_response",
|
|
100
|
+
approval_id: id,
|
|
101
|
+
decision: d.decision,
|
|
102
|
+
hmac: r.hmac,
|
|
103
|
+
...(d.reason ? { reason: d.reason } : {}),
|
|
104
|
+
...(d.remember ? { remember: "conversation" } : {}),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Refuses the same relayed call answered twice, before any request (#1254 M4). Completeness against
|
|
110
|
+
* the response's released calls is the server's check: without stored state this server cannot list them.
|
|
111
|
+
*/
|
|
112
|
+
export function checkToolOutputs(outputs) {
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
const twice = new Set();
|
|
115
|
+
for (const o of outputs) {
|
|
116
|
+
if (seen.has(o.callId))
|
|
117
|
+
twice.add(o.callId);
|
|
118
|
+
seen.add(o.callId);
|
|
119
|
+
}
|
|
120
|
+
if (twice.size)
|
|
121
|
+
throw new Error(`Tool calls answered more than once: ${[...twice].join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
/** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
|
|
124
|
+
export function toToolOutputItems(outputs) {
|
|
125
|
+
return outputs.map((o) => ({
|
|
126
|
+
type: "function_call_output",
|
|
127
|
+
call_id: o.callId,
|
|
128
|
+
output: o.output,
|
|
129
|
+
...(o.failed ? { failed: true } : {}),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The decisions and tool outputs a continuation sends (#1254 M5). A pure relay pause has no pending
|
|
134
|
+
* approval, so the approval input is optional: `decideAll` adds nothing (P1) and only `decisions` naming
|
|
135
|
+
* an id is refused. A mixed pause needs its approvals decided in the same call as `outputs`. Without
|
|
136
|
+
* `outputs` this delegates to {@link planDecisions} unchanged, approval-only behaviour included.
|
|
137
|
+
*/
|
|
138
|
+
export function planContinuation(pending, input, responseId) {
|
|
139
|
+
const outputs = input.outputs ?? [];
|
|
140
|
+
if (outputs.length === 0) {
|
|
141
|
+
return { outputs: [], approvals: planDecisions(pending, input, responseId) };
|
|
142
|
+
}
|
|
143
|
+
checkToolOutputs(outputs);
|
|
144
|
+
const outputItems = toToolOutputItems(outputs);
|
|
145
|
+
if (pending.length === 0) {
|
|
146
|
+
const hasList = input.decisions !== undefined && input.decisions.length > 0;
|
|
147
|
+
// Both given is refused the same way planDecisions refuses it, regardless of pending state (#1254
|
|
148
|
+
// review): pending === 0 licenses only the P1 no-op, never a malformed pair of approval inputs.
|
|
149
|
+
if (hasList && input.decideAll !== undefined) {
|
|
150
|
+
throw new Error("Give either `decisions` or `decideAll` (exactly one): one decision per pending approval, or one for all. " +
|
|
151
|
+
"Answer relayed tool calls with `outputs`.");
|
|
152
|
+
}
|
|
153
|
+
if (hasList) {
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const twice = new Set();
|
|
156
|
+
for (const d of input.decisions) {
|
|
157
|
+
if (seen.has(d.approvalId))
|
|
158
|
+
twice.add(d.approvalId);
|
|
159
|
+
seen.add(d.approvalId);
|
|
160
|
+
}
|
|
161
|
+
if (twice.size)
|
|
162
|
+
throw new Error(`Approvals decided more than once: ${[...twice].join(", ")}`);
|
|
163
|
+
throw new Error(`Not pending on ${responseId}: ${input.decisions.map((d) => d.approvalId).join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
return { outputs: outputItems, approvals: [] };
|
|
166
|
+
}
|
|
167
|
+
const hasApprovalInput = (input.decisions !== undefined && input.decisions.length > 0) || input.decideAll !== undefined;
|
|
168
|
+
if (!hasApprovalInput) {
|
|
169
|
+
throw new Error(`${responseId} also waits for approvals: every relayed tool call and every pending approval must be answered ` +
|
|
170
|
+
`in the same call as \`outputs\`. Undecided: ${pending.map((r) => String(r.id)).join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
return { outputs: outputItems, approvals: planDecisions(pending, input, responseId) };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
|
|
176
|
+
* instructions and model from this reference, so label and model must be the run's (D11). Never the
|
|
177
|
+
* echoed `name@<versionNumber>` — it would 404 as a label.
|
|
178
|
+
* With `mode`, the `backbone:mode` item follows the decisions (#656).
|
|
179
|
+
* `outputs` (#1254 M6) go first: the server replays accepted outputs ahead of other items, so the
|
|
180
|
+
* wire matches what is persisted.
|
|
181
|
+
*/
|
|
182
|
+
export async function continueWithDecisions(client, agentId, responseId, items, label, model, mode, outputs = []) {
|
|
183
|
+
const ordered = [...outputs, ...items];
|
|
184
|
+
const body = {
|
|
185
|
+
model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
|
|
186
|
+
previous_response_id: responseId,
|
|
187
|
+
stream: false,
|
|
188
|
+
// With `mode`, the backbone:mode item follows the decisions (#656 D7); without it, nothing is added (D2).
|
|
189
|
+
input: mode ? [...ordered, modeItem(mode)] : ordered,
|
|
190
|
+
};
|
|
191
|
+
// The generated body type does not model backbone: extension items, hence the cast.
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
+
const { data } = await client.POST("/v1/responses", { body });
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
/** The gateway error code appended to `BackboneApiError.errorType` on the two relay codes (#1254 spec §3). */
|
|
197
|
+
const RELAY_ERROR_HINTS = {
|
|
198
|
+
incomplete_tool_outputs: "every released tool call and every pending approval of a paused response must be answered in one call. " +
|
|
199
|
+
"Check `pendingToolCalls` and `pendingApprovals` in the run envelope.",
|
|
200
|
+
unknown_tool_output: "that call id was not released by this response. Use the responseId of the latest envelope.",
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* `formatErrorForMcp`, with a hint appended for the two relay error codes (#1254 spec §3): every
|
|
204
|
+
* other tool keeps the plain text, since `formatErrorForMcp` itself is shared by every tool.
|
|
205
|
+
*/
|
|
206
|
+
export function decideErrorText(error) {
|
|
207
|
+
const text = formatErrorForMcp(error);
|
|
208
|
+
if (!(error instanceof BackboneApiError))
|
|
209
|
+
return text;
|
|
210
|
+
// hasOwn: an errorType named after an Object.prototype key (e.g. "toString") must not resolve to
|
|
211
|
+
// an inherited member — the gateway's error code is server-controlled, but never trusted as a key
|
|
212
|
+
// into a plain object (#1254 review; same guard as chatUrlFor in agent-run.ts).
|
|
213
|
+
const hint = Object.hasOwn(RELAY_ERROR_HINTS, error.errorType) ? RELAY_ERROR_HINTS[error.errorType] : undefined;
|
|
214
|
+
return hint ? `${text}\nHint: ${hint}` : text;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=agent-decide.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
|
|
3
|
+
* from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
|
|
4
|
+
* only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
|
|
5
|
+
* The connect pause is not copied by hand: `connect-pause.ts` is a guarded byte copy of the CLI's (#1086).
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
8
|
+
type AnyRecord = Record<string, any>;
|
|
9
|
+
export type RunStatus = "completed" | "requires_approval" | "requires_tool_output" | "incomplete";
|
|
10
|
+
export interface PendingApproval {
|
|
11
|
+
approvalId: string;
|
|
12
|
+
callId: string;
|
|
13
|
+
tool: string;
|
|
14
|
+
arguments: unknown;
|
|
15
|
+
policyClass: string;
|
|
16
|
+
/** Why the automatic approver escalated this request to a human (#634); null when it did not. */
|
|
17
|
+
reason: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ToolCallSummary {
|
|
20
|
+
tool: string;
|
|
21
|
+
callId: string;
|
|
22
|
+
/** `incomplete` when the server-side tool run failed (its `function_call_output` carries that status). */
|
|
23
|
+
status: "completed" | "incomplete";
|
|
24
|
+
}
|
|
25
|
+
export interface PendingToolCall {
|
|
26
|
+
tool: string;
|
|
27
|
+
callId: string;
|
|
28
|
+
arguments: unknown;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A connector the run waits for the user to connect, allow or reconnect in chat.2kw.ai: an open
|
|
32
|
+
* `backbone:connector_auth_request` (#807 R12). The caller never answers the connect call; the
|
|
33
|
+
* continuation re-checks access itself.
|
|
34
|
+
*/
|
|
35
|
+
export interface PendingConnection {
|
|
36
|
+
serverLabel: string;
|
|
37
|
+
host: string;
|
|
38
|
+
/** `connect`, `allow` or `reconnect`. */
|
|
39
|
+
reason: string;
|
|
40
|
+
/** The egress difference an allow is re-asked for; absent otherwise. */
|
|
41
|
+
destinations?: string[];
|
|
42
|
+
}
|
|
43
|
+
export { DEFAULT_CHAT_URL };
|
|
44
|
+
/**
|
|
45
|
+
* The chat web host that belongs to the server's API base URL (`AI_2KW_BASE_URL`), where a member
|
|
46
|
+
* connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped. The
|
|
47
|
+
* mapping itself is the shared `connect-pause.ts`'s {@link chatOriginFor} (#1086).
|
|
48
|
+
*/
|
|
49
|
+
export declare function chatUrlFor(baseUrl: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
50
|
+
/** The end user's conversation mode (epic &59); the server matches these three values exactly. */
|
|
51
|
+
export type ConversationMode = "plan" | "ask" | "auto";
|
|
52
|
+
export declare const CONVERSATION_MODES: readonly ConversationMode[];
|
|
53
|
+
/**
|
|
54
|
+
* Who may change the mode (#656 D8), stated in both tool descriptions: the mode outlives the request
|
|
55
|
+
* that sets it, so a model that switched on its own would lift the user's choice for every later turn.
|
|
56
|
+
*/
|
|
57
|
+
export declare const MODE_RULE: string;
|
|
58
|
+
/** The `backbone:mode` input item (S1 D7); always appended last. */
|
|
59
|
+
export declare function modeItem(mode: ConversationMode): {
|
|
60
|
+
type: "backbone:mode";
|
|
61
|
+
mode: ConversationMode;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
|
|
65
|
+
* and any value other than the three read as null, which also means "none set".
|
|
66
|
+
*/
|
|
67
|
+
export declare function responseMode(result: ResponsesResult | undefined): ConversationMode | null;
|
|
68
|
+
export interface RunEnvelope {
|
|
69
|
+
status: RunStatus;
|
|
70
|
+
/** The conversation mode the request ran under (`conversation_mode`); null when none is set. */
|
|
71
|
+
mode: ConversationMode | null;
|
|
72
|
+
agent: string | null;
|
|
73
|
+
version: number | null;
|
|
74
|
+
responseId: string | null;
|
|
75
|
+
conversationId: string | null;
|
|
76
|
+
text: string;
|
|
77
|
+
toolCalls: ToolCallSummary[];
|
|
78
|
+
pendingApprovals: PendingApproval[];
|
|
79
|
+
pendingToolCalls: PendingToolCall[];
|
|
80
|
+
pendingConnections: PendingConnection[];
|
|
81
|
+
incompleteReason: string | null;
|
|
82
|
+
usage: {
|
|
83
|
+
inputTokens: number;
|
|
84
|
+
outputTokens: number;
|
|
85
|
+
} | null;
|
|
86
|
+
next: string | null;
|
|
87
|
+
}
|
|
88
|
+
/** Loose wire shape of a `POST /v1/responses` result; the OpenAPI spec types it as `object`. */
|
|
89
|
+
export interface ResponsesResult {
|
|
90
|
+
id?: string;
|
|
91
|
+
status?: string;
|
|
92
|
+
model?: string;
|
|
93
|
+
output?: AnyRecord[];
|
|
94
|
+
usage?: {
|
|
95
|
+
input_tokens?: number;
|
|
96
|
+
output_tokens?: number;
|
|
97
|
+
total_tokens?: number;
|
|
98
|
+
};
|
|
99
|
+
conversation?: {
|
|
100
|
+
id?: string;
|
|
101
|
+
} | null;
|
|
102
|
+
incomplete_details?: {
|
|
103
|
+
reason?: string;
|
|
104
|
+
} | null;
|
|
105
|
+
conversation_mode?: string | null;
|
|
106
|
+
}
|
|
107
|
+
/** The assistant's text: every `output_text` part of every `message` item, in order. */
|
|
108
|
+
export declare function extractResponseText(result: ResponsesResult | undefined): string;
|
|
109
|
+
/**
|
|
110
|
+
* The response echoes `model` as `agent/{name}@{versionNumber}` (all digits), optionally
|
|
111
|
+
* followed by a `#<model>` override. Only a trailing all-digit `@N` is read as a version;
|
|
112
|
+
* any other `@` suffix stays part of the name.
|
|
113
|
+
*/
|
|
114
|
+
export declare function parseAgentModel(model?: string): {
|
|
115
|
+
agent: string | null;
|
|
116
|
+
version: number | null;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
|
|
120
|
+
* the decision continues on the same label and model (D11). Falls back to the echoed agent name.
|
|
121
|
+
* @param chatUrl the chat web host of the API ({@link chatUrlFor}); a connect pause's `next` names its
|
|
122
|
+
* Connectors page.
|
|
123
|
+
*/
|
|
124
|
+
export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string, chatUrl?: string): RunEnvelope;
|
|
125
|
+
//# sourceMappingURL=agent-run.d.ts.map
|