@beeos-ai/bma-dsh-acp 0.1.0 → 0.2.1
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 +9 -7
- package/lib/agent.js +71 -16
- package/lib/protocol.js +5 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ It has no DSH dependency of its own (test `pins`), runs the shared conformance s
|
|
|
7
7
|
## Install and run (npm)
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx --yes @beeos-ai/bma-dsh-acp@0.1
|
|
10
|
+
npx --yes @beeos-ai/bma-dsh-acp@0.2.1 # one-off; or: npm install -g @beeos-ai/bma-dsh-acp
|
|
11
11
|
BMA_DSH_ACP_HOME=$HOME/.bma-dsh-acp bma-dsh-acp # ACP (JSON-RPC, newline-delimited) on stdin/stdout
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -29,10 +29,10 @@ Using it from the BMA executor SDK (`bma-executor-sdk`, Go `ACPAgent`; the SDK n
|
|
|
29
29
|
|
|
30
30
|
```go
|
|
31
31
|
agent, err := bmaexecutor.NewACPAgent(ctx, bmaexecutor.ACPAgentOptions{
|
|
32
|
-
// installed once with `npm install -g @beeos-ai/bma-dsh-acp@0.1
|
|
32
|
+
// installed once with `npm install -g @beeos-ai/bma-dsh-acp@0.2.1` (the `bma-dsh-acp` bin of that prefix):
|
|
33
33
|
Command: []string{"/usr/local/bin/bma-dsh-acp"},
|
|
34
34
|
// or fetched on first use (slow first start; the cache keeps it afterwards):
|
|
35
|
-
// Command: []string{"/usr/local/bin/npx", "--yes", "@beeos-ai/bma-dsh-acp@0.1
|
|
35
|
+
// Command: []string{"/usr/local/bin/npx", "--yes", "@beeos-ai/bma-dsh-acp@0.2.1"},
|
|
36
36
|
StateDir: stateDir, // becomes BMA_DSH_ACP_HOME
|
|
37
37
|
Core: core,
|
|
38
38
|
Upstream: bmaexecutor.ACPModelUpstream{BaseURL: "https://api.example.com/v1", Model: "my-model"},
|
|
@@ -45,14 +45,16 @@ Standard ACP v1 (`@agentclientprotocol/sdk`), plus BMA additions under the `bma`
|
|
|
45
45
|
|
|
46
46
|
| ACP | bma-dsh-acp |
|
|
47
47
|
|---|---|
|
|
48
|
-
| `initialize` | `loadSession`, image prompts; `_meta.bma` = `{dsh, tools, streaming: "token", permissionRelay: "full", modes}` |
|
|
49
|
-
| `session/new`, `session/load` | `_meta.bma` = `{sessionId?, systemPrompt?, model: {baseUrl, token, model, contextWindow?, maxOutputTokens?}}` (required: no model request without an attributable route). `sessionId` = platform session id = DSH session id. Response `_meta.bma.toolCatalog` (the six CMA tools; anything else is refused, fail-closed). `load` replays the conversation as `session/update`; an unknown session is `-32002` |
|
|
48
|
+
| `initialize` | `loadSession`, image prompts; `_meta.bma` = `{dsh, tools, hostTools, streaming: "token", permissionRelay: "full", modes}`. Request `_meta.bma.hostTools` = `[{name, description, parameters, readOnly, kind, permission?}]`: tools the client's machine adds to the six (for example `computer_observe`, `computer_action`, `ask_user_question`); registered for every session, asked for by `permission` (`ask` by default, `allow`), run by the client like the six |
|
|
49
|
+
| `session/new`, `session/load` | `_meta.bma` = `{sessionId?, systemPrompt?, model: {baseUrl, token, model, contextWindow?, maxOutputTokens?}}` (required: no model request without an attributable route). `sessionId` = platform session id = DSH session id. Response `_meta.bma.toolCatalog` (the six CMA tools plus the declared host tools; anything else is refused, fail-closed). `load` replays the conversation as `session/update`; an unknown session is `-32002` |
|
|
50
50
|
| `session/set_mode` | `default` (ask before bash/write/edit) or `accept_edits` (ask before bash); read/glob/grep never ask |
|
|
51
51
|
| `session/prompt` | `_meta.bma` = `{inputId?, toolPolicy?}` (`toolPolicy`: per-tool allow/ask/deny for this prompt, e.g. from turn.start `tool_policy`). Streams `agent_message_chunk` / `agent_thought_chunk` with a `messageId`; the durable text of that message is the notification `_bma/message {sessionId, messageId, kind, text}` |
|
|
52
|
-
| tool call | `tool_call` (`kind`, full `rawInput`, `_meta.bma {name, permission}`) → `session/request_permission` (full input; `allow_once` / `reject_once`) when the policy says ask → request `_bma/tools/call {sessionId, toolCallId, name, input}` → `{text, isError}` (the client runs it) → `tool_call_update` completed/failed with the result |
|
|
52
|
+
| tool call | `tool_call` (`kind`, full `rawInput`, `_meta.bma {name, permission}`) → `session/request_permission` (full input; `allow_once` / `reject_once`) when the policy says ask → request `_bma/tools/call {sessionId, toolCallId, name, input}` → `{text, isError, images?: [{mimeType, data}]}` (the client runs it; images are shown to the model after the tool message, as a user message — Chat Completions tool messages carry text only) → `tool_call_update` completed/failed with the result |
|
|
53
53
|
| `session/cancel` | the turn ends with `stopReason: cancelled`; a pending permission or tool call is abandoned (`$/cancel_request` is sent for it) |
|
|
54
54
|
| stops | `output_limit_reached` → `stopReason: max_tokens` + `_meta.bma {stopCode, detail}`; `context_too_large`, `model_stream_idle` → JSON-RPC error `-32603` with `data.bma {stopCode, detail}`; other failures → `-32603` |
|
|
55
|
-
| `session/close` | releases the session (its JSONL stays
|
|
55
|
+
| `session/close` | releases the session's agent scope (its memory; its JSONL stays, so `session/load` brings the whole conversation back); advertised as `agentCapabilities.sessionCapabilities.close`. A client that keeps many sessions (the executor SDK's `ACPAgent` keeps at most `MaxResidentSessions` and closes the least recently used idle one) is what bounds the memory of this process: it holds every session that was opened and not closed. Closing a session that is not open answers `-32002`, which the ACP SDK treats as closed |
|
|
56
|
+
|
|
57
|
+
Sessions share one DSH kernel in the process; each session is its own agent scope (conversation, tool catalog, model route, permissions): session-level isolation, not one DSH context per session.
|
|
56
58
|
|
|
57
59
|
Environment: `BMA_DSH_ACP_HOME` (state directory, required), `BMA_DSH_ACP_STREAM_IDLE_MS`, `BRAIN_MODEL_CATALOG`. stdout carries ACP only (console output goes to stderr).
|
|
58
60
|
|
package/lib/agent.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// bma-dsh-acp: the shared DSH adapter served as an ACP agent (ADR 0027).
|
|
2
2
|
//
|
|
3
3
|
// ACP shared adapter / client
|
|
4
|
-
// initialize capabilities + _meta.bma facts (dsh version, tools, streaming, permission relay)
|
|
4
|
+
// initialize capabilities + _meta.bma facts (dsh version, tools, streaming, permission relay);
|
|
5
|
+
// request _meta.bma.hostTools adds the client machine's own tools to the six
|
|
5
6
|
// session/new | load createSession | resumeSession (+ history replay); _meta.bma carries the model route
|
|
7
|
+
// session/close releases the session's agent scope (memory); its JSONL stays for session/load. Advertised as
|
|
8
|
+
// sessionCapabilities.close, so a client can keep many sessions by closing the least recently used
|
|
6
9
|
// session/set_mode per-session tool policy (default | accept_edits)
|
|
7
10
|
// session/prompt submitInput: deltas -> agent_*_chunk (messageId), durable -> _bma/message
|
|
8
11
|
// tool call tool_call -> [request_permission] -> _bma/tools/call (client runs it) -> tool_call_update
|
|
@@ -20,6 +23,8 @@ import { TOOL_SPECS } from "./tools.js";
|
|
|
20
23
|
export const VERSION = shellPkg.version;
|
|
21
24
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
22
25
|
const MEDIA = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
26
|
+
const HOST_TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
27
|
+
const KINDS = new Set(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "switch_mode", "other"]);
|
|
23
28
|
export class DshAcpShell {
|
|
24
29
|
app;
|
|
25
30
|
client;
|
|
@@ -27,12 +32,16 @@ export class DshAcpShell {
|
|
|
27
32
|
declared = new Map();
|
|
28
33
|
adapter;
|
|
29
34
|
catalog;
|
|
35
|
+
/** This build decodes images (DshAdapter.probeImageInput); without it image prompts are refused. */
|
|
36
|
+
imageInput = false;
|
|
37
|
+
/** The client machine's tools (initialize _meta.bma.hostTools). */
|
|
38
|
+
hostTools = [];
|
|
30
39
|
constructor(adapter, catalog) {
|
|
31
40
|
this.adapter = adapter;
|
|
32
41
|
this.catalog = catalog;
|
|
33
42
|
this.app = acpAgent({ name: "bma-dsh-acp" })
|
|
34
43
|
.onConnect((conn) => { this.client = conn.client; })
|
|
35
|
-
.onRequest("initialize", () => this.initialize())
|
|
44
|
+
.onRequest("initialize", ({ params }) => this.initialize(params))
|
|
36
45
|
.onRequest("session/new", ({ params }) => this.open(params, false))
|
|
37
46
|
.onRequest("session/load", ({ params }) => this.open(params, true))
|
|
38
47
|
.onRequest("session/set_mode", ({ params }) => this.setMode(params))
|
|
@@ -52,9 +61,13 @@ export class DshAcpShell {
|
|
|
52
61
|
return { baseUrl: s.model.baseUrl, token: s.model.token, model: s.model.model };
|
|
53
62
|
},
|
|
54
63
|
firstChunk: () => { },
|
|
64
|
+
idleRetry: (sid, result) => { process.stderr.write(`bma-dsh-acp: model stream idle before any content (session ${sid}): ${result}\n`); },
|
|
55
65
|
usage: () => { }, // metered by the client's model proxy
|
|
56
66
|
});
|
|
57
|
-
|
|
67
|
+
shell.registerTools();
|
|
68
|
+
shell.imageInput = await adapter.probeImageInput();
|
|
69
|
+
if (!shell.imageInput)
|
|
70
|
+
process.stderr.write("bma-dsh-acp: image input is unavailable in this build (the image decoder could not be loaded); image prompts are refused\n");
|
|
58
71
|
return shell;
|
|
59
72
|
}
|
|
60
73
|
async dispose() {
|
|
@@ -62,13 +75,23 @@ export class DshAcpShell {
|
|
|
62
75
|
s.running?.abort();
|
|
63
76
|
await this.adapter.dispose();
|
|
64
77
|
}
|
|
65
|
-
|
|
78
|
+
registerTools() {
|
|
79
|
+
this.adapter.registerTools([...TOOL_SPECS, ...this.hostTools].map((spec) => this.tool(spec)));
|
|
80
|
+
}
|
|
81
|
+
toolNames() {
|
|
82
|
+
return [...TOOL_NAMES, ...this.hostTools.map((t) => t.name)];
|
|
83
|
+
}
|
|
84
|
+
initialize(params) {
|
|
85
|
+
if (this.sessions.size)
|
|
86
|
+
throw RequestError.invalidRequest(undefined, "initialize after a session was opened");
|
|
87
|
+
this.hostTools = hostToolsMeta(params._meta);
|
|
88
|
+
this.registerTools();
|
|
66
89
|
return {
|
|
67
90
|
protocolVersion: PROTOCOL_VERSION,
|
|
68
|
-
agentCapabilities: { loadSession: true, promptCapabilities: { image:
|
|
91
|
+
agentCapabilities: { loadSession: true, sessionCapabilities: { close: {} }, promptCapabilities: { image: this.imageInput, audio: false, embeddedContext: false }, mcpCapabilities: { http: false, sse: false } },
|
|
69
92
|
agentInfo: { name: "bma-dsh-acp", title: "BMA DSH", version: VERSION },
|
|
70
93
|
authMethods: [],
|
|
71
|
-
_meta: { [META]: { dsh: DSH_VERSION, tools: TOOL_NAMES, streaming: "token", permissionRelay: "full", modes: Object.keys(MODES) } },
|
|
94
|
+
_meta: { [META]: { dsh: DSH_VERSION, tools: TOOL_NAMES, hostTools: this.hostTools.map((t) => t.name), streaming: "token", permissionRelay: "full", modes: Object.keys(MODES) } },
|
|
72
95
|
};
|
|
73
96
|
}
|
|
74
97
|
async open(params, load) {
|
|
@@ -86,7 +109,7 @@ export class DshAcpShell {
|
|
|
86
109
|
open.model = meta.model; // a reload in the same process: new route, same conversation
|
|
87
110
|
}
|
|
88
111
|
else {
|
|
89
|
-
const s = { id, model: meta.model, mode: "default", policy: modePolicy("default"), ids: freshIds(), chain: Promise.resolve() };
|
|
112
|
+
const s = { id, model: meta.model, mode: "default", policy: modePolicy("default", this.hostTools), ids: freshIds(), chain: Promise.resolve() };
|
|
90
113
|
this.sessions.set(id, s); // the route must exist before the first model request
|
|
91
114
|
const init = { sessionId: id, model: meta.model.model, systemPrompt: meta.systemPrompt ?? "", cwd: params.cwd };
|
|
92
115
|
try {
|
|
@@ -103,7 +126,7 @@ export class DshAcpShell {
|
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
const catalog = [...this.adapter.toolCatalog(id)];
|
|
106
|
-
const violation = catalogViolation("dsh",
|
|
129
|
+
const violation = catalogViolation("dsh", this.toolNames(), catalog);
|
|
107
130
|
if (violation) {
|
|
108
131
|
await this.close(id);
|
|
109
132
|
throw RequestError.internalError(undefined, violation);
|
|
@@ -131,16 +154,18 @@ export class DshAcpShell {
|
|
|
131
154
|
throw RequestError.invalidParams(undefined, `unknown mode ${params.modeId}`);
|
|
132
155
|
s.mode = params.modeId;
|
|
133
156
|
if (!s.running)
|
|
134
|
-
s.policy = modePolicy(s.mode);
|
|
157
|
+
s.policy = modePolicy(s.mode, this.hostTools);
|
|
135
158
|
return {};
|
|
136
159
|
}
|
|
137
160
|
async prompt(params) {
|
|
138
161
|
const s = this.get(params.sessionId);
|
|
139
162
|
if (s.running)
|
|
140
163
|
throw RequestError.invalidRequest(undefined, "a prompt is already running in this session");
|
|
141
|
-
const meta = promptMeta(params._meta);
|
|
142
|
-
s.policy = { ...modePolicy(s.mode), ...meta.toolPolicy };
|
|
164
|
+
const meta = promptMeta(params._meta, this.toolNames());
|
|
165
|
+
s.policy = { ...modePolicy(s.mode, this.hostTools), ...meta.toolPolicy };
|
|
143
166
|
const input = { id: meta.inputId ?? randomUUID(), ...promptInput(params.prompt) };
|
|
167
|
+
if (input.images && !this.imageInput)
|
|
168
|
+
throw RequestError.invalidParams(undefined, "image input is unavailable in this build of bma-dsh-acp");
|
|
144
169
|
const ctl = new AbortController();
|
|
145
170
|
s.running = ctl;
|
|
146
171
|
try {
|
|
@@ -166,7 +191,7 @@ export class DshAcpShell {
|
|
|
166
191
|
}
|
|
167
192
|
finally {
|
|
168
193
|
s.running = undefined;
|
|
169
|
-
s.policy = modePolicy(s.mode);
|
|
194
|
+
s.policy = modePolicy(s.mode, this.hostTools);
|
|
170
195
|
s.ids = freshIds();
|
|
171
196
|
}
|
|
172
197
|
}
|
|
@@ -211,7 +236,7 @@ export class DshAcpShell {
|
|
|
211
236
|
async invoke(spec, call) {
|
|
212
237
|
const s = this.get(call.sessionId);
|
|
213
238
|
const client = this.client;
|
|
214
|
-
const permission = s.policy[spec.name];
|
|
239
|
+
const permission = s.policy[spec.name] ?? "ask";
|
|
215
240
|
// Let the turn's queued message events go out first (they were produced before this call).
|
|
216
241
|
await new Promise((resolve) => setImmediate(resolve));
|
|
217
242
|
s.ids = freshIds();
|
|
@@ -247,7 +272,9 @@ export class DshAcpShell {
|
|
|
247
272
|
const result = await untilAborted(client.request(TOOLS_CALL, { sessionId: s.id, toolCallId: call.callId, name: spec.name, input: call.input }, { cancellationSignal: call.signal }), call.signal);
|
|
248
273
|
if (!result)
|
|
249
274
|
return finish(aborted());
|
|
250
|
-
|
|
275
|
+
const images = (result.images ?? []).filter((i) => MEDIA.has(i.mimeType) && typeof i.data === "string" && i.data)
|
|
276
|
+
.map((i) => ({ mediaType: i.mimeType, data: i.data }));
|
|
277
|
+
return finish({ text: String(result.text ?? ""), isError: Boolean(result.isError), ...(images.length ? { images } : {}) });
|
|
251
278
|
}
|
|
252
279
|
catch (err) {
|
|
253
280
|
if (call.signal.aborted)
|
|
@@ -317,7 +344,35 @@ function sessionMeta(raw) {
|
|
|
317
344
|
model: { baseUrl: String(model.baseUrl), token: model.token, model: model.model, ...(contextWindow ? { contextWindow } : {}), ...(maxOutputTokens ? { maxOutputTokens } : {}) },
|
|
318
345
|
};
|
|
319
346
|
}
|
|
320
|
-
function
|
|
347
|
+
function hostToolsMeta(raw) {
|
|
348
|
+
const tools = bmaMeta(raw)?.hostTools;
|
|
349
|
+
if (tools === undefined)
|
|
350
|
+
return [];
|
|
351
|
+
if (!Array.isArray(tools))
|
|
352
|
+
throw RequestError.invalidParams(undefined, "_meta.bma.hostTools must be an array");
|
|
353
|
+
const seen = new Set(TOOL_NAMES);
|
|
354
|
+
return tools.map((t, i) => {
|
|
355
|
+
const at = `_meta.bma.hostTools[${i}]`;
|
|
356
|
+
if (typeof t !== "object" || t === null)
|
|
357
|
+
throw RequestError.invalidParams(undefined, `${at} must be an object`);
|
|
358
|
+
if (typeof t.name !== "string" || !HOST_TOOL_NAME.test(t.name) || seen.has(t.name))
|
|
359
|
+
throw RequestError.invalidParams(undefined, `${at}.name must be a new snake_case tool name`);
|
|
360
|
+
seen.add(t.name);
|
|
361
|
+
if (typeof t.description !== "string" || !t.description)
|
|
362
|
+
throw RequestError.invalidParams(undefined, `${at}.description is required`);
|
|
363
|
+
const p = t.parameters;
|
|
364
|
+
if (typeof p !== "object" || p === null || p.type !== "object")
|
|
365
|
+
throw RequestError.invalidParams(undefined, `${at}.parameters must be an object JSON Schema`);
|
|
366
|
+
if (typeof t.readOnly !== "boolean")
|
|
367
|
+
throw RequestError.invalidParams(undefined, `${at}.readOnly must be a boolean`);
|
|
368
|
+
if (typeof t.kind !== "string" || !KINDS.has(t.kind))
|
|
369
|
+
throw RequestError.invalidParams(undefined, `${at}.kind must be an ACP tool kind`);
|
|
370
|
+
if (t.permission !== undefined && t.permission !== "allow" && t.permission !== "ask")
|
|
371
|
+
throw RequestError.invalidParams(undefined, `${at}.permission must be allow or ask`);
|
|
372
|
+
return { name: t.name, description: t.description, parameters: p, readOnly: t.readOnly, kind: t.kind, ...(t.permission ? { permission: t.permission } : {}) };
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function promptMeta(raw, names) {
|
|
321
376
|
const meta = bmaMeta(raw) ?? {};
|
|
322
377
|
const out = {};
|
|
323
378
|
if (meta.inputId !== undefined) {
|
|
@@ -330,7 +385,7 @@ function promptMeta(raw) {
|
|
|
330
385
|
if (typeof policy !== "object" || policy === null)
|
|
331
386
|
throw RequestError.invalidParams(undefined, "_meta.bma.toolPolicy must be an object");
|
|
332
387
|
for (const [name, value] of Object.entries(policy)) {
|
|
333
|
-
if (!
|
|
388
|
+
if (!names.includes(name) || !["allow", "ask", "deny"].includes(value)) {
|
|
334
389
|
throw RequestError.invalidParams(undefined, `_meta.bma.toolPolicy.${name} must be allow, ask or deny for a known tool`);
|
|
335
390
|
}
|
|
336
391
|
}
|
package/lib/protocol.js
CHANGED
|
@@ -7,8 +7,11 @@ export const MODES = {
|
|
|
7
7
|
default: { name: "Ask before changes", description: "Ask before bash, write and edit; read, glob and grep run without asking." },
|
|
8
8
|
accept_edits: { name: "Accept edits", description: "Ask before bash; write, edit and the read-only tools run without asking." },
|
|
9
9
|
};
|
|
10
|
-
export function modePolicy(mode) {
|
|
10
|
+
export function modePolicy(mode, hostTools = []) {
|
|
11
11
|
const ask = "ask";
|
|
12
12
|
const edits = mode === "accept_edits" ? "allow" : "ask";
|
|
13
|
-
|
|
13
|
+
const policy = { bash: ask, write: edits, edit: edits, read: "allow", glob: "allow", grep: "allow" };
|
|
14
|
+
for (const tool of hostTools)
|
|
15
|
+
policy[tool.name] = tool.permission ?? "ask";
|
|
16
|
+
return policy;
|
|
14
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beeos-ai/bma-dsh-acp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "bma-dsh-acp: the shared DSH adapter served as an Agent Client Protocol (stdio) agent, driven by the BMA executor's ACP client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"agent-client-protocol",
|
|
11
11
|
"managed-agents"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://bma.beeos.ai",
|
|
13
|
+
"homepage": "https://staging.bma.beeos.ai",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/beeos-ai/beeos-managed-agents.git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@agentclientprotocol/sdk": "2.2.0",
|
|
29
29
|
"zod": "4.6.7",
|
|
30
|
-
"@beeos-ai/bma-dsh-adapter": "0.1
|
|
30
|
+
"@beeos-ai/bma-dsh-adapter": "0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"esbuild": "0.28.2"
|