@beeos-ai/device-agent 0.7.2 → 0.9.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/dist/app/fixed-session-id.d.ts +19 -0
- package/dist/app/fixed-session-id.js +26 -0
- package/dist/app/fixed-session-id.js.map +1 -0
- package/dist/app/jsonl-session-sweeper.d.ts +90 -0
- package/dist/app/jsonl-session-sweeper.js +162 -0
- package/dist/app/jsonl-session-sweeper.js.map +1 -0
- package/dist/app/jsonl-store.d.ts +10 -0
- package/dist/app/jsonl-store.js +10 -4
- package/dist/app/jsonl-store.js.map +1 -1
- package/dist/app/session-manager.d.ts +135 -1
- package/dist/app/session-manager.js +195 -12
- package/dist/app/session-manager.js.map +1 -1
- package/dist/bootstrap.js +157 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.js +20 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +82 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet/domain/device.d.ts +17 -0
- package/dist/fleet/domain/device.js +1 -0
- package/dist/fleet/domain/device.js.map +1 -1
- package/dist/fleet/domain/fsm.d.ts +30 -1
- package/dist/fleet/domain/fsm.js +100 -4
- package/dist/fleet/domain/fsm.js.map +1 -1
- package/dist/fleet/supervisor.js +38 -18
- package/dist/fleet/supervisor.js.map +1 -1
- package/dist/inbound/cli/main.js +12 -0
- package/dist/inbound/cli/main.js.map +1 -1
- package/dist/messaging/token-provider.d.ts +43 -0
- package/dist/messaging/token-provider.js +65 -0
- package/dist/messaging/token-provider.js.map +1 -0
- package/dist/plugins/acp/index.d.ts +27 -30
- package/dist/plugins/acp/index.js +44 -137
- package/dist/plugins/acp/index.js.map +1 -1
- package/dist/plugins/message-bus/busy-reply.d.ts +39 -0
- package/dist/plugins/message-bus/busy-reply.js +30 -0
- package/dist/plugins/message-bus/busy-reply.js.map +1 -0
- package/dist/plugins/message-bus/cancel.d.ts +48 -0
- package/dist/plugins/message-bus/cancel.js +66 -0
- package/dist/plugins/message-bus/cancel.js.map +1 -0
- package/dist/plugins/message-bus/inbound.d.ts +71 -0
- package/dist/plugins/message-bus/inbound.js +180 -0
- package/dist/plugins/message-bus/inbound.js.map +1 -0
- package/dist/plugins/message-bus/index.d.ts +146 -0
- package/dist/plugins/message-bus/index.js +382 -0
- package/dist/plugins/message-bus/index.js.map +1 -0
- package/dist/plugins/message-bus/ms-channel-notifier.d.ts +101 -0
- package/dist/plugins/message-bus/ms-channel-notifier.js +181 -0
- package/dist/plugins/message-bus/ms-channel-notifier.js.map +1 -0
- package/dist/plugins/message-bus/ms-inflight-tracker.d.ts +36 -0
- package/dist/plugins/message-bus/ms-inflight-tracker.js +42 -0
- package/dist/plugins/message-bus/ms-inflight-tracker.js.map +1 -0
- package/dist/ports/plugin.d.ts +12 -0
- package/package.json +2 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound — `chat_message` / `agent_request` envelope handler.
|
|
3
|
+
*
|
|
4
|
+
* v0.9.0 — wires `MessageSDK.onMessage(...)` directly to
|
|
5
|
+
* `SessionManager.promptForSession(sid, prompt)`. Per-channel session
|
|
6
|
+
* isolation:
|
|
7
|
+
*
|
|
8
|
+
* 1. Extract `{ message, channelId, messageId, contextId, sessionKey,
|
|
9
|
+
* files }` from the payload via `extractChatPrompt` (returns null
|
|
10
|
+
* on malformed envelopes — silently dropped).
|
|
11
|
+
* 2. Reject if no `channelId` or no `messageId` — replies must round
|
|
12
|
+
* trip through `ch:{channelId}` with `in_reply_to=messageId`, so
|
|
13
|
+
* neither is optional.
|
|
14
|
+
* 3. Layer 3 in-flight gate: if `sessionManager.isTurnInFlight()`
|
|
15
|
+
* returns true, send a busy-reply on the inbound channel and
|
|
16
|
+
* return. The agent serialises every prompt through a single
|
|
17
|
+
* physical device, so concurrent requests are rejected (not
|
|
18
|
+
* queued) — matches v0.8.0 contract.
|
|
19
|
+
* 4. Resolve the per-channel sid from `sessionKey` (the SDK already
|
|
20
|
+
* computed `agent:{id}:invoke:ctx-{contextId}` /
|
|
21
|
+
* `:ch-{channelId}`). Bind a fresh `MsChannelNotifier` to that
|
|
22
|
+
* sid for the duration of the turn.
|
|
23
|
+
* 5. Build the prompt (text + a minimal `[attachments]` block when
|
|
24
|
+
* `files.length > 0`) and call
|
|
25
|
+
* `sessionManager.promptForSession(sid, promptText)`.
|
|
26
|
+
* 6. The notifier translates streaming session/update envelopes into
|
|
27
|
+
* MS wire envelopes (`agent_reply_delta`, terminal `agent_reply`)
|
|
28
|
+
* automatically — no separate outbound listener needed.
|
|
29
|
+
* 7. ALWAYS unbind the notifier on completion (success / error /
|
|
30
|
+
* cancellation) so the next turn on the same channel gets a fresh
|
|
31
|
+
* notifier with `accumulatedText = ""`.
|
|
32
|
+
*
|
|
33
|
+
* Defensive paths:
|
|
34
|
+
* - If `promptForSession` rejects with `"agent_busy"` (extremely
|
|
35
|
+
* rare race after the explicit `isTurnInFlight` check), fall
|
|
36
|
+
* through to the busy-reply path so the caller's `/wait` doesn't
|
|
37
|
+
* hang.
|
|
38
|
+
* - For any other thrown error, publish a terminal `agent_reply`
|
|
39
|
+
* with `error` set so callers see a definite outcome.
|
|
40
|
+
*/
|
|
41
|
+
import { extractChatPrompt, } from "@beeos-ai/message-sdk";
|
|
42
|
+
import { MsChannelNotifier } from "./ms-channel-notifier.js";
|
|
43
|
+
/**
|
|
44
|
+
* Build the per-envelope handler. The returned function is what gets
|
|
45
|
+
* registered with `sdk.onMessage(type, handler)` — keeping the factory
|
|
46
|
+
* shape lets unit tests instantiate the closure with mock collaborators
|
|
47
|
+
* and feed envelopes synchronously.
|
|
48
|
+
*/
|
|
49
|
+
export function buildMessageBusInboundHandler(opts) {
|
|
50
|
+
return async (envelope) => {
|
|
51
|
+
const prompt = extractChatPrompt(envelope, {
|
|
52
|
+
source: opts.source,
|
|
53
|
+
localAgentId: opts.localAgentId ?? "",
|
|
54
|
+
});
|
|
55
|
+
if (!prompt) {
|
|
56
|
+
opts.logger.warn({ type: envelope.type, message_id: envelope.message_id }, "message-bus: dropped envelope (no prompt could be extracted)");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!prompt.channelId || !prompt.messageId) {
|
|
60
|
+
opts.logger.warn({
|
|
61
|
+
type: envelope.type,
|
|
62
|
+
hasChannelId: !!prompt.channelId,
|
|
63
|
+
hasMessageId: !!prompt.messageId,
|
|
64
|
+
}, "message-bus: dropped envelope (missing channelId or messageId — cannot correlate reply)");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Layer 3 — physical-device-serial gate. The agent has ONE device
|
|
68
|
+
// and ONE in-flight task slot. Concurrent invocations get an
|
|
69
|
+
// `agent_busy` reply so callers can retry instead of hanging on
|
|
70
|
+
// /wait.
|
|
71
|
+
if (opts.sessionManager.isTurnInFlight()) {
|
|
72
|
+
opts.logger.warn({
|
|
73
|
+
incomingMessageId: prompt.messageId,
|
|
74
|
+
incomingChannelId: prompt.channelId,
|
|
75
|
+
}, "message-bus: agent busy — sending agent_busy reply");
|
|
76
|
+
await opts.busyReply.replyBusy({
|
|
77
|
+
channelId: prompt.channelId,
|
|
78
|
+
inboundMessageId: prompt.messageId,
|
|
79
|
+
sessionKey: prompt.sessionKey,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Per-channel session anchor. `sessionKey` is the canonical id —
|
|
84
|
+
// multiple chat_message envelopes carrying the same `context_id`
|
|
85
|
+
// (or `channel_id` fallback) flow into the same sid, so the
|
|
86
|
+
// history-injection preamble in `_executePrompt` can stitch them
|
|
87
|
+
// into a continuous conversation.
|
|
88
|
+
const sid = prompt.sessionKey;
|
|
89
|
+
const notifier = new MsChannelNotifier({
|
|
90
|
+
sdk: opts.sdk,
|
|
91
|
+
channelId: prompt.channelId,
|
|
92
|
+
inboundMessageId: prompt.messageId,
|
|
93
|
+
sessionKey: prompt.sessionKey,
|
|
94
|
+
logger: opts.logger,
|
|
95
|
+
});
|
|
96
|
+
opts.sessionManager.bindNotifier(sid, notifier);
|
|
97
|
+
opts.tracker.set({
|
|
98
|
+
messageId: prompt.messageId,
|
|
99
|
+
channelId: prompt.channelId,
|
|
100
|
+
sid,
|
|
101
|
+
});
|
|
102
|
+
const promptText = appendAttachments(prompt.message, prompt.files);
|
|
103
|
+
opts.logger.info({
|
|
104
|
+
sid,
|
|
105
|
+
channelId: prompt.channelId,
|
|
106
|
+
inboundMessageId: prompt.messageId,
|
|
107
|
+
contextId: prompt.contextId,
|
|
108
|
+
sessionKey: prompt.sessionKey,
|
|
109
|
+
files: prompt.files.length,
|
|
110
|
+
}, "message-bus: turn started");
|
|
111
|
+
try {
|
|
112
|
+
const result = await opts.sessionManager.promptForSession(sid, promptText);
|
|
113
|
+
opts.logger.info({
|
|
114
|
+
sid,
|
|
115
|
+
channelId: prompt.channelId,
|
|
116
|
+
inboundMessageId: prompt.messageId,
|
|
117
|
+
stopReason: result.stopReason,
|
|
118
|
+
}, "message-bus: turn finished");
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
opts.logger.warn({
|
|
123
|
+
sid,
|
|
124
|
+
channelId: prompt.channelId,
|
|
125
|
+
inboundMessageId: prompt.messageId,
|
|
126
|
+
err: message,
|
|
127
|
+
}, "message-bus: promptForSession rejected — emitting fallback reply");
|
|
128
|
+
// If the notifier already published the terminal `agent_reply`
|
|
129
|
+
// (normal stream_complete path) the second publish is harmless
|
|
130
|
+
// because the notifier guards on `finalized`.
|
|
131
|
+
if (message === "agent_busy") {
|
|
132
|
+
await opts.busyReply.replyBusy({
|
|
133
|
+
channelId: prompt.channelId,
|
|
134
|
+
inboundMessageId: prompt.messageId,
|
|
135
|
+
sessionKey: prompt.sessionKey,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (!notifier.isFinalized) {
|
|
139
|
+
// Surface the error as a terminal agent_reply so callers see
|
|
140
|
+
// a definite outcome instead of a /wait timeout.
|
|
141
|
+
try {
|
|
142
|
+
await opts.sdk.sendToChannel(prompt.channelId, "agent_reply", {
|
|
143
|
+
session_key: prompt.sessionKey,
|
|
144
|
+
error: message,
|
|
145
|
+
text: notifier.currentText,
|
|
146
|
+
}, { inReplyTo: prompt.messageId });
|
|
147
|
+
}
|
|
148
|
+
catch (publishErr) {
|
|
149
|
+
opts.logger.error({
|
|
150
|
+
err: publishErr instanceof Error
|
|
151
|
+
? publishErr.message
|
|
152
|
+
: String(publishErr),
|
|
153
|
+
channelId: prompt.channelId,
|
|
154
|
+
}, "message-bus: failed to publish fallback agent_reply");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
opts.sessionManager.unbindNotifier(sid);
|
|
160
|
+
opts.tracker.clear(prompt.messageId);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Append a markdown-style `[attachments]` block listing file URLs to
|
|
166
|
+
* the prompt text. KISS: today the device-agent agent-core only takes
|
|
167
|
+
* a single string prompt, so the only way to surface attachments is
|
|
168
|
+
* inline. Empty `files` returns the message unchanged.
|
|
169
|
+
*
|
|
170
|
+
* Deeper integration (downloading via the resource-resolution layer,
|
|
171
|
+
* uploading to the device sandbox) is intentionally out of scope —
|
|
172
|
+
* see plan §E.
|
|
173
|
+
*/
|
|
174
|
+
function appendAttachments(message, files) {
|
|
175
|
+
if (!files || files.length === 0)
|
|
176
|
+
return message;
|
|
177
|
+
const list = files.map((u) => `- ${u}`).join("\n");
|
|
178
|
+
return `${message}\n\n[attachments]\n${list}`;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin — message-bus.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes the device-agent to the BeeOS Message Service so it can
|
|
5
|
+
* receive `chat_message` envelopes from L0 / A2A / MCP / OpenAPI
|
|
6
|
+
* invocations and reply over `ch:{channelId}` with `agent_reply` /
|
|
7
|
+
* `agent_reply_delta` (in_reply_to = original message_id).
|
|
8
|
+
*
|
|
9
|
+
* Disabled by default — opt in by setting `BEEOS_MESSAGE_BUS_ENABLED=true`
|
|
10
|
+
* (or `--message-bus`) AND providing both `--agent-gateway-url` and
|
|
11
|
+
* an Ed25519 identity. Without those the plugin logs a one-line warning
|
|
12
|
+
* during `start()` and stays disabled — the rest of the agent (Bridge
|
|
13
|
+
* ACP, HTTP debug) still runs.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle:
|
|
16
|
+
* - `init` — read config, decide enabled / disabled.
|
|
17
|
+
* - `start` — kick off the connect loop. Returns IMMEDIATELY (does
|
|
18
|
+
* not block bootstrap on the network). The first connect attempt
|
|
19
|
+
* and any subsequent retries run in the background.
|
|
20
|
+
* - `stop` — cancel any pending retry, disconnect SDK, unsubscribe
|
|
21
|
+
* outbound listener, drop in-flight register.
|
|
22
|
+
*
|
|
23
|
+
* Failure mode: if `tokenProvider()` rejects (e.g. the device instance
|
|
24
|
+
* has not been provisioned with `agentPublicKey` yet), the plugin
|
|
25
|
+
* schedules a retry with exponential backoff (30s → 60s → 120s → 240s,
|
|
26
|
+
* capped at 300s) and keeps trying forever until either `connect`
|
|
27
|
+
* succeeds or `stop()` cancels the loop. This mirrors beeos-claw's
|
|
28
|
+
* `localServer.onceAgentMappingReady(...)` deferred-connect pattern —
|
|
29
|
+
* see `[index.ts:484-498](agents/beeos-claw/src/index.ts)`.
|
|
30
|
+
*/
|
|
31
|
+
import { MessageSDK, type TokenProvider } from "@beeos-ai/message-sdk";
|
|
32
|
+
import type { Logger } from "../../logger/index.js";
|
|
33
|
+
import type { AgentPlugin, PluginContext } from "../../ports/plugin.js";
|
|
34
|
+
/**
|
|
35
|
+
* Backoff schedule for token / connect failures (milliseconds). Each
|
|
36
|
+
* entry is the delay BEFORE the next retry. The last entry caps the
|
|
37
|
+
* delay; once we hit the cap we keep retrying forever at that interval.
|
|
38
|
+
*
|
|
39
|
+
* Tunable via `MessageBusPluginOptions.retrySchedule` — tests pin a
|
|
40
|
+
* tighter schedule (e.g. `[5, 5, 5]`) to keep `vi.useFakeTimers()`
|
|
41
|
+
* runs cheap.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_RETRY_SCHEDULE_MS: readonly number[];
|
|
44
|
+
export interface MessageBusPluginOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Ed25519 keypair used to sign `POST /api/v1/messaging/token`. When
|
|
47
|
+
* absent the plugin self-disables.
|
|
48
|
+
*/
|
|
49
|
+
keypair?: {
|
|
50
|
+
publicKey: Uint8Array;
|
|
51
|
+
privateKey: Uint8Array;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Override the SDK constructor — used by tests to inject a fake
|
|
55
|
+
* MessageSDK whose `connect` / `onMessage` / `sendToChannel` are
|
|
56
|
+
* spies. Production code leaves this `undefined`.
|
|
57
|
+
*/
|
|
58
|
+
sdkFactory?: (logger: Logger) => MessageSDK;
|
|
59
|
+
/**
|
|
60
|
+
* Override the token provider builder — used by tests to skip the
|
|
61
|
+
* real Agent Gateway round-trip.
|
|
62
|
+
*/
|
|
63
|
+
tokenProviderFactory?: (opts: {
|
|
64
|
+
agentGatewayUrl: string;
|
|
65
|
+
keypair: {
|
|
66
|
+
publicKey: Uint8Array;
|
|
67
|
+
privateKey: Uint8Array;
|
|
68
|
+
};
|
|
69
|
+
}) => TokenProvider;
|
|
70
|
+
/**
|
|
71
|
+
* Local agent identity surfaced into `extractChatPrompt` for
|
|
72
|
+
* sessionKey prefixing. Defaults to the empty string — fine for the
|
|
73
|
+
* single-session-multiplex topology.
|
|
74
|
+
*/
|
|
75
|
+
localAgentId?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Override the retry backoff schedule (ms). Defaults to
|
|
78
|
+
* `DEFAULT_RETRY_SCHEDULE_MS`. Tests pass small numbers; production
|
|
79
|
+
* leaves it `undefined`.
|
|
80
|
+
*/
|
|
81
|
+
retrySchedule?: readonly number[];
|
|
82
|
+
/**
|
|
83
|
+
* Test seam — `setTimeout` / `clearTimeout` shim. Defaults to
|
|
84
|
+
* the global `setTimeout` family. Tests inject a deterministic
|
|
85
|
+
* scheduler so they don't need real timers.
|
|
86
|
+
*/
|
|
87
|
+
scheduler?: {
|
|
88
|
+
setTimeout: (cb: () => void, ms: number) => unknown;
|
|
89
|
+
clearTimeout: (handle: unknown) => void;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export declare class MessageBusPlugin implements AgentPlugin {
|
|
93
|
+
private readonly opts;
|
|
94
|
+
readonly name = "message-bus";
|
|
95
|
+
private ctx?;
|
|
96
|
+
private sdk;
|
|
97
|
+
private tracker;
|
|
98
|
+
private resolvedPrincipalId;
|
|
99
|
+
private stopped;
|
|
100
|
+
private retryHandle;
|
|
101
|
+
private retryResolve;
|
|
102
|
+
private connectAttempt;
|
|
103
|
+
private legacyDeprecationLogged;
|
|
104
|
+
private connectSettled;
|
|
105
|
+
private resolveConnectSettled;
|
|
106
|
+
constructor(opts?: MessageBusPluginOptions);
|
|
107
|
+
init(ctx: PluginContext): Promise<void>;
|
|
108
|
+
start(): Promise<void>;
|
|
109
|
+
stop(): Promise<void>;
|
|
110
|
+
get principalId(): string;
|
|
111
|
+
/**
|
|
112
|
+
* Test helper — resolves once `start()`'s connect loop has either
|
|
113
|
+
* successfully connected or `stop()` was called. Production code
|
|
114
|
+
* never needs this.
|
|
115
|
+
*/
|
|
116
|
+
whenSettled(): Promise<void>;
|
|
117
|
+
private runConnectLoop;
|
|
118
|
+
/**
|
|
119
|
+
* Single connect attempt. Returns `true` on success (sdk connected,
|
|
120
|
+
* handlers + listener installed) and `false` on any failure (caller
|
|
121
|
+
* should schedule a retry).
|
|
122
|
+
*
|
|
123
|
+
* Each attempt builds a FRESH `MessageSDK` instance — re-using one
|
|
124
|
+
* across reconnects would carry stale handlers and confuse the
|
|
125
|
+
* centrifuge-js internal state. The SDK has its own internal
|
|
126
|
+
* reconnect once `.connect()` succeeds; this loop is for the OPENING
|
|
127
|
+
* handshake only.
|
|
128
|
+
*/
|
|
129
|
+
private tryConnectOnce;
|
|
130
|
+
private nextDelayMs;
|
|
131
|
+
/**
|
|
132
|
+
* Log an attempt failure with attenuated severity — first attempt
|
|
133
|
+
* + a few well-spaced retries get a `warn`, the rest are `debug` to
|
|
134
|
+
* avoid log spam when an instance has never been provisioned.
|
|
135
|
+
*/
|
|
136
|
+
private logAttemptFailure;
|
|
137
|
+
private logRetryScheduled;
|
|
138
|
+
/**
|
|
139
|
+
* Sleep `ms` milliseconds. Returns `true` when stop() cancels the
|
|
140
|
+
* timer mid-flight (so the caller breaks out of its loop), or
|
|
141
|
+
* `false` once the timer fires normally.
|
|
142
|
+
*/
|
|
143
|
+
private sleep;
|
|
144
|
+
private cancelPendingRetry;
|
|
145
|
+
private scheduler;
|
|
146
|
+
}
|