@botcord/daemon 0.2.78 → 0.2.79
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/attention-policy-fetcher.d.ts +14 -0
- package/dist/attention-policy-fetcher.js +59 -0
- package/dist/cloud-daemon.js +8 -0
- package/dist/cloud-gateway-runtime.d.ts +29 -0
- package/dist/cloud-gateway-runtime.js +122 -0
- package/dist/daemon.js +21 -6
- package/dist/gateway/channels/botcord.js +29 -9
- package/dist/gateway/channels/login-session.d.ts +12 -0
- package/dist/gateway/channels/login-session.js +20 -2
- package/dist/gateway/channels/sanitize.d.ts +5 -18
- package/dist/gateway/channels/sanitize.js +5 -54
- package/dist/gateway/channels/text-split.d.ts +5 -11
- package/dist/gateway/channels/text-split.js +5 -31
- package/dist/gateway/dispatcher.d.ts +7 -1
- package/dist/gateway/dispatcher.js +88 -8
- package/dist/gateway/gateway.d.ts +16 -1
- package/dist/gateway/gateway.js +21 -0
- package/dist/gateway/policy-resolver.js +17 -9
- package/dist/gateway/runtimes/deepseek-tui.js +31 -12
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/provision.d.ts +7 -3
- package/dist/provision.js +115 -8
- package/dist/room-recovery-context.d.ts +11 -0
- package/dist/room-recovery-context.js +97 -0
- package/package.json +2 -2
- package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
- package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
- package/src/__tests__/gateway-control.test.ts +136 -0
- package/src/__tests__/policy-resolver.test.ts +20 -0
- package/src/__tests__/provision.test.ts +65 -0
- package/src/attention-policy-fetcher.ts +87 -0
- package/src/cloud-daemon.ts +8 -0
- package/src/cloud-gateway-runtime.ts +171 -0
- package/src/daemon.ts +23 -6
- package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +177 -0
- package/src/gateway/__tests__/dispatcher.test.ts +56 -0
- package/src/gateway/channels/botcord.ts +32 -8
- package/src/gateway/channels/login-session.ts +20 -2
- package/src/gateway/channels/sanitize.ts +8 -66
- package/src/gateway/channels/text-split.ts +5 -27
- package/src/gateway/dispatcher.ts +123 -27
- package/src/gateway/gateway.ts +29 -0
- package/src/gateway/policy-resolver.ts +20 -9
- package/src/gateway/runtimes/deepseek-tui.ts +37 -12
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/provision.ts +133 -7
- package/src/room-recovery-context.ts +131 -0
|
@@ -1736,6 +1736,71 @@ describe("update_agent handler", () => {
|
|
|
1736
1736
|
});
|
|
1737
1737
|
});
|
|
1738
1738
|
|
|
1739
|
+
it("updates runtime selectors in credentials and managed route", async () => {
|
|
1740
|
+
await withSandboxHome(async ({ tmp, fs, path: nodePath }) => {
|
|
1741
|
+
mockState.cfg = {
|
|
1742
|
+
defaultRoute: {
|
|
1743
|
+
adapter: "codex",
|
|
1744
|
+
cwd: tmp,
|
|
1745
|
+
extraArgs: ["--skip-git-repo-check"],
|
|
1746
|
+
},
|
|
1747
|
+
routes: [],
|
|
1748
|
+
streamBlocks: true,
|
|
1749
|
+
};
|
|
1750
|
+
const credDir = nodePath.join(tmp, ".botcord", "credentials");
|
|
1751
|
+
fs.mkdirSync(credDir, { recursive: true });
|
|
1752
|
+
fs.writeFileSync(
|
|
1753
|
+
nodePath.join(credDir, "ag_runtime.json"),
|
|
1754
|
+
JSON.stringify({
|
|
1755
|
+
version: 1,
|
|
1756
|
+
hubUrl: "https://hub.example",
|
|
1757
|
+
agentId: "ag_runtime",
|
|
1758
|
+
keyId: "k_runtime",
|
|
1759
|
+
privateKey: Buffer.alloc(32, 24).toString("base64"),
|
|
1760
|
+
savedAt: new Date().toISOString(),
|
|
1761
|
+
}),
|
|
1762
|
+
);
|
|
1763
|
+
|
|
1764
|
+
const gw = makeFakeGateway();
|
|
1765
|
+
const provisioner = createProvisioner({
|
|
1766
|
+
gateway: gw as unknown as Parameters<typeof createProvisioner>[0]["gateway"],
|
|
1767
|
+
});
|
|
1768
|
+
const ack = await provisioner({
|
|
1769
|
+
id: "req_update_runtime",
|
|
1770
|
+
type: CONTROL_FRAME_TYPES.UPDATE_AGENT,
|
|
1771
|
+
params: {
|
|
1772
|
+
agentId: "ag_runtime",
|
|
1773
|
+
runtime: "codex",
|
|
1774
|
+
runtimeModel: "gpt-5.2",
|
|
1775
|
+
reasoningEffort: "high",
|
|
1776
|
+
thinking: true,
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
expect(ack.ok).toBe(true);
|
|
1781
|
+
expect((ack.result as { changed: boolean }).changed).toBe(true);
|
|
1782
|
+
|
|
1783
|
+
const saved = JSON.parse(
|
|
1784
|
+
fs.readFileSync(nodePath.join(credDir, "ag_runtime.json"), "utf8"),
|
|
1785
|
+
) as Record<string, unknown>;
|
|
1786
|
+
expect(saved.runtime).toBe("codex");
|
|
1787
|
+
expect(saved.runtimeModel).toBe("gpt-5.2");
|
|
1788
|
+
expect(saved.reasoningEffort).toBe("high");
|
|
1789
|
+
expect(saved.thinking).toBe(true);
|
|
1790
|
+
expect(gw.listManagedRoutes()[0]).toMatchObject({
|
|
1791
|
+
match: { accountId: "ag_runtime" },
|
|
1792
|
+
runtime: "codex",
|
|
1793
|
+
extraArgs: [
|
|
1794
|
+
"--skip-git-repo-check",
|
|
1795
|
+
"--model",
|
|
1796
|
+
"gpt-5.2",
|
|
1797
|
+
"-c",
|
|
1798
|
+
'model_reasoning_effort="high"',
|
|
1799
|
+
],
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1739
1804
|
it("rejects update_agent without agentId", async () => {
|
|
1740
1805
|
const gw = makeFakeGateway();
|
|
1741
1806
|
const provisioner = createProvisioner({
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BotCordClient,
|
|
3
|
+
defaultCredentialsFile,
|
|
4
|
+
loadStoredCredentials,
|
|
5
|
+
updateCredentialsToken,
|
|
6
|
+
} from "@botcord/protocol-core";
|
|
7
|
+
import type { DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
|
|
8
|
+
|
|
9
|
+
interface CachedClient {
|
|
10
|
+
client: BotCordClient;
|
|
11
|
+
credentialsPath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AttentionPolicyFetcherOptions {
|
|
15
|
+
credentialPathByAgentId: Map<string, string>;
|
|
16
|
+
defaultCredentialsPath?: string;
|
|
17
|
+
hubBaseUrl?: string;
|
|
18
|
+
log?: {
|
|
19
|
+
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type AttentionPolicyFetcher = (args: {
|
|
24
|
+
agentId: string;
|
|
25
|
+
roomId?: string | null;
|
|
26
|
+
}) => Promise<DaemonAttentionPolicy | undefined>;
|
|
27
|
+
|
|
28
|
+
export function createAttentionPolicyFetcher(
|
|
29
|
+
opts: AttentionPolicyFetcherOptions,
|
|
30
|
+
): AttentionPolicyFetcher {
|
|
31
|
+
const clients = new Map<string, CachedClient>();
|
|
32
|
+
|
|
33
|
+
function getClient(agentId: string): BotCordClient | null {
|
|
34
|
+
const existing = clients.get(agentId);
|
|
35
|
+
if (existing) return existing.client;
|
|
36
|
+
|
|
37
|
+
const credentialsPath =
|
|
38
|
+
opts.credentialPathByAgentId.get(agentId) ??
|
|
39
|
+
opts.defaultCredentialsPath ??
|
|
40
|
+
defaultCredentialsFile(agentId);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const creds = loadStoredCredentials(credentialsPath);
|
|
44
|
+
const client = new BotCordClient({
|
|
45
|
+
hubUrl: opts.hubBaseUrl ?? creds.hubUrl,
|
|
46
|
+
agentId: creds.agentId,
|
|
47
|
+
keyId: creds.keyId,
|
|
48
|
+
privateKey: creds.privateKey,
|
|
49
|
+
...(creds.token ? { token: creds.token } : {}),
|
|
50
|
+
...(creds.tokenExpiresAt !== undefined
|
|
51
|
+
? { tokenExpiresAt: creds.tokenExpiresAt }
|
|
52
|
+
: {}),
|
|
53
|
+
});
|
|
54
|
+
client.onTokenRefresh = (token, expiresAt) => {
|
|
55
|
+
try {
|
|
56
|
+
updateCredentialsToken(credentialsPath, token, expiresAt);
|
|
57
|
+
} catch {
|
|
58
|
+
// Persistence failures are non-fatal; the next refresh retries.
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
clients.set(agentId, { client, credentialsPath });
|
|
62
|
+
return client;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
opts.log?.warn("daemon.attention-policy.client-init-failed", {
|
|
65
|
+
agentId,
|
|
66
|
+
credentialsPath,
|
|
67
|
+
error: err instanceof Error ? err.message : String(err),
|
|
68
|
+
});
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return async ({ agentId, roomId }) => {
|
|
74
|
+
const client = getClient(agentId);
|
|
75
|
+
if (!client) return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return await client.getAttentionPolicy({ roomId });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
opts.log?.warn("daemon.attention-policy.fetch-failed", {
|
|
80
|
+
agentId,
|
|
81
|
+
roomId: roomId ?? null,
|
|
82
|
+
error: err instanceof Error ? err.message : String(err),
|
|
83
|
+
});
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/cloud-daemon.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
|
32
32
|
import { readWorkingMemorySnapshot } from "./working-memory.js";
|
|
33
33
|
import { createRoomStaticContextBuilder } from "./room-context.js";
|
|
34
34
|
import { createRoomContextFetcher } from "./room-context-fetcher.js";
|
|
35
|
+
import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
|
|
35
36
|
import { composeBotCordUserTurn } from "./turn-text.js";
|
|
36
37
|
import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
|
|
37
38
|
import { scanMention } from "./mention-scan.js";
|
|
@@ -143,6 +144,12 @@ export async function startCloudDaemon(
|
|
|
143
144
|
fetchRoomInfo: roomContextFetcher,
|
|
144
145
|
log: logger,
|
|
145
146
|
});
|
|
147
|
+
const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
|
|
148
|
+
credentialPathByAgentId,
|
|
149
|
+
hubBaseUrl: cloudCfg.hubUrl,
|
|
150
|
+
limit: 20,
|
|
151
|
+
log: logger,
|
|
152
|
+
});
|
|
146
153
|
|
|
147
154
|
type PerAgentBuilder = (
|
|
148
155
|
msg: GatewayInboundMessage,
|
|
@@ -258,6 +265,7 @@ export async function startCloudDaemon(
|
|
|
258
265
|
turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
|
|
259
266
|
buildSystemContext,
|
|
260
267
|
buildMemoryContext,
|
|
268
|
+
buildRuntimeRecoveryContext,
|
|
261
269
|
onInbound,
|
|
262
270
|
onTurnComplete,
|
|
263
271
|
composeUserTurn: composeBotCordUserTurn,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RUNTIME_FRAME_TYPES,
|
|
3
|
+
type GatewayInboundFrame,
|
|
4
|
+
} from "@botcord/protocol-core";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ChannelAdapter,
|
|
8
|
+
ChannelSendContext,
|
|
9
|
+
ChannelSendResult,
|
|
10
|
+
ChannelStatusSnapshot,
|
|
11
|
+
Gateway,
|
|
12
|
+
GatewayInboundMessage,
|
|
13
|
+
GatewayLogger,
|
|
14
|
+
} from "./gateway/index.js";
|
|
15
|
+
|
|
16
|
+
export interface CloudGatewayRuntimeResult {
|
|
17
|
+
accepted: boolean;
|
|
18
|
+
eventId: string;
|
|
19
|
+
gatewayId: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
conversationId: string;
|
|
22
|
+
turnId: string;
|
|
23
|
+
outbound?: {
|
|
24
|
+
finalText: string;
|
|
25
|
+
providerMessageId?: string | null;
|
|
26
|
+
};
|
|
27
|
+
error?: {
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute one ingress-originated runtime frame through the normal Gateway
|
|
35
|
+
* dispatcher while keeping provider I/O outside the cloud sandbox.
|
|
36
|
+
*
|
|
37
|
+
* The temporary channel is scoped to this call and implements only the
|
|
38
|
+
* dispatcher-facing send/status surface. Its send() method captures the
|
|
39
|
+
* final runtime reply; the Hub relay converts that into
|
|
40
|
+
* gateway_outbound_complete for gateway-ingress, which then calls the real
|
|
41
|
+
* provider API.
|
|
42
|
+
*/
|
|
43
|
+
export async function handleCloudGatewayRuntimeInbound(
|
|
44
|
+
gateway: Gateway,
|
|
45
|
+
frame: GatewayInboundFrame,
|
|
46
|
+
log?: GatewayLogger,
|
|
47
|
+
): Promise<CloudGatewayRuntimeResult> {
|
|
48
|
+
if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
|
|
49
|
+
return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
|
|
50
|
+
}
|
|
51
|
+
if (!frame.gateway_id || !frame.agent_id || !frame.event_id) {
|
|
52
|
+
return rejected(frame, "bad_frame", "gateway_id, agent_id and event_id are required");
|
|
53
|
+
}
|
|
54
|
+
if (frame.message.accountId !== frame.agent_id) {
|
|
55
|
+
return rejected(frame, "account_mismatch", "message.accountId does not match frame.agent_id");
|
|
56
|
+
}
|
|
57
|
+
if (frame.message.channel !== frame.gateway_id) {
|
|
58
|
+
return rejected(frame, "channel_mismatch", "message.channel does not match frame.gateway_id");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let accepted = false;
|
|
62
|
+
let outboundText: string | null = null;
|
|
63
|
+
let providerMessageId: string | null | undefined;
|
|
64
|
+
const channel = createRuntimeRelayChannel({
|
|
65
|
+
id: frame.gateway_id,
|
|
66
|
+
provider: frame.provider,
|
|
67
|
+
accountId: frame.agent_id,
|
|
68
|
+
onSend: async (ctx) => {
|
|
69
|
+
outboundText = ctx.message.text ?? "";
|
|
70
|
+
providerMessageId = ctx.message.traceId ?? null;
|
|
71
|
+
return { providerMessageId };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const message: GatewayInboundMessage = {
|
|
76
|
+
...frame.message,
|
|
77
|
+
raw: {
|
|
78
|
+
source_type: "cloud_gateway_ingress",
|
|
79
|
+
provider: frame.provider,
|
|
80
|
+
event_id: frame.event_id,
|
|
81
|
+
gateway_id: frame.gateway_id,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await gateway.injectInboundThrough(message, channel, {
|
|
87
|
+
accept: async () => {
|
|
88
|
+
accepted = true;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
log?.warn("cloud gateway runtime dispatch failed", {
|
|
94
|
+
eventId: frame.event_id,
|
|
95
|
+
gatewayId: frame.gateway_id,
|
|
96
|
+
agentId: frame.agent_id,
|
|
97
|
+
error: message,
|
|
98
|
+
});
|
|
99
|
+
return rejected(frame, "dispatch_failed", message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
accepted,
|
|
104
|
+
eventId: frame.event_id,
|
|
105
|
+
gatewayId: frame.gateway_id,
|
|
106
|
+
agentId: frame.agent_id,
|
|
107
|
+
conversationId: frame.message.conversation.id,
|
|
108
|
+
turnId: `turn_${frame.event_id}`,
|
|
109
|
+
...(outboundText !== null
|
|
110
|
+
? {
|
|
111
|
+
outbound: {
|
|
112
|
+
finalText: outboundText,
|
|
113
|
+
providerMessageId: providerMessageId ?? null,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(!accepted
|
|
118
|
+
? { error: { code: "not_accepted", message: "dispatcher did not accept inbound" } }
|
|
119
|
+
: {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createRuntimeRelayChannel(opts: {
|
|
124
|
+
id: string;
|
|
125
|
+
provider: string;
|
|
126
|
+
accountId: string;
|
|
127
|
+
onSend: (ctx: ChannelSendContext) => Promise<ChannelSendResult>;
|
|
128
|
+
}): ChannelAdapter {
|
|
129
|
+
let lastSendAt: number | undefined;
|
|
130
|
+
return {
|
|
131
|
+
id: opts.id,
|
|
132
|
+
type: opts.provider,
|
|
133
|
+
async start() {
|
|
134
|
+
return undefined;
|
|
135
|
+
},
|
|
136
|
+
async stop() {
|
|
137
|
+
return undefined;
|
|
138
|
+
},
|
|
139
|
+
async send(ctx) {
|
|
140
|
+
lastSendAt = Date.now();
|
|
141
|
+
return opts.onSend(ctx);
|
|
142
|
+
},
|
|
143
|
+
status(): ChannelStatusSnapshot {
|
|
144
|
+
return {
|
|
145
|
+
channel: opts.id,
|
|
146
|
+
accountId: opts.accountId,
|
|
147
|
+
running: true,
|
|
148
|
+
connected: true,
|
|
149
|
+
authorized: true,
|
|
150
|
+
provider: opts.provider as ChannelStatusSnapshot["provider"],
|
|
151
|
+
...(lastSendAt ? { lastSendAt } : {}),
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rejected(
|
|
158
|
+
frame: Partial<GatewayInboundFrame>,
|
|
159
|
+
code: string,
|
|
160
|
+
message: string,
|
|
161
|
+
): CloudGatewayRuntimeResult {
|
|
162
|
+
return {
|
|
163
|
+
accepted: false,
|
|
164
|
+
eventId: frame.event_id ?? "",
|
|
165
|
+
gatewayId: frame.gateway_id ?? "",
|
|
166
|
+
agentId: frame.agent_id ?? "",
|
|
167
|
+
conversationId: frame.message?.conversation.id ?? "",
|
|
168
|
+
turnId: frame.event_id ? `turn_${frame.event_id}` : "turn_unknown",
|
|
169
|
+
error: { code, message },
|
|
170
|
+
};
|
|
171
|
+
}
|
package/src/daemon.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
|
39
39
|
import { readWorkingMemorySnapshot } from "./working-memory.js";
|
|
40
40
|
import { createRoomStaticContextBuilder } from "./room-context.js";
|
|
41
41
|
import { createRoomContextFetcher } from "./room-context-fetcher.js";
|
|
42
|
+
import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
|
|
42
43
|
import {
|
|
43
44
|
buildLoopRiskPrompt,
|
|
44
45
|
loopRiskSessionKey,
|
|
@@ -50,6 +51,7 @@ import { UserAuthManager } from "./user-auth.js";
|
|
|
50
51
|
import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
|
|
51
52
|
import { scanMention } from "./mention-scan.js";
|
|
52
53
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
54
|
+
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -364,6 +366,13 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
364
366
|
fetchRoomInfo: roomContextFetcher,
|
|
365
367
|
log: logger,
|
|
366
368
|
});
|
|
369
|
+
const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
|
|
370
|
+
credentialPathByAgentId,
|
|
371
|
+
...(opts.credentialsPath ? { defaultCredentialsPath: opts.credentialsPath } : {}),
|
|
372
|
+
...(opts.hubBaseUrl ? { hubBaseUrl: opts.hubBaseUrl } : {}),
|
|
373
|
+
limit: 20,
|
|
374
|
+
log: logger,
|
|
375
|
+
});
|
|
367
376
|
|
|
368
377
|
// Cache one system-context builder per configured agentId. The gateway
|
|
369
378
|
// calls this with each inbound message and we pick the right builder by
|
|
@@ -442,13 +451,20 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
442
451
|
});
|
|
443
452
|
};
|
|
444
453
|
|
|
445
|
-
// Per-agent attention policy cache (
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// Per-agent attention policy cache (design §4.2 / §5). It is seeded from
|
|
455
|
+
// `provision_agent` / `policy_updated` frames when available and falls back
|
|
456
|
+
// to Hub on cold misses, so daemon restarts preserve dashboard policy.
|
|
457
|
+
const fetchAttentionPolicy = createAttentionPolicyFetcher({
|
|
458
|
+
credentialPathByAgentId,
|
|
459
|
+
defaultCredentialsPath: opts.credentialsPath,
|
|
460
|
+
hubBaseUrl: opts.hubBaseUrl,
|
|
461
|
+
log: logger,
|
|
462
|
+
});
|
|
450
463
|
const policyResolver = new PolicyResolver({
|
|
451
|
-
fetchGlobal: async (
|
|
464
|
+
fetchGlobal: async (agentId: string) =>
|
|
465
|
+
fetchAttentionPolicy({ agentId, roomId: null }),
|
|
466
|
+
fetchEffective: async (agentId: string, roomId: string) =>
|
|
467
|
+
fetchAttentionPolicy({ agentId, roomId }),
|
|
452
468
|
});
|
|
453
469
|
|
|
454
470
|
// Display-name lookup for the mention text-fallback. Populated from boot
|
|
@@ -528,6 +544,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
528
544
|
turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
|
|
529
545
|
buildSystemContext,
|
|
530
546
|
buildMemoryContext,
|
|
547
|
+
buildRuntimeRecoveryContext,
|
|
531
548
|
onInbound,
|
|
532
549
|
onOutbound,
|
|
533
550
|
onRuntimeCircuitBreakerChange: pushLiveRuntimeSnapshot,
|
|
@@ -925,6 +925,103 @@ describe("createBotCordChannel — streamBlock()", () => {
|
|
|
925
925
|
}
|
|
926
926
|
});
|
|
927
927
|
|
|
928
|
+
it("normalizes current DeepSeek item.delta assistant text", async () => {
|
|
929
|
+
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
930
|
+
const realFetch = globalThis.fetch;
|
|
931
|
+
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
932
|
+
try {
|
|
933
|
+
const client = makeClient({
|
|
934
|
+
getHubUrl: vi.fn().mockReturnValue("https://hub.example.com"),
|
|
935
|
+
});
|
|
936
|
+
const channel = createBotCordChannel({
|
|
937
|
+
id: "botcord-main",
|
|
938
|
+
accountId: "ag_self",
|
|
939
|
+
agentId: "ag_self",
|
|
940
|
+
client,
|
|
941
|
+
hubBaseUrl: "https://hub.example.com",
|
|
942
|
+
});
|
|
943
|
+
await channel.streamBlock!({
|
|
944
|
+
traceId: "m_trace",
|
|
945
|
+
accountId: "ag_self",
|
|
946
|
+
conversationId: "rm_oc_1",
|
|
947
|
+
block: {
|
|
948
|
+
kind: "assistant_text",
|
|
949
|
+
seq: 6,
|
|
950
|
+
raw: {
|
|
951
|
+
event: "item.delta",
|
|
952
|
+
payload: { thread_id: "thr_1", turn_id: "turn_1", kind: "agent_message", delta: "hello" },
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
log: silentLog,
|
|
956
|
+
});
|
|
957
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
958
|
+
const body = JSON.parse(init.body as string);
|
|
959
|
+
expect(body.block).toEqual({
|
|
960
|
+
kind: "assistant",
|
|
961
|
+
seq: 6,
|
|
962
|
+
payload: { text: "hello" },
|
|
963
|
+
});
|
|
964
|
+
} finally {
|
|
965
|
+
globalThis.fetch = realFetch;
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("normalizes current DeepSeek item.started tool input", () => {
|
|
970
|
+
expect(
|
|
971
|
+
__normalizeBlockForHubForTests(
|
|
972
|
+
{
|
|
973
|
+
kind: "tool_use",
|
|
974
|
+
seq: 7,
|
|
975
|
+
raw: {
|
|
976
|
+
event: "item.started",
|
|
977
|
+
payload: {
|
|
978
|
+
item: { id: "item_tool", kind: "tool_call", status: "in_progress" },
|
|
979
|
+
tool: { id: "call_1", name: "web_search", input: { query: "上海天气" } },
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
7,
|
|
984
|
+
),
|
|
985
|
+
).toEqual({
|
|
986
|
+
kind: "tool_call",
|
|
987
|
+
seq: 7,
|
|
988
|
+
payload: {
|
|
989
|
+
id: "call_1",
|
|
990
|
+
name: "web_search",
|
|
991
|
+
params: { query: "上海天气" },
|
|
992
|
+
status: "in_progress",
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it("normalizes current DeepSeek agent_reasoning details", () => {
|
|
998
|
+
expect(
|
|
999
|
+
__normalizeBlockForHubForTests(
|
|
1000
|
+
{
|
|
1001
|
+
kind: "thinking",
|
|
1002
|
+
seq: 8,
|
|
1003
|
+
raw: {
|
|
1004
|
+
event: "item.completed",
|
|
1005
|
+
payload: {
|
|
1006
|
+
item: {
|
|
1007
|
+
id: "item_reasoning",
|
|
1008
|
+
kind: "agent_reasoning",
|
|
1009
|
+
status: "completed",
|
|
1010
|
+
summary: "I should answer briefly.",
|
|
1011
|
+
detail: "I should answer briefly.",
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
8,
|
|
1017
|
+
),
|
|
1018
|
+
).toEqual({
|
|
1019
|
+
kind: "thinking",
|
|
1020
|
+
seq: 8,
|
|
1021
|
+
payload: { details: "I should answer briefly." },
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
928
1025
|
it("marks DeepSeek terminal events for owner-chat stream cleanup", async () => {
|
|
929
1026
|
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
930
1027
|
const realFetch = globalThis.fetch;
|