@beeos-ai/bma-dsh-acp 0.1.0 → 0.2.0
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 +8 -6
- package/lib/agent.js +69 -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.
|
|
10
|
+
npx --yes @beeos-ai/bma-dsh-acp@0.2.0 # 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.
|
|
32
|
+
// installed once with `npm install -g @beeos-ai/bma-dsh-acp@0.2.0` (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.
|
|
35
|
+
// Command: []string{"/usr/local/bin/npx", "--yes", "@beeos-ai/bma-dsh-acp@0.2.0"},
|
|
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,15 +45,17 @@ 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
55
|
| `session/close` | releases the session (its JSONL stays for a later load) |
|
|
56
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.
|
|
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
|
|
|
59
61
|
## Build (bundle and npm form)
|
package/lib/agent.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
6
7
|
// session/set_mode per-session tool policy (default | accept_edits)
|
|
7
8
|
// session/prompt submitInput: deltas -> agent_*_chunk (messageId), durable -> _bma/message
|
|
@@ -20,6 +21,8 @@ import { TOOL_SPECS } from "./tools.js";
|
|
|
20
21
|
export const VERSION = shellPkg.version;
|
|
21
22
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
22
23
|
const MEDIA = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
24
|
+
const HOST_TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
25
|
+
const KINDS = new Set(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "switch_mode", "other"]);
|
|
23
26
|
export class DshAcpShell {
|
|
24
27
|
app;
|
|
25
28
|
client;
|
|
@@ -27,12 +30,16 @@ export class DshAcpShell {
|
|
|
27
30
|
declared = new Map();
|
|
28
31
|
adapter;
|
|
29
32
|
catalog;
|
|
33
|
+
/** This build decodes images (DshAdapter.probeImageInput); without it image prompts are refused. */
|
|
34
|
+
imageInput = false;
|
|
35
|
+
/** The client machine's tools (initialize _meta.bma.hostTools). */
|
|
36
|
+
hostTools = [];
|
|
30
37
|
constructor(adapter, catalog) {
|
|
31
38
|
this.adapter = adapter;
|
|
32
39
|
this.catalog = catalog;
|
|
33
40
|
this.app = acpAgent({ name: "bma-dsh-acp" })
|
|
34
41
|
.onConnect((conn) => { this.client = conn.client; })
|
|
35
|
-
.onRequest("initialize", () => this.initialize())
|
|
42
|
+
.onRequest("initialize", ({ params }) => this.initialize(params))
|
|
36
43
|
.onRequest("session/new", ({ params }) => this.open(params, false))
|
|
37
44
|
.onRequest("session/load", ({ params }) => this.open(params, true))
|
|
38
45
|
.onRequest("session/set_mode", ({ params }) => this.setMode(params))
|
|
@@ -52,9 +59,13 @@ export class DshAcpShell {
|
|
|
52
59
|
return { baseUrl: s.model.baseUrl, token: s.model.token, model: s.model.model };
|
|
53
60
|
},
|
|
54
61
|
firstChunk: () => { },
|
|
62
|
+
idleRetry: (sid, result) => { process.stderr.write(`bma-dsh-acp: model stream idle before any content (session ${sid}): ${result}\n`); },
|
|
55
63
|
usage: () => { }, // metered by the client's model proxy
|
|
56
64
|
});
|
|
57
|
-
|
|
65
|
+
shell.registerTools();
|
|
66
|
+
shell.imageInput = await adapter.probeImageInput();
|
|
67
|
+
if (!shell.imageInput)
|
|
68
|
+
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
69
|
return shell;
|
|
59
70
|
}
|
|
60
71
|
async dispose() {
|
|
@@ -62,13 +73,23 @@ export class DshAcpShell {
|
|
|
62
73
|
s.running?.abort();
|
|
63
74
|
await this.adapter.dispose();
|
|
64
75
|
}
|
|
65
|
-
|
|
76
|
+
registerTools() {
|
|
77
|
+
this.adapter.registerTools([...TOOL_SPECS, ...this.hostTools].map((spec) => this.tool(spec)));
|
|
78
|
+
}
|
|
79
|
+
toolNames() {
|
|
80
|
+
return [...TOOL_NAMES, ...this.hostTools.map((t) => t.name)];
|
|
81
|
+
}
|
|
82
|
+
initialize(params) {
|
|
83
|
+
if (this.sessions.size)
|
|
84
|
+
throw RequestError.invalidRequest(undefined, "initialize after a session was opened");
|
|
85
|
+
this.hostTools = hostToolsMeta(params._meta);
|
|
86
|
+
this.registerTools();
|
|
66
87
|
return {
|
|
67
88
|
protocolVersion: PROTOCOL_VERSION,
|
|
68
|
-
agentCapabilities: { loadSession: true, promptCapabilities: { image:
|
|
89
|
+
agentCapabilities: { loadSession: true, promptCapabilities: { image: this.imageInput, audio: false, embeddedContext: false }, mcpCapabilities: { http: false, sse: false } },
|
|
69
90
|
agentInfo: { name: "bma-dsh-acp", title: "BMA DSH", version: VERSION },
|
|
70
91
|
authMethods: [],
|
|
71
|
-
_meta: { [META]: { dsh: DSH_VERSION, tools: TOOL_NAMES, streaming: "token", permissionRelay: "full", modes: Object.keys(MODES) } },
|
|
92
|
+
_meta: { [META]: { dsh: DSH_VERSION, tools: TOOL_NAMES, hostTools: this.hostTools.map((t) => t.name), streaming: "token", permissionRelay: "full", modes: Object.keys(MODES) } },
|
|
72
93
|
};
|
|
73
94
|
}
|
|
74
95
|
async open(params, load) {
|
|
@@ -86,7 +107,7 @@ export class DshAcpShell {
|
|
|
86
107
|
open.model = meta.model; // a reload in the same process: new route, same conversation
|
|
87
108
|
}
|
|
88
109
|
else {
|
|
89
|
-
const s = { id, model: meta.model, mode: "default", policy: modePolicy("default"), ids: freshIds(), chain: Promise.resolve() };
|
|
110
|
+
const s = { id, model: meta.model, mode: "default", policy: modePolicy("default", this.hostTools), ids: freshIds(), chain: Promise.resolve() };
|
|
90
111
|
this.sessions.set(id, s); // the route must exist before the first model request
|
|
91
112
|
const init = { sessionId: id, model: meta.model.model, systemPrompt: meta.systemPrompt ?? "", cwd: params.cwd };
|
|
92
113
|
try {
|
|
@@ -103,7 +124,7 @@ export class DshAcpShell {
|
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
const catalog = [...this.adapter.toolCatalog(id)];
|
|
106
|
-
const violation = catalogViolation("dsh",
|
|
127
|
+
const violation = catalogViolation("dsh", this.toolNames(), catalog);
|
|
107
128
|
if (violation) {
|
|
108
129
|
await this.close(id);
|
|
109
130
|
throw RequestError.internalError(undefined, violation);
|
|
@@ -131,16 +152,18 @@ export class DshAcpShell {
|
|
|
131
152
|
throw RequestError.invalidParams(undefined, `unknown mode ${params.modeId}`);
|
|
132
153
|
s.mode = params.modeId;
|
|
133
154
|
if (!s.running)
|
|
134
|
-
s.policy = modePolicy(s.mode);
|
|
155
|
+
s.policy = modePolicy(s.mode, this.hostTools);
|
|
135
156
|
return {};
|
|
136
157
|
}
|
|
137
158
|
async prompt(params) {
|
|
138
159
|
const s = this.get(params.sessionId);
|
|
139
160
|
if (s.running)
|
|
140
161
|
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 };
|
|
162
|
+
const meta = promptMeta(params._meta, this.toolNames());
|
|
163
|
+
s.policy = { ...modePolicy(s.mode, this.hostTools), ...meta.toolPolicy };
|
|
143
164
|
const input = { id: meta.inputId ?? randomUUID(), ...promptInput(params.prompt) };
|
|
165
|
+
if (input.images && !this.imageInput)
|
|
166
|
+
throw RequestError.invalidParams(undefined, "image input is unavailable in this build of bma-dsh-acp");
|
|
144
167
|
const ctl = new AbortController();
|
|
145
168
|
s.running = ctl;
|
|
146
169
|
try {
|
|
@@ -166,7 +189,7 @@ export class DshAcpShell {
|
|
|
166
189
|
}
|
|
167
190
|
finally {
|
|
168
191
|
s.running = undefined;
|
|
169
|
-
s.policy = modePolicy(s.mode);
|
|
192
|
+
s.policy = modePolicy(s.mode, this.hostTools);
|
|
170
193
|
s.ids = freshIds();
|
|
171
194
|
}
|
|
172
195
|
}
|
|
@@ -211,7 +234,7 @@ export class DshAcpShell {
|
|
|
211
234
|
async invoke(spec, call) {
|
|
212
235
|
const s = this.get(call.sessionId);
|
|
213
236
|
const client = this.client;
|
|
214
|
-
const permission = s.policy[spec.name];
|
|
237
|
+
const permission = s.policy[spec.name] ?? "ask";
|
|
215
238
|
// Let the turn's queued message events go out first (they were produced before this call).
|
|
216
239
|
await new Promise((resolve) => setImmediate(resolve));
|
|
217
240
|
s.ids = freshIds();
|
|
@@ -247,7 +270,9 @@ export class DshAcpShell {
|
|
|
247
270
|
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
271
|
if (!result)
|
|
249
272
|
return finish(aborted());
|
|
250
|
-
|
|
273
|
+
const images = (result.images ?? []).filter((i) => MEDIA.has(i.mimeType) && typeof i.data === "string" && i.data)
|
|
274
|
+
.map((i) => ({ mediaType: i.mimeType, data: i.data }));
|
|
275
|
+
return finish({ text: String(result.text ?? ""), isError: Boolean(result.isError), ...(images.length ? { images } : {}) });
|
|
251
276
|
}
|
|
252
277
|
catch (err) {
|
|
253
278
|
if (call.signal.aborted)
|
|
@@ -317,7 +342,35 @@ function sessionMeta(raw) {
|
|
|
317
342
|
model: { baseUrl: String(model.baseUrl), token: model.token, model: model.model, ...(contextWindow ? { contextWindow } : {}), ...(maxOutputTokens ? { maxOutputTokens } : {}) },
|
|
318
343
|
};
|
|
319
344
|
}
|
|
320
|
-
function
|
|
345
|
+
function hostToolsMeta(raw) {
|
|
346
|
+
const tools = bmaMeta(raw)?.hostTools;
|
|
347
|
+
if (tools === undefined)
|
|
348
|
+
return [];
|
|
349
|
+
if (!Array.isArray(tools))
|
|
350
|
+
throw RequestError.invalidParams(undefined, "_meta.bma.hostTools must be an array");
|
|
351
|
+
const seen = new Set(TOOL_NAMES);
|
|
352
|
+
return tools.map((t, i) => {
|
|
353
|
+
const at = `_meta.bma.hostTools[${i}]`;
|
|
354
|
+
if (typeof t !== "object" || t === null)
|
|
355
|
+
throw RequestError.invalidParams(undefined, `${at} must be an object`);
|
|
356
|
+
if (typeof t.name !== "string" || !HOST_TOOL_NAME.test(t.name) || seen.has(t.name))
|
|
357
|
+
throw RequestError.invalidParams(undefined, `${at}.name must be a new snake_case tool name`);
|
|
358
|
+
seen.add(t.name);
|
|
359
|
+
if (typeof t.description !== "string" || !t.description)
|
|
360
|
+
throw RequestError.invalidParams(undefined, `${at}.description is required`);
|
|
361
|
+
const p = t.parameters;
|
|
362
|
+
if (typeof p !== "object" || p === null || p.type !== "object")
|
|
363
|
+
throw RequestError.invalidParams(undefined, `${at}.parameters must be an object JSON Schema`);
|
|
364
|
+
if (typeof t.readOnly !== "boolean")
|
|
365
|
+
throw RequestError.invalidParams(undefined, `${at}.readOnly must be a boolean`);
|
|
366
|
+
if (typeof t.kind !== "string" || !KINDS.has(t.kind))
|
|
367
|
+
throw RequestError.invalidParams(undefined, `${at}.kind must be an ACP tool kind`);
|
|
368
|
+
if (t.permission !== undefined && t.permission !== "allow" && t.permission !== "ask")
|
|
369
|
+
throw RequestError.invalidParams(undefined, `${at}.permission must be allow or ask`);
|
|
370
|
+
return { name: t.name, description: t.description, parameters: p, readOnly: t.readOnly, kind: t.kind, ...(t.permission ? { permission: t.permission } : {}) };
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function promptMeta(raw, names) {
|
|
321
374
|
const meta = bmaMeta(raw) ?? {};
|
|
322
375
|
const out = {};
|
|
323
376
|
if (meta.inputId !== undefined) {
|
|
@@ -330,7 +383,7 @@ function promptMeta(raw) {
|
|
|
330
383
|
if (typeof policy !== "object" || policy === null)
|
|
331
384
|
throw RequestError.invalidParams(undefined, "_meta.bma.toolPolicy must be an object");
|
|
332
385
|
for (const [name, value] of Object.entries(policy)) {
|
|
333
|
-
if (!
|
|
386
|
+
if (!names.includes(name) || !["allow", "ask", "deny"].includes(value)) {
|
|
334
387
|
throw RequestError.invalidParams(undefined, `_meta.bma.toolPolicy.${name} must be allow, ask or deny for a known tool`);
|
|
335
388
|
}
|
|
336
389
|
}
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
30
|
+
"@beeos-ai/bma-dsh-adapter": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"esbuild": "0.28.2"
|