@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 @@
|
|
|
1
|
+
{"version":3,"file":"token-provider.js","sourceRoot":"","sources":["../../src/messaging/token-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,OAAO,EACL,4BAA4B,GAG7B,MAAM,uBAAuB,CAAC;AAI/B,sEAAsE;AACtE,kEAAkE;AAClE,uDAAuD;AACvD,MAAM,oBAAoB,GAAG,MAAM,CAAC,IAAI,CACtC,kCAAkC,EAClC,KAAK,CACN,CAAC;AAUF;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAAgB;IAEhB,OAAO;QACL,eAAe,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACxE,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE;YACd,MAAM,OAAO,GACX,OAAO,CAAC,UAAU,CAAC,MAAM,KAAK,EAAE;gBAC9B,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;oBACZ,oBAAoB;oBACpB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;iBAChC,CAAC;gBACJ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAgB,CAAC;gBACrC,GAAG,EAAE,OAAO;gBACZ,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,OAAO;aACd,CAAC,CAAC;YACH,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACvE,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kCAAkC,CAChD,IAAyC;IAEzC,OAAO,4BAA4B,CAAC;QAClC,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,QAAQ,EAAE,0BAA0B,CAAC,IAAI,CAAC,OAAO,CAAC;QAClD,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -4,25 +4,31 @@
|
|
|
4
4
|
* Bridges the device-agent's `AgentHost` to the BeeOS platform via the
|
|
5
5
|
* Agent Control Protocol (ACP) over a WebSocket transport.
|
|
6
6
|
*
|
|
7
|
-
* Responsibilities:
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Responsibilities (v0.9.0):
|
|
8
|
+
* - Open the Bridge WebSocket transport.
|
|
9
|
+
* - Build the per-transport `TransportSessionNotifier` and BIND it
|
|
10
|
+
* to the operator's fixed sid via
|
|
11
|
+
* `ctx.sessionManager.bindNotifier(fixedSessionId, notifier)`. The
|
|
12
|
+
* SessionManager itself, JsonlMessageStore, JsonlSessionSweeper,
|
|
13
|
+
* and PluginCommandRegistry are all owned by `bootstrap.ts` —
|
|
14
|
+
* plugin pulls them via `PluginContext` so MessageBusPlugin can
|
|
15
|
+
* share the SAME instance (the in-flight gate / Layer 3 device-
|
|
16
|
+
* physical-serial constraint depends on a single SessionManager
|
|
17
|
+
* per process).
|
|
18
|
+
* - Wire ACP handlers (`session/*`, `device/*`, `models/list`,
|
|
19
|
+
* `skills/list`) against `ctx.sessionManager`.
|
|
20
|
+
* - Bind the router and fan `session/update` notifications back
|
|
21
|
+
* through the transport for the fixed sid.
|
|
15
22
|
*
|
|
16
|
-
* Single-session sid derivation
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* binary upgrades. Standalone mode (no keypair) falls back to
|
|
21
|
-
* `device:standalone` so local CLI testing still gets a stable sid.
|
|
23
|
+
* Single-session sid derivation lives in `bootstrap.ts`
|
|
24
|
+
* (`deriveFixedSessionId`) — the format is
|
|
25
|
+
* `device:<first16HexOfSha256(publicKey)>` (or `device:standalone`),
|
|
26
|
+
* unchanged from v0.8.0.
|
|
22
27
|
*
|
|
23
28
|
* Disabled when `--standalone` (no remote control plane) or `--no-acp`
|
|
24
29
|
* is passed; in that mode only the http-debug plugin is useful.
|
|
25
30
|
*/
|
|
31
|
+
import { deriveFixedSessionId } from "../../app/fixed-session-id.js";
|
|
26
32
|
import type { IMcpRuntime } from "../../mcp-client/runtime.js";
|
|
27
33
|
import type { IBridgeTransport } from "../../ports/transport.js";
|
|
28
34
|
import type { AgentPlugin, PluginContext } from "../../ports/plugin.js";
|
|
@@ -39,33 +45,24 @@ export interface AcpPluginOptions {
|
|
|
39
45
|
publicKey: Uint8Array;
|
|
40
46
|
privateKey: Uint8Array;
|
|
41
47
|
};
|
|
42
|
-
/**
|
|
43
|
-
* Agent Gateway base URL. Required for the `/screenshot` plugin
|
|
44
|
-
* command to upload frames via presign+confirm. When unset, the
|
|
45
|
-
* command is NOT registered (the device-agent gracefully degrades —
|
|
46
|
-
* `skills/list` returns an empty list and `/screenshot` falls
|
|
47
|
-
* through to the AgentRunner).
|
|
48
|
-
*/
|
|
49
|
-
agentGatewayUrl?: string;
|
|
50
48
|
/** Optional pre-built transport (used by tests for mock injection). */
|
|
51
49
|
transportOverride?: IBridgeTransport;
|
|
52
|
-
/**
|
|
53
|
-
* Override the sessions directory. Tests pin this to a tmp dir; in
|
|
54
|
-
* production the default `${BEEOS_HOME ?? ~/.beeos}/device-agent/sessions`
|
|
55
|
-
* resolves via `getDeviceAgentSessionsDir()`.
|
|
56
|
-
*/
|
|
57
|
-
sessionsDirOverride?: string;
|
|
58
50
|
}
|
|
59
51
|
export declare class AcpPlugin implements AgentPlugin {
|
|
60
52
|
private readonly opts;
|
|
61
53
|
readonly name = "acp";
|
|
62
54
|
private ctx?;
|
|
63
55
|
private transport;
|
|
64
|
-
private
|
|
65
|
-
private store;
|
|
56
|
+
private boundSid;
|
|
66
57
|
private unbind;
|
|
67
58
|
constructor(opts: AcpPluginOptions);
|
|
68
59
|
init(ctx: PluginContext): Promise<void>;
|
|
69
60
|
start(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Visible for tests — exposes the sid this plugin binds to so test
|
|
63
|
+
* harnesses can construct a SessionManager with the matching
|
|
64
|
+
* `fixedSessionId` without re-deriving the value.
|
|
65
|
+
*/
|
|
66
|
+
static deriveFixedSessionId: typeof deriveFixedSessionId;
|
|
70
67
|
stop(): Promise<void>;
|
|
71
68
|
}
|
|
@@ -4,34 +4,32 @@
|
|
|
4
4
|
* Bridges the device-agent's `AgentHost` to the BeeOS platform via the
|
|
5
5
|
* Agent Control Protocol (ACP) over a WebSocket transport.
|
|
6
6
|
*
|
|
7
|
-
* Responsibilities:
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Responsibilities (v0.9.0):
|
|
8
|
+
* - Open the Bridge WebSocket transport.
|
|
9
|
+
* - Build the per-transport `TransportSessionNotifier` and BIND it
|
|
10
|
+
* to the operator's fixed sid via
|
|
11
|
+
* `ctx.sessionManager.bindNotifier(fixedSessionId, notifier)`. The
|
|
12
|
+
* SessionManager itself, JsonlMessageStore, JsonlSessionSweeper,
|
|
13
|
+
* and PluginCommandRegistry are all owned by `bootstrap.ts` —
|
|
14
|
+
* plugin pulls them via `PluginContext` so MessageBusPlugin can
|
|
15
|
+
* share the SAME instance (the in-flight gate / Layer 3 device-
|
|
16
|
+
* physical-serial constraint depends on a single SessionManager
|
|
17
|
+
* per process).
|
|
18
|
+
* - Wire ACP handlers (`session/*`, `device/*`, `models/list`,
|
|
19
|
+
* `skills/list`) against `ctx.sessionManager`.
|
|
20
|
+
* - Bind the router and fan `session/update` notifications back
|
|
21
|
+
* through the transport for the fixed sid.
|
|
15
22
|
*
|
|
16
|
-
* Single-session sid derivation
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* binary upgrades. Standalone mode (no keypair) falls back to
|
|
21
|
-
* `device:standalone` so local CLI testing still gets a stable sid.
|
|
23
|
+
* Single-session sid derivation lives in `bootstrap.ts`
|
|
24
|
+
* (`deriveFixedSessionId`) — the format is
|
|
25
|
+
* `device:<first16HexOfSha256(publicKey)>` (or `device:standalone`),
|
|
26
|
+
* unchanged from v0.8.0.
|
|
22
27
|
*
|
|
23
28
|
* Disabled when `--standalone` (no remote control plane) or `--no-acp`
|
|
24
29
|
* is passed; in that mode only the http-debug plugin is useful.
|
|
25
30
|
*/
|
|
26
|
-
import { fingerprint as sha256Fingerprint } from "@beeos-ai/bridge-client";
|
|
27
31
|
import { BridgeWsTransport } from "../../adapters/transport-bridge-ws.js";
|
|
28
|
-
import {
|
|
29
|
-
import { JsonlMessageStore } from "../../app/jsonl-store.js";
|
|
30
|
-
import { PluginCommandRegistry } from "../../app/plugin-commands/registry.js";
|
|
31
|
-
import { ScreenshotCommandHandler } from "../../app/plugin-commands/screenshot-command.js";
|
|
32
|
-
import { ScreenshotService } from "../../app/screenshot-service.js";
|
|
33
|
-
import { SessionManager } from "../../app/session-manager.js";
|
|
34
|
-
import { getDeviceAgentSessionsDir } from "../../config/paths.js";
|
|
32
|
+
import { deriveFixedSessionId } from "../../app/fixed-session-id.js";
|
|
35
33
|
import { AcpDeviceHandlers } from "../../inbound/acp/handlers/device.js";
|
|
36
34
|
import { AcpSessionHandlers } from "../../inbound/acp/handlers/session.js";
|
|
37
35
|
import { AcpSkillsHandlers } from "../../inbound/acp/handlers/skills.js";
|
|
@@ -42,15 +40,14 @@ export class AcpPlugin {
|
|
|
42
40
|
name = "acp";
|
|
43
41
|
ctx;
|
|
44
42
|
transport = null;
|
|
45
|
-
|
|
46
|
-
store = null;
|
|
43
|
+
boundSid = null;
|
|
47
44
|
unbind = null;
|
|
48
45
|
constructor(opts) {
|
|
49
46
|
this.opts = opts;
|
|
50
47
|
}
|
|
51
48
|
async init(ctx) {
|
|
52
49
|
this.ctx = ctx;
|
|
53
|
-
const { config, log,
|
|
50
|
+
const { config, log, sessionManager } = ctx;
|
|
54
51
|
if (config.disableAcp) {
|
|
55
52
|
log.info("acp: disabled by --no-acp / disableAcp config");
|
|
56
53
|
return;
|
|
@@ -69,93 +66,18 @@ export class AcpPlugin {
|
|
|
69
66
|
transport.on("connected", () => log.info("acp: bridge connected"));
|
|
70
67
|
transport.on("disconnected", (reason) => log.warn({ reason }, "acp: bridge disconnected"));
|
|
71
68
|
transport.on("error", (err) => log.error({ err: err.message }, "acp: bridge transport error"));
|
|
69
|
+
// Bind the operator's fixed sid to the Bridge WS transport. MS
|
|
70
|
+
// sessions get their own per-channel notifiers via
|
|
71
|
+
// `MsChannelNotifier` (bound by MessageBusPlugin), so the two
|
|
72
|
+
// surfaces never publish onto each other's channels.
|
|
72
73
|
const fixedSessionId = deriveFixedSessionId(this.opts.keypair.publicKey);
|
|
73
|
-
const sessionsDir = this.opts.sessionsDirOverride ?? getDeviceAgentSessionsDir();
|
|
74
|
-
const messageStore = await JsonlMessageStore.open({
|
|
75
|
-
sessionsDir,
|
|
76
|
-
preloadSids: [fixedSessionId],
|
|
77
|
-
logger: { warn: (...args) => log.warn(...args) },
|
|
78
|
-
});
|
|
79
|
-
log.info({ fixedSessionId, sessionsDir }, "acp: jsonl store ready");
|
|
80
74
|
const notifier = new TransportSessionNotifier(transport);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// degraded modes (e.g. local dev without an agent gateway URL still
|
|
84
|
-
// boots, just without `/screenshot`).
|
|
85
|
-
const pluginCommands = new PluginCommandRegistry();
|
|
86
|
-
if (this.opts.agentGatewayUrl) {
|
|
87
|
-
const uploader = new AgentGatewayUserFileUploader({
|
|
88
|
-
agentGatewayUrl: this.opts.agentGatewayUrl,
|
|
89
|
-
identity: {
|
|
90
|
-
publicKey: this.opts.keypair.publicKey,
|
|
91
|
-
privateKey: this.opts.keypair.privateKey,
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
// ScreenshotService takes the live MCP runtime — bind to a single
|
|
95
|
-
// resolved instance per plugin lifetime; on autospawn restart the
|
|
96
|
-
// outer mcpProvider issues a fresh runtime and the next call will
|
|
97
|
-
// observe it transparently (the service captures the runtime at
|
|
98
|
-
// each `captureAndUpload` call, NOT at construction).
|
|
99
|
-
const lazyMcp = {
|
|
100
|
-
screenshot: () => this.opts.mcpProvider().screenshot(),
|
|
101
|
-
uiDump: (o) => this.opts.mcpProvider().uiDump(o),
|
|
102
|
-
act: (a) => this.opts.mcpProvider().act(a),
|
|
103
|
-
};
|
|
104
|
-
const screenshotService = new ScreenshotService(lazyMcp, uploader);
|
|
105
|
-
pluginCommands.register(new ScreenshotCommandHandler(screenshotService));
|
|
106
|
-
log.info({ commands: pluginCommands.list().map((c) => c.name) }, "acp: plugin commands registered");
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
log.info("acp: agentGatewayUrl unset — skipping `/screenshot` and other upload-dependent plugin commands");
|
|
110
|
-
}
|
|
111
|
-
const sessions = new SessionManager({
|
|
112
|
-
host,
|
|
113
|
-
messageStore,
|
|
114
|
-
notifier,
|
|
115
|
-
fixedSessionId,
|
|
116
|
-
pluginCommands,
|
|
117
|
-
// `Config.sessionTimeoutMs` is the single source of truth for idle GC.
|
|
118
|
-
// 0 disables the threshold check (effectively keeps sessions forever
|
|
119
|
-
// until `maxSessions` LRU eviction kicks in) — it's surfaced in the
|
|
120
|
-
// schema so kiosk deployments can pin a deterministic value.
|
|
121
|
-
sessionTtlMs: config.sessionTimeoutMs > 0 ? config.sessionTimeoutMs : undefined,
|
|
122
|
-
onSetModel: async (provider, model) => {
|
|
123
|
-
try {
|
|
124
|
-
await host.agent.setModel(provider, model);
|
|
125
|
-
log.info({ provider, model }, "acp: agent model switched");
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
log.warn({ err: e.message, provider, model }, "acp: setModel failed");
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
// Surface plugin command lifecycle into the agent's pino log so
|
|
132
|
-
// failures are auditable. Without this hook the only signal a failed
|
|
133
|
-
// `/screenshot` produces is a `system_message_chunk` notice, which
|
|
134
|
-
// some clients render and some swallow — leaving operators staring
|
|
135
|
-
// at silent JSONL.
|
|
136
|
-
onPluginCommandEvent: (ev) => {
|
|
137
|
-
if (ev.kind === "begin") {
|
|
138
|
-
log.info({ command: ev.name, sessionId: ev.sessionId, turnId: ev.turnId, args: ev.args }, "acp: plugin command begin");
|
|
139
|
-
}
|
|
140
|
-
else if (ev.kind === "end") {
|
|
141
|
-
log.info({ command: ev.name, sessionId: ev.sessionId, turnId: ev.turnId, durationMs: ev.durationMs }, "acp: plugin command end");
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
log.warn({
|
|
145
|
-
command: ev.name,
|
|
146
|
-
sessionId: ev.sessionId,
|
|
147
|
-
turnId: ev.turnId,
|
|
148
|
-
durationMs: ev.durationMs,
|
|
149
|
-
err: ev.error.message,
|
|
150
|
-
stack: ev.error.stack,
|
|
151
|
-
}, "acp: plugin command failed");
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
});
|
|
75
|
+
sessionManager.bindNotifier(fixedSessionId, notifier);
|
|
76
|
+
this.boundSid = fixedSessionId;
|
|
155
77
|
const router = new AcpRouter({
|
|
156
78
|
device: new AcpDeviceHandlers(this.opts.mcpProvider),
|
|
157
|
-
session: new AcpSessionHandlers(
|
|
158
|
-
skills: new AcpSkillsHandlers(
|
|
79
|
+
session: new AcpSessionHandlers(sessionManager),
|
|
80
|
+
skills: new AcpSkillsHandlers(sessionManager.pluginCommandRegistry),
|
|
159
81
|
log: ({ method, error }) => {
|
|
160
82
|
if (error)
|
|
161
83
|
log.warn({ method, err: error.message }, "acp handler error");
|
|
@@ -164,8 +86,6 @@ export class AcpPlugin {
|
|
|
164
86
|
},
|
|
165
87
|
});
|
|
166
88
|
this.transport = transport;
|
|
167
|
-
this.sessions = sessions;
|
|
168
|
-
this.store = messageStore;
|
|
169
89
|
this.unbind = bindRouterToTransport(router, transport);
|
|
170
90
|
}
|
|
171
91
|
async start() {
|
|
@@ -173,45 +93,32 @@ export class AcpPlugin {
|
|
|
173
93
|
return;
|
|
174
94
|
await this.transport.connect();
|
|
175
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Visible for tests — exposes the sid this plugin binds to so test
|
|
98
|
+
* harnesses can construct a SessionManager with the matching
|
|
99
|
+
* `fixedSessionId` without re-deriving the value.
|
|
100
|
+
*/
|
|
101
|
+
static deriveFixedSessionId = deriveFixedSessionId;
|
|
176
102
|
async stop() {
|
|
177
|
-
if (this.
|
|
178
|
-
this.sessions.detach();
|
|
179
|
-
this.sessions = null;
|
|
180
|
-
}
|
|
181
|
-
if (this.unbind) {
|
|
103
|
+
if (this.boundSid && this.ctx) {
|
|
182
104
|
try {
|
|
183
|
-
|
|
105
|
+
this.ctx.sessionManager.unbindNotifier(this.boundSid);
|
|
184
106
|
}
|
|
185
107
|
catch {
|
|
186
108
|
/* ignore */
|
|
187
109
|
}
|
|
188
|
-
this.
|
|
110
|
+
this.boundSid = null;
|
|
189
111
|
}
|
|
190
|
-
if (this.
|
|
112
|
+
if (this.unbind) {
|
|
191
113
|
try {
|
|
192
|
-
await this.
|
|
114
|
+
await this.unbind();
|
|
193
115
|
}
|
|
194
|
-
catch
|
|
195
|
-
|
|
196
|
-
this.ctx?.log.warn({ err: e.message }, "acp: jsonl store close failed");
|
|
116
|
+
catch {
|
|
117
|
+
/* ignore */
|
|
197
118
|
}
|
|
198
|
-
this.
|
|
119
|
+
this.unbind = null;
|
|
199
120
|
}
|
|
200
121
|
this.transport = null;
|
|
201
122
|
}
|
|
202
123
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Compute the per-device fixed sid.
|
|
205
|
-
*
|
|
206
|
-
* Format: `device:<first16HexOfSha256(publicKey)>`. 16 hex chars give
|
|
207
|
-
* 64 bits of entropy — far more than enough to avoid collisions
|
|
208
|
-
* across the BeeOS device fleet, while keeping the sid short enough
|
|
209
|
-
* to use as a filename / log correlation tag without truncation.
|
|
210
|
-
*/
|
|
211
|
-
function deriveFixedSessionId(publicKey) {
|
|
212
|
-
if (!publicKey || publicKey.length === 0)
|
|
213
|
-
return "device:standalone";
|
|
214
|
-
const hex = sha256Fingerprint(publicKey);
|
|
215
|
-
return `device:${hex.slice(0, 16)}`;
|
|
216
|
-
}
|
|
217
124
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugins/acp/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugins/acp/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Busy-reply path — emits a terminal `agent_reply` with `error: agent_busy`
|
|
3
|
+
* back to the caller's IM channel when a second `chat_message` arrives
|
|
4
|
+
* while a turn is already in flight.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the SessionManager's "one turn per session" contract that the
|
|
7
|
+
* ACP handlers enforce — concurrent prompts on the same session are
|
|
8
|
+
* rejected, not queued. The MS path keeps the same semantics so callers
|
|
9
|
+
* (L0 / A2A / MCP / OpenAPI) get a deterministic refusal instead of a
|
|
10
|
+
* `/wait` timeout.
|
|
11
|
+
*
|
|
12
|
+
* Type/payload alignment: uses the same `agent_reply` type that the
|
|
13
|
+
* outbound listener emits, with a `{ error }` body that matches
|
|
14
|
+
* `backend/pkg/chatinvoke/invoker.go::extractReplyFinal` (which reads
|
|
15
|
+
* `payload.error` first, falling back to `payload.text`).
|
|
16
|
+
*/
|
|
17
|
+
import type { MessageSDK } from "@beeos-ai/message-sdk";
|
|
18
|
+
export interface BusyReplyRequest {
|
|
19
|
+
channelId: string;
|
|
20
|
+
inboundMessageId: string;
|
|
21
|
+
/**
|
|
22
|
+
* Session key derived from the rejected envelope (via
|
|
23
|
+
* `extractChatPrompt(envelope).sessionKey`). Always set so the busy
|
|
24
|
+
* reply payload has the same `{ session_key, error }` shape as a
|
|
25
|
+
* regular `agent_reply`.
|
|
26
|
+
*/
|
|
27
|
+
sessionKey: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BusyReplyHandler {
|
|
30
|
+
replyBusy(req: BusyReplyRequest): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface BusyReplyHandlerOptions {
|
|
33
|
+
sdk: MessageSDK;
|
|
34
|
+
}
|
|
35
|
+
export declare class MessageSdkBusyReplyHandler implements BusyReplyHandler {
|
|
36
|
+
private readonly opts;
|
|
37
|
+
constructor(opts: BusyReplyHandlerOptions);
|
|
38
|
+
replyBusy(req: BusyReplyRequest): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Busy-reply path — emits a terminal `agent_reply` with `error: agent_busy`
|
|
3
|
+
* back to the caller's IM channel when a second `chat_message` arrives
|
|
4
|
+
* while a turn is already in flight.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the SessionManager's "one turn per session" contract that the
|
|
7
|
+
* ACP handlers enforce — concurrent prompts on the same session are
|
|
8
|
+
* rejected, not queued. The MS path keeps the same semantics so callers
|
|
9
|
+
* (L0 / A2A / MCP / OpenAPI) get a deterministic refusal instead of a
|
|
10
|
+
* `/wait` timeout.
|
|
11
|
+
*
|
|
12
|
+
* Type/payload alignment: uses the same `agent_reply` type that the
|
|
13
|
+
* outbound listener emits, with a `{ error }` body that matches
|
|
14
|
+
* `backend/pkg/chatinvoke/invoker.go::extractReplyFinal` (which reads
|
|
15
|
+
* `payload.error` first, falling back to `payload.text`).
|
|
16
|
+
*/
|
|
17
|
+
export class MessageSdkBusyReplyHandler {
|
|
18
|
+
opts;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
}
|
|
22
|
+
async replyBusy(req) {
|
|
23
|
+
await this.opts.sdk.sendToChannel(req.channelId, "agent_reply", {
|
|
24
|
+
session_key: req.sessionKey,
|
|
25
|
+
error: "agent_busy",
|
|
26
|
+
text: "device-agent is processing another request — please retry once it completes.",
|
|
27
|
+
}, { inReplyTo: req.inboundMessageId });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=busy-reply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"busy-reply.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/busy-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAE9D,KAAK,CAAC,SAAS,CAAC,GAAqB;QACnC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAC/B,GAAG,CAAC,SAAS,EACb,aAAa,EACb;YACE,WAAW,EAAE,GAAG,CAAC,UAAU;YAC3B,KAAK,EAAE,YAAY;YACnB,IAAI,EAAE,8EAA8E;SACrF,EACD,EAAE,SAAS,EAAE,GAAG,CAAC,gBAAgB,EAAE,CACpC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound — `chat_cancel` envelope handler.
|
|
3
|
+
*
|
|
4
|
+
* The BeeOS platform pushes a protocol-agnostic `chat_cancel` envelope
|
|
5
|
+
* onto the agent's personal Centrifugo channel when an in-flight
|
|
6
|
+
* `chat_message` turn must be aborted. Triggers include:
|
|
7
|
+
*
|
|
8
|
+
* - A2A Service `tasks/cancel` JSON-RPC (caller-initiated)
|
|
9
|
+
* - A2A Service `InvokeTask` ctx-cancel fallback (caller HTTP
|
|
10
|
+
* disconnect / deadline)
|
|
11
|
+
* - A2A Service timeout sweeper (task exceeded `timeout_ms`)
|
|
12
|
+
*
|
|
13
|
+
* The wire payload (see `backend/pkg/convmsg/extract.go`
|
|
14
|
+
* `MessageTypeChatCancel`) carries IM primitives only:
|
|
15
|
+
*
|
|
16
|
+
* {
|
|
17
|
+
* "target_message_id": "<message_id of the original chat_message>",
|
|
18
|
+
* "reason": "canceled" | "caller_disconnected" | "timeout"
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Agents MUST NOT decode protocol headers (no `task_id`, no
|
|
22
|
+
* `a2a_task_cancel`) — that contract is enforced here by intentionally
|
|
23
|
+
* reading nothing besides `target_message_id` and `reason`.
|
|
24
|
+
*
|
|
25
|
+
* v0.9.0 lookup path: query the tiny `MsInflightTracker` for the
|
|
26
|
+
* matching sid, then call `sessionManager.cancel()`. The SessionManager
|
|
27
|
+
* itself owns the AbortController and only allows ONE in-flight turn
|
|
28
|
+
* globally — so resolving by inboundMessageId is unambiguous (either
|
|
29
|
+
* the current turn is THIS turn, or the cancel is for a stale /
|
|
30
|
+
* already-completed turn we should silently drop).
|
|
31
|
+
*
|
|
32
|
+
* No-ops:
|
|
33
|
+
* - missing / non-string `target_message_id` → drop, debug log
|
|
34
|
+
* - tracker has no entry for that message_id → drop, debug log
|
|
35
|
+
* (the turn already finished, or this is a stray cancel for a
|
|
36
|
+
* different agent on a shared principal — both safe to ignore)
|
|
37
|
+
*/
|
|
38
|
+
import type { MessageEnvelope } from "@beeos-ai/message-sdk";
|
|
39
|
+
import type { SessionManager } from "../../app/session-manager.js";
|
|
40
|
+
import type { Logger } from "../../logger/index.js";
|
|
41
|
+
import type { MsInflightTracker } from "./ms-inflight-tracker.js";
|
|
42
|
+
export declare const MESSAGE_TYPE_CHAT_CANCEL = "chat_cancel";
|
|
43
|
+
export interface MessageBusCancelHandlerOptions {
|
|
44
|
+
sessionManager: SessionManager;
|
|
45
|
+
tracker: MsInflightTracker;
|
|
46
|
+
logger: Logger;
|
|
47
|
+
}
|
|
48
|
+
export declare function buildMessageBusCancelHandler(opts: MessageBusCancelHandlerOptions): (envelope: MessageEnvelope) => Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound — `chat_cancel` envelope handler.
|
|
3
|
+
*
|
|
4
|
+
* The BeeOS platform pushes a protocol-agnostic `chat_cancel` envelope
|
|
5
|
+
* onto the agent's personal Centrifugo channel when an in-flight
|
|
6
|
+
* `chat_message` turn must be aborted. Triggers include:
|
|
7
|
+
*
|
|
8
|
+
* - A2A Service `tasks/cancel` JSON-RPC (caller-initiated)
|
|
9
|
+
* - A2A Service `InvokeTask` ctx-cancel fallback (caller HTTP
|
|
10
|
+
* disconnect / deadline)
|
|
11
|
+
* - A2A Service timeout sweeper (task exceeded `timeout_ms`)
|
|
12
|
+
*
|
|
13
|
+
* The wire payload (see `backend/pkg/convmsg/extract.go`
|
|
14
|
+
* `MessageTypeChatCancel`) carries IM primitives only:
|
|
15
|
+
*
|
|
16
|
+
* {
|
|
17
|
+
* "target_message_id": "<message_id of the original chat_message>",
|
|
18
|
+
* "reason": "canceled" | "caller_disconnected" | "timeout"
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Agents MUST NOT decode protocol headers (no `task_id`, no
|
|
22
|
+
* `a2a_task_cancel`) — that contract is enforced here by intentionally
|
|
23
|
+
* reading nothing besides `target_message_id` and `reason`.
|
|
24
|
+
*
|
|
25
|
+
* v0.9.0 lookup path: query the tiny `MsInflightTracker` for the
|
|
26
|
+
* matching sid, then call `sessionManager.cancel()`. The SessionManager
|
|
27
|
+
* itself owns the AbortController and only allows ONE in-flight turn
|
|
28
|
+
* globally — so resolving by inboundMessageId is unambiguous (either
|
|
29
|
+
* the current turn is THIS turn, or the cancel is for a stale /
|
|
30
|
+
* already-completed turn we should silently drop).
|
|
31
|
+
*
|
|
32
|
+
* No-ops:
|
|
33
|
+
* - missing / non-string `target_message_id` → drop, debug log
|
|
34
|
+
* - tracker has no entry for that message_id → drop, debug log
|
|
35
|
+
* (the turn already finished, or this is a stray cancel for a
|
|
36
|
+
* different agent on a shared principal — both safe to ignore)
|
|
37
|
+
*/
|
|
38
|
+
export const MESSAGE_TYPE_CHAT_CANCEL = "chat_cancel";
|
|
39
|
+
export function buildMessageBusCancelHandler(opts) {
|
|
40
|
+
return async (envelope) => {
|
|
41
|
+
const payload = (envelope.payload ?? {});
|
|
42
|
+
const targetMessageId = pickString(payload.target_message_id);
|
|
43
|
+
if (!targetMessageId) {
|
|
44
|
+
opts.logger.debug({ type: envelope.type, message_id: envelope.message_id }, "message-bus: chat_cancel dropped (no target_message_id)");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const reason = pickString(payload.reason) || "unknown";
|
|
48
|
+
const entry = opts.tracker.findByInboundMessageId(targetMessageId);
|
|
49
|
+
if (!entry) {
|
|
50
|
+
opts.logger.debug({ targetMessageId, reason }, "message-bus: chat_cancel for unknown / already-finished turn — no-op");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const cancelled = await opts.sessionManager.cancel();
|
|
54
|
+
opts.logger.info({
|
|
55
|
+
sid: entry.sid,
|
|
56
|
+
channelId: entry.channelId,
|
|
57
|
+
targetMessageId,
|
|
58
|
+
reason,
|
|
59
|
+
cancelled,
|
|
60
|
+
}, "message-bus: chat_cancel received — aborting in-flight turn");
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function pickString(v) {
|
|
64
|
+
return typeof v === "string" && v.length > 0 ? v : "";
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=cancel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/cancel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AASH,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAC;AAQtD,MAAM,UAAU,4BAA4B,CAC1C,IAAoC;IAEpC,OAAO,KAAK,EAAE,QAAQ,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAA4B,CAAC;QACpE,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,EACxD,yDAAyD,CAC1D,CAAC;YACF,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;QAEvD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC;QACnE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,EAAE,eAAe,EAAE,MAAM,EAAE,EAC3B,sEAAsE,CACvE,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;YACE,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,eAAe;YACf,MAAM;YACN,SAAS;SACV,EACD,6DAA6D,CAC9D,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { type MessageEnvelope, type MessageSDK } from "@beeos-ai/message-sdk";
|
|
42
|
+
import type { SessionManager } from "../../app/session-manager.js";
|
|
43
|
+
import type { Logger } from "../../logger/index.js";
|
|
44
|
+
import type { BusyReplyHandler } from "./busy-reply.js";
|
|
45
|
+
import type { MsInflightTracker } from "./ms-inflight-tracker.js";
|
|
46
|
+
export interface MessageBusInboundOptions {
|
|
47
|
+
sessionManager: SessionManager;
|
|
48
|
+
sdk: MessageSDK;
|
|
49
|
+
busyReply: BusyReplyHandler;
|
|
50
|
+
tracker: MsInflightTracker;
|
|
51
|
+
logger: Logger;
|
|
52
|
+
/**
|
|
53
|
+
* Local agent identity surfaced to `extractChatPrompt`. Used only for
|
|
54
|
+
* sessionKey prefixing; safe to leave empty in standalone-style
|
|
55
|
+
* tests.
|
|
56
|
+
*/
|
|
57
|
+
localAgentId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Source tag passed to `extractChatPrompt`. Identifies which Centrifugo
|
|
60
|
+
* envelope type triggered the call — `chat_message` is the primary L0
|
|
61
|
+
* path, `agent_request` is the legacy A2A SendTask shim.
|
|
62
|
+
*/
|
|
63
|
+
source: "chat_message" | "agent_request";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the per-envelope handler. The returned function is what gets
|
|
67
|
+
* registered with `sdk.onMessage(type, handler)` — keeping the factory
|
|
68
|
+
* shape lets unit tests instantiate the closure with mock collaborators
|
|
69
|
+
* and feed envelopes synchronously.
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildMessageBusInboundHandler(opts: MessageBusInboundOptions): (envelope: MessageEnvelope) => Promise<void>;
|