@botcord/daemon 0.2.83 → 0.2.84
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/cloud-daemon.js +25 -1
- package/dist/cloud-gateway-runtime.d.ts +18 -1
- package/dist/cloud-gateway-runtime.js +33 -3
- package/dist/provision.d.ts +8 -0
- package/dist/provision.js +3 -2
- package/package.json +2 -2
- package/src/__tests__/cloud-gateway-runtime.test.ts +62 -1
- package/src/cloud-daemon.ts +25 -1
- package/src/cloud-gateway-runtime.ts +53 -2
- package/src/provision.ts +18 -2
package/dist/cloud-daemon.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* See ``docs/cloud-agent-technical-design.md`` §4 + §6.
|
|
11
11
|
*/
|
|
12
|
-
import { shouldWake } from "@botcord/protocol-core";
|
|
12
|
+
import { CONTROL_FRAME_TYPES, shouldWake } from "@botcord/protocol-core";
|
|
13
13
|
import { Gateway, resolveTranscriptEnabled, } from "./gateway/index.js";
|
|
14
14
|
import { ActivityTracker } from "./activity-tracker.js";
|
|
15
15
|
import { SESSIONS_PATH, SNAPSHOT_PATH } from "./config.js";
|
|
@@ -201,10 +201,34 @@ export async function startCloudDaemon(opts) {
|
|
|
201
201
|
if (!opts.disableControlChannel) {
|
|
202
202
|
const auth = asUserAuthManager(new CloudAuthManager(cloudCfg));
|
|
203
203
|
const provisionerFactory = opts.provisionerFactory ?? createProvisioner;
|
|
204
|
+
// Forward-declare controlChannel-bound emitter: the provisioner is
|
|
205
|
+
// built before the channel exists, so the closure captures the slot
|
|
206
|
+
// and reads it back lazily once cloud-daemon assigns the instance.
|
|
207
|
+
const cloudGatewayTypingEmitter = (event) => {
|
|
208
|
+
const ch = controlChannel;
|
|
209
|
+
if (!ch)
|
|
210
|
+
return;
|
|
211
|
+
ch.send({
|
|
212
|
+
id: `cgrs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
213
|
+
type: CONTROL_FRAME_TYPES.CLOUD_GATEWAY_RUNTIME_STATUS,
|
|
214
|
+
params: {
|
|
215
|
+
eventId: event.eventId,
|
|
216
|
+
turnId: event.turnId,
|
|
217
|
+
gatewayId: event.gatewayId,
|
|
218
|
+
agentId: event.agentId,
|
|
219
|
+
conversationId: event.conversationId,
|
|
220
|
+
kind: "typing",
|
|
221
|
+
phase: event.phase,
|
|
222
|
+
traceId: event.traceId ?? null,
|
|
223
|
+
},
|
|
224
|
+
ts: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
};
|
|
204
227
|
const provisioner = provisionerFactory({
|
|
205
228
|
gateway,
|
|
206
229
|
policyResolver,
|
|
207
230
|
onAgentInstalled,
|
|
231
|
+
cloudGatewayTypingEmitter,
|
|
208
232
|
});
|
|
209
233
|
const ControlChannelCtor = opts.controlChannelFactory ?? ControlChannel;
|
|
210
234
|
controlChannel = new ControlChannelCtor({
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { type GatewayInboundFrame } from "@botcord/protocol-core";
|
|
2
2
|
import type { Gateway, GatewayLogger } from "./gateway/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fire-and-forget hook invoked when the dispatcher signals "typing" while
|
|
5
|
+
* a cloud-gateway runtime turn is in flight. The cloud daemon wires this
|
|
6
|
+
* to a control-plane `cloud_gateway_runtime_status` push so the Hub can
|
|
7
|
+
* relay it to the ingress runtime WS and the third-party provider can
|
|
8
|
+
* render a typing indicator.
|
|
9
|
+
*/
|
|
10
|
+
export interface CloudGatewayTypingEvent {
|
|
11
|
+
eventId: string;
|
|
12
|
+
turnId: string;
|
|
13
|
+
gatewayId: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
conversationId: string;
|
|
16
|
+
phase: "started" | "stopped";
|
|
17
|
+
traceId?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export type CloudGatewayTypingEmitter = (event: CloudGatewayTypingEvent) => void;
|
|
3
20
|
export interface CloudGatewayRuntimeResult {
|
|
4
21
|
accepted: boolean;
|
|
5
22
|
eventId: string;
|
|
@@ -26,4 +43,4 @@ export interface CloudGatewayRuntimeResult {
|
|
|
26
43
|
* gateway_outbound_complete for gateway-ingress, which then calls the real
|
|
27
44
|
* provider API.
|
|
28
45
|
*/
|
|
29
|
-
export declare function handleCloudGatewayRuntimeInbound(gateway: Gateway, frame: GatewayInboundFrame, log?: GatewayLogger): Promise<CloudGatewayRuntimeResult>;
|
|
46
|
+
export declare function handleCloudGatewayRuntimeInbound(gateway: Gateway, frame: GatewayInboundFrame, log?: GatewayLogger, onTyping?: CloudGatewayTypingEmitter): Promise<CloudGatewayRuntimeResult>;
|
|
@@ -9,7 +9,7 @@ import { RUNTIME_FRAME_TYPES, } from "@botcord/protocol-core";
|
|
|
9
9
|
* gateway_outbound_complete for gateway-ingress, which then calls the real
|
|
10
10
|
* provider API.
|
|
11
11
|
*/
|
|
12
|
-
export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
|
|
12
|
+
export async function handleCloudGatewayRuntimeInbound(gateway, frame, log, onTyping) {
|
|
13
13
|
if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
|
|
14
14
|
return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
|
|
15
15
|
}
|
|
@@ -25,6 +25,7 @@ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
|
|
|
25
25
|
let accepted = false;
|
|
26
26
|
let outboundText = null;
|
|
27
27
|
let providerMessageId;
|
|
28
|
+
const turnId = `turn_${frame.event_id}`;
|
|
28
29
|
const channel = createRuntimeRelayChannel({
|
|
29
30
|
id: frame.gateway_id,
|
|
30
31
|
provider: frame.provider,
|
|
@@ -34,6 +35,29 @@ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
|
|
|
34
35
|
providerMessageId = ctx.message.traceId ?? null;
|
|
35
36
|
return { providerMessageId };
|
|
36
37
|
},
|
|
38
|
+
onTyping: onTyping
|
|
39
|
+
? (ctx) => {
|
|
40
|
+
try {
|
|
41
|
+
onTyping({
|
|
42
|
+
eventId: frame.event_id,
|
|
43
|
+
turnId,
|
|
44
|
+
gatewayId: frame.gateway_id,
|
|
45
|
+
agentId: frame.agent_id,
|
|
46
|
+
conversationId: frame.message.conversation.id,
|
|
47
|
+
phase: "started",
|
|
48
|
+
traceId: ctx.traceId ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
log?.warn?.("cloud gateway typing emit failed", {
|
|
53
|
+
eventId: frame.event_id,
|
|
54
|
+
gatewayId: frame.gateway_id,
|
|
55
|
+
agentId: frame.agent_id,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
: undefined,
|
|
37
61
|
});
|
|
38
62
|
const message = {
|
|
39
63
|
...frame.message,
|
|
@@ -67,7 +91,7 @@ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
|
|
|
67
91
|
gatewayId: frame.gateway_id,
|
|
68
92
|
agentId: frame.agent_id,
|
|
69
93
|
conversationId: frame.message.conversation.id,
|
|
70
|
-
turnId
|
|
94
|
+
turnId,
|
|
71
95
|
...(outboundText !== null
|
|
72
96
|
? {
|
|
73
97
|
outbound: {
|
|
@@ -83,7 +107,7 @@ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
|
|
|
83
107
|
}
|
|
84
108
|
function createRuntimeRelayChannel(opts) {
|
|
85
109
|
let lastSendAt;
|
|
86
|
-
|
|
110
|
+
const adapter = {
|
|
87
111
|
id: opts.id,
|
|
88
112
|
type: opts.provider,
|
|
89
113
|
async start() {
|
|
@@ -108,6 +132,12 @@ function createRuntimeRelayChannel(opts) {
|
|
|
108
132
|
};
|
|
109
133
|
},
|
|
110
134
|
};
|
|
135
|
+
if (opts.onTyping) {
|
|
136
|
+
adapter.typing = async (ctx) => {
|
|
137
|
+
opts.onTyping(ctx);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return adapter;
|
|
111
141
|
}
|
|
112
142
|
function rejected(frame, code, message) {
|
|
113
143
|
return {
|
package/dist/provision.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { PolicyResolverLike } from "./gateway/policy-resolver.js";
|
|
|
4
4
|
import type { GatewayRuntimeSnapshot } from "./gateway/index.js";
|
|
5
5
|
import { type DaemonConfig } from "./config.js";
|
|
6
6
|
import type { LoginSessionStore } from "./gateway/channels/login-session.js";
|
|
7
|
+
import { type CloudGatewayTypingEmitter } from "./cloud-gateway-runtime.js";
|
|
7
8
|
/**
|
|
8
9
|
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
9
10
|
* provision. Mirrors the credential fields the daemon's per-agent caches
|
|
@@ -58,6 +59,13 @@ export interface ProvisionerOptions {
|
|
|
58
59
|
* spin up a default in-memory store.
|
|
59
60
|
*/
|
|
60
61
|
loginSessions?: LoginSessionStore;
|
|
62
|
+
/**
|
|
63
|
+
* Optional callback invoked when an in-flight `cloud_gateway_runtime_inbound`
|
|
64
|
+
* fires a typing event. The cloud daemon wires this to its control-plane
|
|
65
|
+
* connection so the Hub can relay the hint to the live ingress runtime WS.
|
|
66
|
+
* Absent for local daemons (no ingress path).
|
|
67
|
+
*/
|
|
68
|
+
cloudGatewayTypingEmitter?: CloudGatewayTypingEmitter;
|
|
61
69
|
}
|
|
62
70
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
63
71
|
type AckBody = Omit<ControlAck, "id">;
|
package/dist/provision.js
CHANGED
|
@@ -20,7 +20,7 @@ import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
|
20
20
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
21
21
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
22
22
|
import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
|
|
23
|
-
import { handleCloudGatewayRuntimeInbound } from "./cloud-gateway-runtime.js";
|
|
23
|
+
import { handleCloudGatewayRuntimeInbound, } from "./cloud-gateway-runtime.js";
|
|
24
24
|
/**
|
|
25
25
|
* Build a dispatcher function that routes a `ControlFrame` to the right
|
|
26
26
|
* handler. Returned function signature matches
|
|
@@ -31,6 +31,7 @@ export function createProvisioner(opts) {
|
|
|
31
31
|
const register = opts.register ?? BotCordClient.register;
|
|
32
32
|
const policyResolver = opts.policyResolver;
|
|
33
33
|
const onAgentInstalled = opts.onAgentInstalled;
|
|
34
|
+
const cloudGatewayTypingEmitter = opts.cloudGatewayTypingEmitter;
|
|
34
35
|
const gatewayControl = createGatewayControl({
|
|
35
36
|
gateway,
|
|
36
37
|
...(opts.loginSessions ? { loginSessions: opts.loginSessions } : {}),
|
|
@@ -286,7 +287,7 @@ export function createProvisioner(opts) {
|
|
|
286
287
|
},
|
|
287
288
|
};
|
|
288
289
|
}
|
|
289
|
-
const result = await handleCloudGatewayRuntimeInbound(gateway, runtimeFrame);
|
|
290
|
+
const result = await handleCloudGatewayRuntimeInbound(gateway, runtimeFrame, undefined, cloudGatewayTypingEmitter);
|
|
290
291
|
return result.accepted
|
|
291
292
|
? { ok: true, result }
|
|
292
293
|
: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botcord/daemon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.84",
|
|
4
4
|
"description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@botcord/cli": "^0.1.7",
|
|
31
|
-
"@botcord/protocol-core": "^0.2.
|
|
31
|
+
"@botcord/protocol-core": "^0.2.12",
|
|
32
32
|
"@larksuiteoapi/node-sdk": "^1.63.1",
|
|
33
33
|
"ws": "^8.20.1"
|
|
34
34
|
},
|
|
@@ -4,7 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
5
|
import { RUNTIME_FRAME_TYPES, type GatewayInboundFrame } from "@botcord/protocol-core";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
handleCloudGatewayRuntimeInbound,
|
|
9
|
+
type CloudGatewayTypingEvent,
|
|
10
|
+
} from "../cloud-gateway-runtime.js";
|
|
8
11
|
import { Gateway, type ChannelAdapter } from "../gateway/index.js";
|
|
9
12
|
|
|
10
13
|
describe("cloud gateway runtime inbound", () => {
|
|
@@ -66,6 +69,64 @@ describe("cloud gateway runtime inbound", () => {
|
|
|
66
69
|
expect(result.outbound?.finalText).toBe("hello from runtime");
|
|
67
70
|
});
|
|
68
71
|
|
|
72
|
+
it("invokes the typing emitter when the dispatcher fires typing.started", async () => {
|
|
73
|
+
const typingEvents: CloudGatewayTypingEvent[] = [];
|
|
74
|
+
const gateway = new Gateway({
|
|
75
|
+
config: {
|
|
76
|
+
channels: [],
|
|
77
|
+
defaultRoute: { runtime: "fake", cwd: tmpDir },
|
|
78
|
+
},
|
|
79
|
+
sessionStorePath: path.join(tmpDir, "sessions.json"),
|
|
80
|
+
createChannel: (cfg) => stubChannel(cfg.id, cfg.type, cfg.accountId),
|
|
81
|
+
createRuntime: () => ({
|
|
82
|
+
id: "fake",
|
|
83
|
+
async run() {
|
|
84
|
+
return { text: "ok", newSessionId: "sess_typing" };
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
transcriptEnabled: false,
|
|
88
|
+
});
|
|
89
|
+
await gateway.start();
|
|
90
|
+
|
|
91
|
+
const frame: GatewayInboundFrame = {
|
|
92
|
+
type: RUNTIME_FRAME_TYPES.GATEWAY_INBOUND,
|
|
93
|
+
event_id: "evt_typing",
|
|
94
|
+
gateway_id: "gw_wc_1",
|
|
95
|
+
agent_id: "ag_typing",
|
|
96
|
+
provider: "wechat",
|
|
97
|
+
message: {
|
|
98
|
+
id: "wechat:alice:t1",
|
|
99
|
+
channel: "gw_wc_1",
|
|
100
|
+
accountId: "ag_typing",
|
|
101
|
+
conversation: { id: "wechat:user:alice", kind: "direct" },
|
|
102
|
+
sender: { id: "wechat:user:alice", kind: "user" },
|
|
103
|
+
text: "ping",
|
|
104
|
+
replyTo: null,
|
|
105
|
+
mentioned: true,
|
|
106
|
+
receivedAt: Date.now(),
|
|
107
|
+
trace: { id: "wechat:alice:t1", streamable: false },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = await handleCloudGatewayRuntimeInbound(
|
|
112
|
+
gateway,
|
|
113
|
+
frame,
|
|
114
|
+
undefined,
|
|
115
|
+
(event) => typingEvents.push(event),
|
|
116
|
+
);
|
|
117
|
+
await gateway.stop("test");
|
|
118
|
+
|
|
119
|
+
expect(result.accepted).toBe(true);
|
|
120
|
+
expect(typingEvents.length).toBeGreaterThanOrEqual(1);
|
|
121
|
+
const first = typingEvents[0]!;
|
|
122
|
+
expect(first.eventId).toBe("evt_typing");
|
|
123
|
+
expect(first.gatewayId).toBe("gw_wc_1");
|
|
124
|
+
expect(first.agentId).toBe("ag_typing");
|
|
125
|
+
expect(first.conversationId).toBe("wechat:user:alice");
|
|
126
|
+
expect(first.phase).toBe("started");
|
|
127
|
+
expect(first.traceId).toBe("wechat:alice:t1");
|
|
128
|
+
});
|
|
129
|
+
|
|
69
130
|
it("rejects frames outside the token scope", async () => {
|
|
70
131
|
const gateway = new Gateway({
|
|
71
132
|
config: {
|
package/src/cloud-daemon.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* See ``docs/cloud-agent-technical-design.md`` §4 + §6.
|
|
11
11
|
*/
|
|
12
|
-
import { shouldWake, type AttentionPolicy } from "@botcord/protocol-core";
|
|
12
|
+
import { CONTROL_FRAME_TYPES, shouldWake, type AttentionPolicy } from "@botcord/protocol-core";
|
|
13
|
+
import type { CloudGatewayTypingEmitter } from "./cloud-gateway-runtime.js";
|
|
13
14
|
import {
|
|
14
15
|
Gateway,
|
|
15
16
|
resolveTranscriptEnabled,
|
|
@@ -284,10 +285,33 @@ export async function startCloudDaemon(
|
|
|
284
285
|
if (!opts.disableControlChannel) {
|
|
285
286
|
const auth = asUserAuthManager(new CloudAuthManager(cloudCfg));
|
|
286
287
|
const provisionerFactory = opts.provisionerFactory ?? createProvisioner;
|
|
288
|
+
// Forward-declare controlChannel-bound emitter: the provisioner is
|
|
289
|
+
// built before the channel exists, so the closure captures the slot
|
|
290
|
+
// and reads it back lazily once cloud-daemon assigns the instance.
|
|
291
|
+
const cloudGatewayTypingEmitter: CloudGatewayTypingEmitter = (event) => {
|
|
292
|
+
const ch = controlChannel;
|
|
293
|
+
if (!ch) return;
|
|
294
|
+
ch.send({
|
|
295
|
+
id: `cgrs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
296
|
+
type: CONTROL_FRAME_TYPES.CLOUD_GATEWAY_RUNTIME_STATUS,
|
|
297
|
+
params: {
|
|
298
|
+
eventId: event.eventId,
|
|
299
|
+
turnId: event.turnId,
|
|
300
|
+
gatewayId: event.gatewayId,
|
|
301
|
+
agentId: event.agentId,
|
|
302
|
+
conversationId: event.conversationId,
|
|
303
|
+
kind: "typing",
|
|
304
|
+
phase: event.phase,
|
|
305
|
+
traceId: event.traceId ?? null,
|
|
306
|
+
},
|
|
307
|
+
ts: Date.now(),
|
|
308
|
+
});
|
|
309
|
+
};
|
|
287
310
|
const provisioner = provisionerFactory({
|
|
288
311
|
gateway,
|
|
289
312
|
policyResolver,
|
|
290
313
|
onAgentInstalled,
|
|
314
|
+
cloudGatewayTypingEmitter,
|
|
291
315
|
});
|
|
292
316
|
const ControlChannelCtor = opts.controlChannelFactory ?? ControlChannel;
|
|
293
317
|
controlChannel = new ControlChannelCtor({
|
|
@@ -8,11 +8,31 @@ import type {
|
|
|
8
8
|
ChannelSendContext,
|
|
9
9
|
ChannelSendResult,
|
|
10
10
|
ChannelStatusSnapshot,
|
|
11
|
+
ChannelTypingContext,
|
|
11
12
|
Gateway,
|
|
12
13
|
GatewayInboundMessage,
|
|
13
14
|
GatewayLogger,
|
|
14
15
|
} from "./gateway/index.js";
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Fire-and-forget hook invoked when the dispatcher signals "typing" while
|
|
19
|
+
* a cloud-gateway runtime turn is in flight. The cloud daemon wires this
|
|
20
|
+
* to a control-plane `cloud_gateway_runtime_status` push so the Hub can
|
|
21
|
+
* relay it to the ingress runtime WS and the third-party provider can
|
|
22
|
+
* render a typing indicator.
|
|
23
|
+
*/
|
|
24
|
+
export interface CloudGatewayTypingEvent {
|
|
25
|
+
eventId: string;
|
|
26
|
+
turnId: string;
|
|
27
|
+
gatewayId: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
conversationId: string;
|
|
30
|
+
phase: "started" | "stopped";
|
|
31
|
+
traceId?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CloudGatewayTypingEmitter = (event: CloudGatewayTypingEvent) => void;
|
|
35
|
+
|
|
16
36
|
export interface CloudGatewayRuntimeResult {
|
|
17
37
|
accepted: boolean;
|
|
18
38
|
eventId: string;
|
|
@@ -44,6 +64,7 @@ export async function handleCloudGatewayRuntimeInbound(
|
|
|
44
64
|
gateway: Gateway,
|
|
45
65
|
frame: GatewayInboundFrame,
|
|
46
66
|
log?: GatewayLogger,
|
|
67
|
+
onTyping?: CloudGatewayTypingEmitter,
|
|
47
68
|
): Promise<CloudGatewayRuntimeResult> {
|
|
48
69
|
if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
|
|
49
70
|
return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
|
|
@@ -61,6 +82,7 @@ export async function handleCloudGatewayRuntimeInbound(
|
|
|
61
82
|
let accepted = false;
|
|
62
83
|
let outboundText: string | null = null;
|
|
63
84
|
let providerMessageId: string | null | undefined;
|
|
85
|
+
const turnId = `turn_${frame.event_id}`;
|
|
64
86
|
const channel = createRuntimeRelayChannel({
|
|
65
87
|
id: frame.gateway_id,
|
|
66
88
|
provider: frame.provider,
|
|
@@ -70,6 +92,28 @@ export async function handleCloudGatewayRuntimeInbound(
|
|
|
70
92
|
providerMessageId = ctx.message.traceId ?? null;
|
|
71
93
|
return { providerMessageId };
|
|
72
94
|
},
|
|
95
|
+
onTyping: onTyping
|
|
96
|
+
? (ctx) => {
|
|
97
|
+
try {
|
|
98
|
+
onTyping({
|
|
99
|
+
eventId: frame.event_id,
|
|
100
|
+
turnId,
|
|
101
|
+
gatewayId: frame.gateway_id,
|
|
102
|
+
agentId: frame.agent_id,
|
|
103
|
+
conversationId: frame.message.conversation.id,
|
|
104
|
+
phase: "started",
|
|
105
|
+
traceId: ctx.traceId ?? null,
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
log?.warn?.("cloud gateway typing emit failed", {
|
|
109
|
+
eventId: frame.event_id,
|
|
110
|
+
gatewayId: frame.gateway_id,
|
|
111
|
+
agentId: frame.agent_id,
|
|
112
|
+
error: err instanceof Error ? err.message : String(err),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
: undefined,
|
|
73
117
|
});
|
|
74
118
|
|
|
75
119
|
const message: GatewayInboundMessage = {
|
|
@@ -105,7 +149,7 @@ export async function handleCloudGatewayRuntimeInbound(
|
|
|
105
149
|
gatewayId: frame.gateway_id,
|
|
106
150
|
agentId: frame.agent_id,
|
|
107
151
|
conversationId: frame.message.conversation.id,
|
|
108
|
-
turnId
|
|
152
|
+
turnId,
|
|
109
153
|
...(outboundText !== null
|
|
110
154
|
? {
|
|
111
155
|
outbound: {
|
|
@@ -125,9 +169,10 @@ function createRuntimeRelayChannel(opts: {
|
|
|
125
169
|
provider: string;
|
|
126
170
|
accountId: string;
|
|
127
171
|
onSend: (ctx: ChannelSendContext) => Promise<ChannelSendResult>;
|
|
172
|
+
onTyping?: (ctx: ChannelTypingContext) => void;
|
|
128
173
|
}): ChannelAdapter {
|
|
129
174
|
let lastSendAt: number | undefined;
|
|
130
|
-
|
|
175
|
+
const adapter: ChannelAdapter = {
|
|
131
176
|
id: opts.id,
|
|
132
177
|
type: opts.provider,
|
|
133
178
|
async start() {
|
|
@@ -152,6 +197,12 @@ function createRuntimeRelayChannel(opts: {
|
|
|
152
197
|
};
|
|
153
198
|
},
|
|
154
199
|
};
|
|
200
|
+
if (opts.onTyping) {
|
|
201
|
+
adapter.typing = async (ctx) => {
|
|
202
|
+
opts.onTyping!(ctx);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return adapter;
|
|
155
206
|
}
|
|
156
207
|
|
|
157
208
|
function rejected(
|
package/src/provision.ts
CHANGED
|
@@ -78,7 +78,10 @@ import {
|
|
|
78
78
|
buildRuntimeSelectionExtraArgs,
|
|
79
79
|
mergeRuntimeExtraArgs,
|
|
80
80
|
} from "./runtime-route-options.js";
|
|
81
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
handleCloudGatewayRuntimeInbound,
|
|
83
|
+
type CloudGatewayTypingEmitter,
|
|
84
|
+
} from "./cloud-gateway-runtime.js";
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
87
|
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
@@ -136,6 +139,13 @@ export interface ProvisionerOptions {
|
|
|
136
139
|
* spin up a default in-memory store.
|
|
137
140
|
*/
|
|
138
141
|
loginSessions?: LoginSessionStore;
|
|
142
|
+
/**
|
|
143
|
+
* Optional callback invoked when an in-flight `cloud_gateway_runtime_inbound`
|
|
144
|
+
* fires a typing event. The cloud daemon wires this to its control-plane
|
|
145
|
+
* connection so the Hub can relay the hint to the live ingress runtime WS.
|
|
146
|
+
* Absent for local daemons (no ingress path).
|
|
147
|
+
*/
|
|
148
|
+
cloudGatewayTypingEmitter?: CloudGatewayTypingEmitter;
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
@@ -153,6 +163,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
153
163
|
const register = opts.register ?? BotCordClient.register;
|
|
154
164
|
const policyResolver = opts.policyResolver;
|
|
155
165
|
const onAgentInstalled = opts.onAgentInstalled;
|
|
166
|
+
const cloudGatewayTypingEmitter = opts.cloudGatewayTypingEmitter;
|
|
156
167
|
const gatewayControl = createGatewayControl({
|
|
157
168
|
gateway,
|
|
158
169
|
...(opts.loginSessions ? { loginSessions: opts.loginSessions } : {}),
|
|
@@ -440,7 +451,12 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
440
451
|
},
|
|
441
452
|
};
|
|
442
453
|
}
|
|
443
|
-
const result = await handleCloudGatewayRuntimeInbound(
|
|
454
|
+
const result = await handleCloudGatewayRuntimeInbound(
|
|
455
|
+
gateway,
|
|
456
|
+
runtimeFrame,
|
|
457
|
+
undefined,
|
|
458
|
+
cloudGatewayTypingEmitter,
|
|
459
|
+
);
|
|
444
460
|
return result.accepted
|
|
445
461
|
? { ok: true, result }
|
|
446
462
|
: {
|