@beeos-ai/bma-executor-sdk 0.0.0-stage → 0.4.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 +170 -2
- package/dist/agent.d.ts +126 -0
- package/dist/agent.js +478 -0
- package/dist/agent.js.map +1 -0
- package/dist/client.d.ts +170 -0
- package/dist/client.js +598 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +85 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/executor.d.ts +1218 -0
- package/dist/generated/executor.js +455 -0
- package/dist/generated/executor.js.map +1 -0
- package/dist/harness.d.ts +261 -0
- package/dist/harness.js +809 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +49 -0
- package/dist/local.js +169 -0
- package/dist/local.js.map +1 -0
- package/dist/rpc.d.ts +38 -0
- package/dist/rpc.js +123 -0
- package/dist/rpc.js.map +1 -0
- package/dist/transport.d.ts +53 -0
- package/dist/transport.js +177 -0
- package/dist/transport.js.map +1 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -1,3 +1,171 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @beeos-ai/bma-executor-sdk (TypeScript)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Executor (`ExecutorClient`), harness (`Harness`) and agent ends of the BMA
|
|
4
|
+
executor protocol `bma.executor.v1.1`, with protocol types generated from the
|
|
5
|
+
BMA contract (`contracts/bma/executor`). Transports: WebSocket, WebSocket over
|
|
6
|
+
a Unix domain socket or Windows named pipe (`unix://`, `npipe://`), in-process.
|
|
7
|
+
Node >= 20.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @beeos-ai/bma-executor-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The REST/façade client is the separate
|
|
14
|
+
`@beeos-ai/bma-sdk` package.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
The executor protocol (`bma.executor.v1.1` and `v1`) for Node.js 20+, with
|
|
19
|
+
the same three parts and behavior as [Go `executor`](..). Protocol
|
|
20
|
+
types are generated from the contract
|
|
21
|
+
([`src/generated/executor.ts`](src/generated/executor.ts), snapshot and lock in
|
|
22
|
+
[`contracts/bma/executor`](../contracts/bma/executor)); the runtime is
|
|
23
|
+
handwritten.
|
|
24
|
+
|
|
25
|
+
### Executor quick start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { ExecutorClient } from "@beeos-ai/bma-executor-sdk";
|
|
29
|
+
|
|
30
|
+
const client = new ExecutorClient({
|
|
31
|
+
environmentKey: process.env.BMA_ENVIRONMENT_KEY,
|
|
32
|
+
environmentId: process.env.BMA_ENVIRONMENT_ID!,
|
|
33
|
+
executor: { name: "my-executor", os: process.platform, arch: process.arch, version: "0.1.0" },
|
|
34
|
+
capabilities: { shell: { enabled: true } },
|
|
35
|
+
});
|
|
36
|
+
client.handleTool("bash", async (call, { signal, progress }) => {
|
|
37
|
+
progress("running");
|
|
38
|
+
return { outcome: "succeeded", is_error: false, content: [{ type: "text", text: "ok" }] };
|
|
39
|
+
});
|
|
40
|
+
await client.start();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Executor lifecycle, cancellation, and results
|
|
44
|
+
|
|
45
|
+
- `tool.cancel` aborts the handler's `signal` (so do `deadline_ms` and
|
|
46
|
+
`stop()`); the client answers `{"canceled":true}` and reports
|
|
47
|
+
`outcome: "canceled"`. A repeated `tool.call` id is acknowledged, not run
|
|
48
|
+
twice. `handleTool(name, …)` routes one tool, `onToolCall` the rest.
|
|
49
|
+
- Call `sessionReady` after workspace/MCP setup and `sessionEnded` after
|
|
50
|
+
cleanup. `tool.result` is retried on `-32009`, `-32004` or
|
|
51
|
+
`data.retryable: true`, idempotent by `tool_call_id`; when every attempt
|
|
52
|
+
fails `onToolResultLost` receives the result to persist and replay.
|
|
53
|
+
- Reconnect: exponential backoff (500 ms base, ×2, 30 s cap, jitter) with a
|
|
54
|
+
fresh token per dial and `resume` (`executor_id`, `last_connection_id`,
|
|
55
|
+
`active_sessions`). Close codes 1008, 4002, 4003, 4004 and 4005 are terminal;
|
|
56
|
+
4001 (token expired) reconnects with a new token.
|
|
57
|
+
- `openMcpChannel(sessionId, serverKey)` opens the raw MCP channel
|
|
58
|
+
(`?channel=mcp&session_id=&server_key=`); `read()`/`write()` carry MCP
|
|
59
|
+
JSON-RPC frames unchanged.
|
|
60
|
+
- A handler that throws `RpcError` keeps its code and data on the wire.
|
|
61
|
+
`onRequest(method, …)` serves `process.*`, `terminal.*` and `skill.install`.
|
|
62
|
+
|
|
63
|
+
### Harness quick start
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { Harness } from "@beeos-ai/bma-executor-sdk";
|
|
67
|
+
|
|
68
|
+
const harness = new Harness({ authenticate: (request) => verify(request) }); // omit only on loopback
|
|
69
|
+
const { url } = await harness.listen(); // or harness.attach(httpServer)
|
|
70
|
+
|
|
71
|
+
const executor = await harness.waitExecutor();
|
|
72
|
+
const session = await executor.assignSession({ session_id: "sess_1" });
|
|
73
|
+
await session.ready();
|
|
74
|
+
const result = await session.callTool({ tool: "bash", input: { command: "ls" } });
|
|
75
|
+
const mcp = await session.mcp("echo"); // raw MCP JSON-RPC
|
|
76
|
+
await session.stop("done");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Semantics match the Go `Harness`: `callTool` is idempotent by `tool_call_id`;
|
|
80
|
+
an unknown call's `tool.result` is `-32002`, a foreign session `-32007`;
|
|
81
|
+
sessions and pending calls survive a reconnect within `leaseTtlMs`; a newer
|
|
82
|
+
connection replaces the old one with 4004; lease expiry or `executor.close`
|
|
83
|
+
fails pending calls with `ToolOutcomeUnknownError`.
|
|
84
|
+
|
|
85
|
+
### Transports
|
|
86
|
+
|
|
87
|
+
Messages are identical on every transport; who dials is independent of the
|
|
88
|
+
protocol role (the executor always sends `executor.register` first).
|
|
89
|
+
|
|
90
|
+
| Transport | Executor end | Harness end |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| WebSocket (cloud, executor dials) | default, or `webSocketDialer(...)` | `listen()`, `attach(server)`, `handleUpgrade(...)` |
|
|
93
|
+
| Local socket, executor dials | `webSocketDialer({ ..., tokenSource: staticToken('unix:///path.sock', token) })` | `new Harness({ authenticate: bearerAuthenticator(token) }).listenLocal(path)` |
|
|
94
|
+
| Local socket, harness dials (bmad listens) | `listenExecutor({ path, token })` → `new ExecutorClient({ dialer: listener.dialer })` | `harness.dialExecutor(listener.url, { token })` |
|
|
95
|
+
| In-process | `dialer: harness.inProcessDialer()` | — |
|
|
96
|
+
|
|
97
|
+
Local sockets carry the same WebSocket protocol as the cloud (subprotocol
|
|
98
|
+
negotiation, bearer token, close codes) over a Unix domain socket
|
|
99
|
+
(`unix:///path.sock`, 0700 directory, 0600 socket) or a Windows named pipe
|
|
100
|
+
(`npipe:////./pipe/<name>`), as the Go and Python SDKs do. Each start writes a
|
|
101
|
+
fresh local token (`createLocalToken`, 0600); environment keys are never used
|
|
102
|
+
locally (see [`src/executor/local.ts`](src/executor/local.ts)). When the harness
|
|
103
|
+
dials the executor it also dials the session MCP channels (`session.mcp`).
|
|
104
|
+
|
|
105
|
+
### Agent executors
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
client.serveAgent(
|
|
109
|
+
{
|
|
110
|
+
openTurn: async (p) => ({ accepted: true, turn_id: p.turn_id, executor_session_ref: "conv_1", resumed: false, effective: {} }),
|
|
111
|
+
runTurn: async (turn, signal) => {
|
|
112
|
+
const answer = await turn.requestInteraction({ kind: "permission", tool_use_id: "tu_1", tool_name: "bash" }, signal);
|
|
113
|
+
turn.emit({ type: "agent.message", content: [{ type: "text", text: answer.outcome }] });
|
|
114
|
+
},
|
|
115
|
+
closeSession: async () => {},
|
|
116
|
+
},
|
|
117
|
+
{ models: [{ id: "my-model" }] },
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The SDK assigns event ids and sequences, delivers events in order with
|
|
122
|
+
ACKs, resends after a reconnect, and frames each turn with
|
|
123
|
+
`session.status_running` and `session.status_idle` (`completed`, `canceled`,
|
|
124
|
+
`failed`). The harness side is `HarnessOptions.agent` (`AgentPlatform`) plus
|
|
125
|
+
`executor.startTurn / sendTurnInput / cancelTurn / closeAgentSession /
|
|
126
|
+
respondInteraction`; a sequence gap answers `-32013` with
|
|
127
|
+
`data.expected_sequence`.
|
|
128
|
+
|
|
129
|
+
### Examples
|
|
130
|
+
|
|
131
|
+
[`examples`](examples) (`npm install` there first):
|
|
132
|
+
`executor-minimal` (cloud executor with bash), `harness-loopback` (bmad-style
|
|
133
|
+
local socket: executor listens, harness dials), `agent-minimal` (agent
|
|
134
|
+
executor and in-memory platform in one process).
|
|
135
|
+
|
|
136
|
+
### Go ↔ TypeScript
|
|
137
|
+
|
|
138
|
+
| Go (`github.com/beeos-ai/bma-sdks/executor`) | TypeScript (`@beeos-ai/bma-executor-sdk`) |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `NewClient(Options)` / `Start` / `Stop` | `new ExecutorClient(options)` / `start()` / `stop()` |
|
|
141
|
+
| `Options.TokenSource`, `StaticToken` | `tokenSource`, `staticToken` (or `dialer`) |
|
|
142
|
+
| `OnToolCall(ctx, call)` | `onToolCall(call, { signal, progress })`, `handleTool(name, …)` |
|
|
143
|
+
| `OnSessionAssign` / `OnSessionStop` / `OnDrain` / `OnRequest` / `OnNotification` | `onSessionAssign` / `onSessionStop` / `onDrain` / `onRequest` / `onNotification` |
|
|
144
|
+
| `SessionReady` / `SessionEnded` / `ReportToolResult` | `sessionReady` / `sessionEnded` / `reportToolResult` |
|
|
145
|
+
| `SendSessionStatus` / `SendToolProgress` / `SendExecutorCapabilitiesUpdate` | `sessionStatus` / `toolProgress` / `updateCapabilities` |
|
|
146
|
+
| `OpenMCPChannel` → `MCPChannel` | `openMcpChannel` → `FrameChannel` |
|
|
147
|
+
| `ServeAgent(AgentHandler, AgentCapability)` | `serveAgent(handler, capability)` |
|
|
148
|
+
| `AgentTurn.Emit/EmitWithID/Delta/Flush/Inputs/RequestInteraction/CancelInteraction` | `emit/emitWithId/delta/flush/inputs/requestInteraction/cancelInteraction` |
|
|
149
|
+
| `NewHarness(HarnessOptions)` (`http.Handler`) | `new Harness(options)` + `listen/listenLocal/attach/handleUpgrade/accept/dialExecutor/inProcessDialer` |
|
|
150
|
+
| `Harness.WaitExecutor` / `Executors` | `waitExecutor` / `executors` |
|
|
151
|
+
| `Executor.AssignSession/Drain/StartTurn/SendTurnInput/CancelTurn/CloseAgentSession/RespondInteraction` | `HarnessExecutor.assignSession/drain/startTurn/sendTurnInput/cancelTurn/closeAgentSession/respondInteraction` |
|
|
152
|
+
| `Session.Ready/CallTool/CancelTool/Stop/MCP/Done/EndReason` | `HarnessSession.ready/callTool/cancelTool/stop/mcp/done/endReason` |
|
|
153
|
+
| `AgentPlatform` | `AgentPlatform` |
|
|
154
|
+
| `*RPCError`, `ErrNotConnected`, `ErrClientStopped`, `ErrSessionEnded`, `ErrToolOutcomeUnknown`, `ErrAssignRejected` | `RpcError`, `NotConnectedError`, `ClientStoppedError`, `SessionEndedError`, `ToolOutcomeUnknownError`, `AssignRejectedError` |
|
|
155
|
+
| `Core` (bash/files/stdio MCP) | not provided |
|
|
156
|
+
| — (TCP WebSocket only) | `Harness.listenLocal` / `listenExecutor` + `Harness.dialExecutor` / `inProcessPair` |
|
|
157
|
+
|
|
158
|
+
### Tests
|
|
159
|
+
|
|
160
|
+
`npm test` runs, besides the REST tests: the conformance examples against
|
|
161
|
+
both TS ends ([`test/conformance.test.ts`](test/conformance.test.ts)), executor ↔
|
|
162
|
+
harness over every transport with reconnect and lease
|
|
163
|
+
([`test/executor.test.ts`](test/executor.test.ts)), the agent capability
|
|
164
|
+
([`test/agent.test.ts`](test/agent.test.ts)), and interop with the Go SDK
|
|
165
|
+
([`test/interop.test.ts`](test/interop.test.ts): TS executor ↔ Go harness, TS
|
|
166
|
+
agent ↔ Go platform, TS harness ↔ Go executor; needs Go), and interop with the
|
|
167
|
+
Python SDK over WebSocket on a Unix socket
|
|
168
|
+
([`test/interop-python.test.ts`](test/interop-python.test.ts): TS executor ↔
|
|
169
|
+
Python harness, Python executor ↔ TS harness; `BMA_PYTHON_SDK=<python dir with
|
|
170
|
+
.venv>`, skipped without it). Go has no Unix-socket dialer yet, so TS ↔ Go
|
|
171
|
+
local-socket interop is pending.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type Ack, type AgentCapability, type AgentEvent, type ConnectionState, type EventDeltaType, type InteractionCancelReason, type InteractionRequestParams, type InteractionResponseParams, type SessionEndedParams, type TurnInputParams, type TurnStartParams, type TurnStartResult } from './generated/executor.js';
|
|
2
|
+
import { type Deferred } from './errors.js';
|
|
3
|
+
import type { LogLevel } from './rpc.js';
|
|
4
|
+
export type { ConnectionState };
|
|
5
|
+
/** A complete local agent (model loop and tools). */
|
|
6
|
+
export interface AgentHandler {
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether to accept a turn.start and reports the configuration the
|
|
9
|
+
* agent will apply. It runs before turn.start is answered, so it must not
|
|
10
|
+
* do the turn's work. A thrown RpcError keeps its code.
|
|
11
|
+
*/
|
|
12
|
+
openTurn(params: TurnStartParams): Promise<TurnStartResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Does the turn's work. signal is aborted by turn.cancel, session.close and
|
|
15
|
+
* deadline_ms. Resolving ends the turn as completed, an aborted signal as
|
|
16
|
+
* canceled (failed for deadline_ms), a rejection as failed (the SDK emits
|
|
17
|
+
* session.error first).
|
|
18
|
+
*/
|
|
19
|
+
runTurn(turn: AgentTurn, signal: AbortSignal): Promise<void>;
|
|
20
|
+
/** Releases the local conversation after session.close. */
|
|
21
|
+
closeSession(sessionId: string, reason: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** What the agent runtime needs from the executor client. */
|
|
24
|
+
export interface AgentHost {
|
|
25
|
+
call<T>(method: string, params: unknown): Promise<T>;
|
|
26
|
+
notify(method: string, params: unknown): void;
|
|
27
|
+
readonly stopSignal: AbortSignal;
|
|
28
|
+
connected(): boolean;
|
|
29
|
+
/** Resolves after the next successful executor.register. */
|
|
30
|
+
nextRegistration(): Promise<void>;
|
|
31
|
+
addSession(id: string): void;
|
|
32
|
+
removeSession(id: string): void;
|
|
33
|
+
sessionEnded(params: SessionEndedParams): Promise<Ack>;
|
|
34
|
+
log(level: LogLevel, message: string, error?: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
/** The handle runTurn uses to talk to the platform. */
|
|
37
|
+
export declare class AgentTurn {
|
|
38
|
+
private readonly session;
|
|
39
|
+
readonly params: TurnStartParams;
|
|
40
|
+
ended: boolean;
|
|
41
|
+
readonly abort: AbortController;
|
|
42
|
+
readonly done: Deferred<void>;
|
|
43
|
+
readonly seen: Set<string>;
|
|
44
|
+
private readonly queue;
|
|
45
|
+
constructor(session: AgentSession, params: TurnStartParams);
|
|
46
|
+
get sessionId(): string;
|
|
47
|
+
get turnId(): string;
|
|
48
|
+
/** turn.input messages in arrival order; the iteration ends with the turn. */
|
|
49
|
+
get inputs(): AsyncIterable<TurnInputParams> & {
|
|
50
|
+
next(): Promise<IteratorResult<TurnInputParams>>;
|
|
51
|
+
};
|
|
52
|
+
/** @internal */
|
|
53
|
+
pushInput(input: TurnInputParams): void;
|
|
54
|
+
/** @internal */
|
|
55
|
+
finish(): void;
|
|
56
|
+
/** A fresh event id, for a message streamed with delta before emitWithId. */
|
|
57
|
+
newEventId(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Queues one durable event and returns its id. Delivery is asynchronous,
|
|
60
|
+
* ordered and at-least-once; the platform deduplicates by event id.
|
|
61
|
+
*/
|
|
62
|
+
emit(event: AgentEvent): string;
|
|
63
|
+
emitWithId(eventId: string, event: AgentEvent): string;
|
|
64
|
+
/** Streams a transient preview of the durable event eventId; dropped while disconnected, never resent. */
|
|
65
|
+
delta(eventId: string, eventType: EventDeltaType, index: number, text: string): void;
|
|
66
|
+
/** Resolves once every event emitted so far is acknowledged. */
|
|
67
|
+
flush(signal?: AbortSignal): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Asks the platform for an approval or an answer and waits for
|
|
70
|
+
* interaction.response. It flushes first so the platform already has the
|
|
71
|
+
* agent.tool_use the request references. When signal aborts or expires_at
|
|
72
|
+
* passes, it withdraws the interaction with interaction.cancel; an expiry
|
|
73
|
+
* resolves with outcome expired, which the agent must treat as a deny.
|
|
74
|
+
*/
|
|
75
|
+
requestInteraction(request: Omit<InteractionRequestParams, 'session_id' | 'turn_id' | 'interaction_id'> & {
|
|
76
|
+
interaction_id?: string;
|
|
77
|
+
}, signal?: AbortSignal): Promise<InteractionResponseParams>;
|
|
78
|
+
/** Withdraws an open interaction, for example one the user answered locally (resolved_locally). */
|
|
79
|
+
cancelInteraction(interactionId: string, reason: InteractionCancelReason): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/** One platform session on this executor: its outbox of durable events and at most one running turn. */
|
|
82
|
+
declare class AgentSession {
|
|
83
|
+
readonly host: AgentHost;
|
|
84
|
+
readonly id: string;
|
|
85
|
+
private nextSequence;
|
|
86
|
+
private nextId;
|
|
87
|
+
private readonly outbox;
|
|
88
|
+
private wake;
|
|
89
|
+
private drained;
|
|
90
|
+
closed: boolean;
|
|
91
|
+
turn?: AgentTurn;
|
|
92
|
+
readonly results: Map<string, TurnStartResult>;
|
|
93
|
+
readonly pending: Map<string, Deferred<InteractionResponseParams>>;
|
|
94
|
+
readonly answered: Set<string>;
|
|
95
|
+
constructor(host: AgentHost, id: string);
|
|
96
|
+
newEventId(): string;
|
|
97
|
+
enqueue(turnId: string, eventId: string, event: AgentEvent): void;
|
|
98
|
+
signal(): void;
|
|
99
|
+
flush(signal?: AbortSignal): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Delivers the outbox head first, one event at a time, so the platform sees
|
|
102
|
+
* every session in sequence order. A transport failure keeps the event and
|
|
103
|
+
* waits for the next registration: the resend after reconnect.
|
|
104
|
+
*/
|
|
105
|
+
private sendLoop;
|
|
106
|
+
/** Retries call across disconnects; only transport failures, executor_busy and retryable errors are retried. */
|
|
107
|
+
callUntilConnected<T>(call: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
108
|
+
withdraw(interactionId: string, reason: InteractionCancelReason): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
/** @internal Routes agent methods for one ExecutorClient. */
|
|
111
|
+
export declare class AgentRuntime {
|
|
112
|
+
private readonly host;
|
|
113
|
+
private readonly handler;
|
|
114
|
+
readonly capability: AgentCapability;
|
|
115
|
+
private readonly sessions;
|
|
116
|
+
constructor(host: AgentHost, handler: AgentHandler, capability: AgentCapability);
|
|
117
|
+
/** Handles an agent method; undefined for every other method. */
|
|
118
|
+
dispatch(method: string, params: unknown): Promise<unknown> | undefined;
|
|
119
|
+
private session;
|
|
120
|
+
private turnStart;
|
|
121
|
+
private runTurn;
|
|
122
|
+
private turnInput;
|
|
123
|
+
private turnCancel;
|
|
124
|
+
private sessionClose;
|
|
125
|
+
private interactionResponse;
|
|
126
|
+
}
|