@botcord/daemon 0.2.75 → 0.2.77
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-auth.d.ts +47 -0
- package/dist/cloud-auth.js +51 -0
- package/dist/cloud-daemon.d.ts +43 -0
- package/dist/cloud-daemon.js +252 -0
- package/dist/cloud-mode.d.ts +45 -0
- package/dist/cloud-mode.js +55 -0
- package/dist/cloud-settle.d.ts +81 -0
- package/dist/cloud-settle.js +100 -0
- package/dist/daemon-singleton.d.ts +26 -0
- package/dist/daemon-singleton.js +91 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +15 -6
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +15 -4
- package/dist/gateway/channels/botcord.d.ts +1 -1
- package/dist/gateway/channels/botcord.js +280 -52
- package/dist/gateway/dispatcher.d.ts +34 -1
- package/dist/gateway/dispatcher.js +277 -20
- package/dist/gateway/gateway.d.ts +9 -1
- package/dist/gateway/gateway.js +4 -1
- package/dist/gateway/runtime-errors.d.ts +6 -0
- package/dist/gateway/runtime-errors.js +14 -0
- package/dist/gateway/runtimes/claude-code.d.ts +8 -0
- package/dist/gateway/runtimes/claude-code.js +92 -4
- package/dist/gateway/runtimes/deepseek-tui.js +19 -5
- package/dist/gateway/transcript.d.ts +1 -1
- package/dist/gateway/types.d.ts +33 -0
- package/dist/index.js +71 -80
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +39 -1
- package/dist/status-render.js +17 -0
- package/package.json +2 -2
- package/src/__tests__/cloud-auth.test.ts +42 -0
- package/src/__tests__/cloud-daemon.test.ts +237 -0
- package/src/__tests__/cloud-mode.test.ts +65 -0
- package/src/__tests__/cloud-settle.test.ts +287 -0
- package/src/__tests__/daemon-singleton.test.ts +89 -0
- package/src/__tests__/doctor.test.ts +34 -0
- package/src/__tests__/runtime-discovery.test.ts +90 -0
- package/src/__tests__/status-render.test.ts +34 -0
- package/src/cloud-auth.ts +78 -0
- package/src/cloud-daemon.ts +338 -0
- package/src/cloud-mode.ts +70 -0
- package/src/cloud-settle.ts +182 -0
- package/src/daemon-singleton.ts +122 -0
- package/src/daemon.ts +18 -5
- package/src/doctor.ts +18 -5
- package/src/gateway/__tests__/botcord-channel.test.ts +98 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +101 -1
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +19 -0
- package/src/gateway/__tests__/dispatcher.test.ts +120 -0
- package/src/gateway/channels/botcord.ts +299 -43
- package/src/gateway/dispatcher.ts +354 -21
- package/src/gateway/gateway.ts +16 -1
- package/src/gateway/runtime-errors.ts +15 -0
- package/src/gateway/runtimes/claude-code.ts +98 -2
- package/src/gateway/runtimes/deepseek-tui.ts +23 -5
- package/src/gateway/transcript.ts +1 -1
- package/src/gateway/types.ts +34 -0
- package/src/index.ts +83 -74
- package/src/provision.ts +45 -1
- package/src/status-render.ts +24 -0
|
@@ -57,7 +57,12 @@ function defaultClientFactory(input) {
|
|
|
57
57
|
function isOwnerTrust(msg) {
|
|
58
58
|
if (msg.room_id?.startsWith(OWNER_CHAT_PREFIX))
|
|
59
59
|
return true;
|
|
60
|
-
|
|
60
|
+
const sourceType = msg.source_type;
|
|
61
|
+
if (sourceType === "dashboard_user_chat")
|
|
62
|
+
return true;
|
|
63
|
+
// Cloud Agent run tasks are Hub-issued on the user's behalf, same
|
|
64
|
+
// trust posture as owner chat.
|
|
65
|
+
if (sourceType === "cloud_agent_run")
|
|
61
66
|
return true;
|
|
62
67
|
return false;
|
|
63
68
|
}
|
|
@@ -89,11 +94,17 @@ function normalizeInbox(msg, options) {
|
|
|
89
94
|
return null;
|
|
90
95
|
// `message` is the normal conversational envelope; `contact_request` is
|
|
91
96
|
// a lightweight inbound asking the agent to notify its owner (the
|
|
92
|
-
// composer appends the notify-owner hint)
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
+
// composer appends the notify-owner hint); `cloud_run` carries a
|
|
98
|
+
// Cloud Agent run task with embedded run_id + budget (the cloud
|
|
99
|
+
// daemon's runtime adapter reads them from `raw.envelope.payload.cloud_run`
|
|
100
|
+
// and reports usage back via /internal/cloud-agents/.../settle when the
|
|
101
|
+
// run completes). All other envelope types (notification, system,
|
|
102
|
+
// contact_added/removed, …) are still filtered out — they belong in
|
|
103
|
+
// a separate push-notification path that daemon does not yet implement.
|
|
104
|
+
const envType = env.type;
|
|
105
|
+
if (envType !== "message" &&
|
|
106
|
+
envType !== "contact_request" &&
|
|
107
|
+
envType !== "cloud_run")
|
|
97
108
|
return null;
|
|
98
109
|
if (!msg.room_id)
|
|
99
110
|
return null;
|
|
@@ -104,7 +115,8 @@ function normalizeInbox(msg, options) {
|
|
|
104
115
|
const text = ownerTrust ? rawText : sanitizeUntrustedContent(rawText);
|
|
105
116
|
const isDm = msg.room_id.startsWith(DM_ROOM_PREFIX);
|
|
106
117
|
const isOwnerChat = msg.room_id.startsWith(OWNER_CHAT_PREFIX);
|
|
107
|
-
const
|
|
118
|
+
const sourceType = msg.source_type;
|
|
119
|
+
const senderKind = ownerTrust || sourceType === "dashboard_human_room" ? "user" : "agent";
|
|
108
120
|
const senderName = msg.source_user_name ?? undefined;
|
|
109
121
|
const threadId = msg.topic_id ?? msg.topic ?? null;
|
|
110
122
|
const streamable = isOwnerChat;
|
|
@@ -348,6 +360,7 @@ export function createBotCordChannel(options) {
|
|
|
348
360
|
let ws = null;
|
|
349
361
|
let reconnectTimer = null;
|
|
350
362
|
let keepaliveTimer = null;
|
|
363
|
+
let pollTimer = null;
|
|
351
364
|
let reconnectAttempt = 0;
|
|
352
365
|
let connectionSeq = 0;
|
|
353
366
|
let consecutiveAuthFailures = 0;
|
|
@@ -371,6 +384,10 @@ export function createBotCordChannel(options) {
|
|
|
371
384
|
clearInterval(keepaliveTimer);
|
|
372
385
|
keepaliveTimer = null;
|
|
373
386
|
}
|
|
387
|
+
if (pollTimer) {
|
|
388
|
+
clearInterval(pollTimer);
|
|
389
|
+
pollTimer = null;
|
|
390
|
+
}
|
|
374
391
|
}
|
|
375
392
|
function markStatus(patch) {
|
|
376
393
|
statusSnapshot = { ...statusSnapshot, ...patch };
|
|
@@ -574,6 +591,17 @@ export function createBotCordChannel(options) {
|
|
|
574
591
|
});
|
|
575
592
|
log.info("botcord ws authenticated", { agentId: msg.agent_id });
|
|
576
593
|
void fireInbox("ws_auth_ok");
|
|
594
|
+
const pollIntervalMs = options.pollIntervalMs ?? 30_000;
|
|
595
|
+
if (pollTimer)
|
|
596
|
+
clearInterval(pollTimer);
|
|
597
|
+
if (pollIntervalMs > 0) {
|
|
598
|
+
pollTimer = setInterval(() => {
|
|
599
|
+
if (ws === socket && socket.readyState === WebSocket.OPEN) {
|
|
600
|
+
void fireInbox("poll_interval");
|
|
601
|
+
}
|
|
602
|
+
}, pollIntervalMs);
|
|
603
|
+
pollTimer.unref?.();
|
|
604
|
+
}
|
|
577
605
|
if (keepaliveTimer)
|
|
578
606
|
clearInterval(keepaliveTimer);
|
|
579
607
|
keepaliveTimer = setInterval(() => {
|
|
@@ -835,55 +863,29 @@ function normalizeBlockForHub(block, seq) {
|
|
|
835
863
|
return { kind: "assistant", seq, payload: { text } };
|
|
836
864
|
}
|
|
837
865
|
if (kind === "tool_use") {
|
|
838
|
-
// Claude Code
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
if (
|
|
843
|
-
payload.name =
|
|
844
|
-
if (
|
|
845
|
-
payload.params =
|
|
846
|
-
if (
|
|
847
|
-
payload.id =
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
851
|
-
const params = codexToolParams(raw.item);
|
|
852
|
-
if (Object.keys(params).length > 0)
|
|
853
|
-
payload.params = params;
|
|
854
|
-
if (typeof raw.item.id === "string")
|
|
855
|
-
payload.id = raw.item.id;
|
|
856
|
-
if (typeof raw.item.status === "string")
|
|
857
|
-
payload.status = raw.item.status;
|
|
866
|
+
// Claude Code, Codex, DeepSeek TUI, Kimi, and ACP all expose tool calls
|
|
867
|
+
// with slightly different field names. Preserve the real invocation input
|
|
868
|
+
// so the dashboard can show more than a bare "tool" label.
|
|
869
|
+
const call = extractToolCall(raw);
|
|
870
|
+
if (call) {
|
|
871
|
+
payload.name = call.name;
|
|
872
|
+
if (call.params !== undefined && !isEmptyRecord(call.params))
|
|
873
|
+
payload.params = call.params;
|
|
874
|
+
if (call.id)
|
|
875
|
+
payload.id = call.id;
|
|
876
|
+
if (call.status)
|
|
877
|
+
payload.status = call.status;
|
|
858
878
|
}
|
|
859
879
|
return { kind: "tool_call", seq, payload };
|
|
860
880
|
}
|
|
861
881
|
if (kind === "tool_result") {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
resultStr = tr.content;
|
|
870
|
-
}
|
|
871
|
-
else if (Array.isArray(tr.content)) {
|
|
872
|
-
resultStr = tr.content
|
|
873
|
-
.map((c) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
|
|
874
|
-
.join("\n");
|
|
875
|
-
}
|
|
876
|
-
payload.result = resultStr;
|
|
877
|
-
if (typeof tr.tool_use_id === "string")
|
|
878
|
-
payload.tool_use_id = tr.tool_use_id;
|
|
879
|
-
}
|
|
880
|
-
else if (raw?.item && typeof raw.item === "object") {
|
|
881
|
-
payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
|
|
882
|
-
if (typeof raw.item.id === "string")
|
|
883
|
-
payload.tool_use_id = raw.item.id;
|
|
884
|
-
const result = codexToolResult(raw.item);
|
|
885
|
-
if (result)
|
|
886
|
-
payload.result = result;
|
|
882
|
+
const result = extractToolResult(raw);
|
|
883
|
+
if (result) {
|
|
884
|
+
if (result.name)
|
|
885
|
+
payload.name = result.name;
|
|
886
|
+
payload.result = result.result;
|
|
887
|
+
if (result.id)
|
|
888
|
+
payload.tool_use_id = result.id;
|
|
887
889
|
}
|
|
888
890
|
return { kind: "tool_result", seq, payload };
|
|
889
891
|
}
|
|
@@ -912,6 +914,15 @@ function normalizeBlockForHub(block, seq) {
|
|
|
912
914
|
return { kind: "thinking", seq, payload };
|
|
913
915
|
}
|
|
914
916
|
// "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
|
|
917
|
+
if (isTerminalRuntimeBlock(raw)) {
|
|
918
|
+
payload.terminal = true;
|
|
919
|
+
payload.details = formatBlockDetails(raw);
|
|
920
|
+
const event = typeof raw?.event === "string" ? raw.event : undefined;
|
|
921
|
+
const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
|
|
922
|
+
if (event || embedded)
|
|
923
|
+
payload.event = event ?? embedded;
|
|
924
|
+
return { kind: "other", seq, payload };
|
|
925
|
+
}
|
|
915
926
|
if (raw?.type === "result") {
|
|
916
927
|
if (typeof raw.result === "string")
|
|
917
928
|
payload.text = raw.result;
|
|
@@ -922,6 +933,177 @@ function normalizeBlockForHub(block, seq) {
|
|
|
922
933
|
}
|
|
923
934
|
return { kind: "other", seq, payload };
|
|
924
935
|
}
|
|
936
|
+
function isTerminalRuntimeBlock(raw) {
|
|
937
|
+
const event = typeof raw?.event === "string" ? raw.event : undefined;
|
|
938
|
+
const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
|
|
939
|
+
const terminal = event ?? embedded;
|
|
940
|
+
return (terminal === "turn.completed" ||
|
|
941
|
+
terminal === "turn.finished" ||
|
|
942
|
+
terminal === "turn.done" ||
|
|
943
|
+
terminal === "done");
|
|
944
|
+
}
|
|
945
|
+
function extractToolCall(raw) {
|
|
946
|
+
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
947
|
+
const tu = contents.find((c) => c?.type === "tool_use");
|
|
948
|
+
if (tu) {
|
|
949
|
+
return {
|
|
950
|
+
name: stringField(tu, "name") ?? "tool",
|
|
951
|
+
params: parseMaybeJson(tu.input ?? tu.arguments),
|
|
952
|
+
id: stringField(tu, "id"),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const deepseek = extractDeepseekToolCall(raw);
|
|
956
|
+
if (deepseek)
|
|
957
|
+
return deepseek;
|
|
958
|
+
const item = raw?.item;
|
|
959
|
+
if (item && typeof item === "object") {
|
|
960
|
+
const params = codexToolParams(item);
|
|
961
|
+
return {
|
|
962
|
+
name: stringField(item, "type") ?? stringField(item, "name") ?? "tool",
|
|
963
|
+
params,
|
|
964
|
+
id: stringField(item, "id"),
|
|
965
|
+
status: stringField(item, "status"),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const toolCalls = Array.isArray(raw?.tool_calls) ? raw.tool_calls : [];
|
|
969
|
+
const toolCall = toolCalls.find((t) => t && typeof t === "object");
|
|
970
|
+
if (toolCall) {
|
|
971
|
+
const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
|
|
972
|
+
return {
|
|
973
|
+
name: stringField(fn, "name") ?? stringField(toolCall, "name") ?? "tool",
|
|
974
|
+
params: parseMaybeJson(fn?.arguments ?? toolCall.arguments ?? toolCall.input ?? toolCall.rawInput),
|
|
975
|
+
id: stringField(toolCall, "id"),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const update = raw?.params?.update ?? raw?.update;
|
|
979
|
+
const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
|
|
980
|
+
if (acpTool && typeof acpTool === "object") {
|
|
981
|
+
return {
|
|
982
|
+
name: stringField(acpTool, "name") ?? stringField(update, "name") ?? "tool",
|
|
983
|
+
params: parseMaybeJson(acpTool.rawInput ??
|
|
984
|
+
acpTool.raw_input ??
|
|
985
|
+
acpTool.input ??
|
|
986
|
+
acpTool.arguments ??
|
|
987
|
+
acpTool.args ??
|
|
988
|
+
acpTool.params) ?? acpTool,
|
|
989
|
+
id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
function extractToolResult(raw) {
|
|
995
|
+
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
996
|
+
const tr = contents.find((c) => c?.type === "tool_result");
|
|
997
|
+
if (tr) {
|
|
998
|
+
return {
|
|
999
|
+
result: stringifyToolResult(tr.content),
|
|
1000
|
+
id: stringField(tr, "tool_use_id"),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const deepseek = extractDeepseekToolResult(raw);
|
|
1004
|
+
if (deepseek)
|
|
1005
|
+
return deepseek;
|
|
1006
|
+
const item = raw?.item;
|
|
1007
|
+
if (item && typeof item === "object") {
|
|
1008
|
+
const result = codexToolResult(item);
|
|
1009
|
+
return {
|
|
1010
|
+
name: stringField(item, "type") ?? stringField(item, "name"),
|
|
1011
|
+
result: result || stringifyToolResult(item),
|
|
1012
|
+
id: stringField(item, "id"),
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (raw?.role === "tool") {
|
|
1016
|
+
return {
|
|
1017
|
+
result: stringifyToolResult(raw.content),
|
|
1018
|
+
id: stringField(raw, "tool_call_id"),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const update = raw?.params?.update ?? raw?.update;
|
|
1022
|
+
const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
|
|
1023
|
+
if (acpTool && typeof acpTool === "object") {
|
|
1024
|
+
const result = acpTool.output ??
|
|
1025
|
+
acpTool.result ??
|
|
1026
|
+
acpTool.content ??
|
|
1027
|
+
acpTool.error ??
|
|
1028
|
+
update.content ??
|
|
1029
|
+
update;
|
|
1030
|
+
return {
|
|
1031
|
+
name: stringField(acpTool, "name") ?? stringField(update, "name"),
|
|
1032
|
+
result: stringifyToolResult(result),
|
|
1033
|
+
id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
function extractDeepseekToolCall(raw) {
|
|
1039
|
+
const payload = raw?.payload;
|
|
1040
|
+
if (!payload || typeof payload !== "object")
|
|
1041
|
+
return null;
|
|
1042
|
+
if (raw?.event === "tool.started") {
|
|
1043
|
+
const tool = payload.tool && typeof payload.tool === "object" ? payload.tool : undefined;
|
|
1044
|
+
return {
|
|
1045
|
+
name: stringField(payload, "name") ?? stringField(tool, "name") ?? "tool",
|
|
1046
|
+
params: parseMaybeJson(payload.input ?? payload.arguments ?? payload.params ?? tool?.input ?? tool?.rawInput),
|
|
1047
|
+
id: stringField(payload, "id") ?? stringField(tool, "id"),
|
|
1048
|
+
status: stringField(payload, "status") ?? stringField(tool, "status"),
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (payload.event === "item.started") {
|
|
1052
|
+
const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
|
|
1053
|
+
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1054
|
+
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1055
|
+
return {
|
|
1056
|
+
name: stringField(tool, "name") ??
|
|
1057
|
+
stringField(inner, "name") ??
|
|
1058
|
+
stringField(item, "name") ??
|
|
1059
|
+
stringField(item, "type") ??
|
|
1060
|
+
"tool",
|
|
1061
|
+
params: parseMaybeJson(tool?.input ??
|
|
1062
|
+
tool?.rawInput ??
|
|
1063
|
+
tool?.arguments ??
|
|
1064
|
+
inner.input ??
|
|
1065
|
+
item?.input ??
|
|
1066
|
+
item?.arguments) ?? tool ?? item,
|
|
1067
|
+
id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
|
|
1068
|
+
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
function extractDeepseekToolResult(raw) {
|
|
1074
|
+
const payload = raw?.payload;
|
|
1075
|
+
if (!payload || typeof payload !== "object")
|
|
1076
|
+
return null;
|
|
1077
|
+
if (raw?.event === "tool.completed") {
|
|
1078
|
+
const result = payload.output ?? payload.result ?? payload.content ?? payload.error ?? payload;
|
|
1079
|
+
return {
|
|
1080
|
+
name: stringField(payload, "name"),
|
|
1081
|
+
result: stringifyToolResult(result),
|
|
1082
|
+
id: stringField(payload, "id"),
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
if (payload.event === "item.completed" || payload.event === "item.failed") {
|
|
1086
|
+
const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
|
|
1087
|
+
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1088
|
+
const result = item?.output ??
|
|
1089
|
+
item?.result ??
|
|
1090
|
+
item?.content ??
|
|
1091
|
+
item?.detail ??
|
|
1092
|
+
item?.summary ??
|
|
1093
|
+
item?.error ??
|
|
1094
|
+
inner.output ??
|
|
1095
|
+
inner.result ??
|
|
1096
|
+
inner.error ??
|
|
1097
|
+
item ??
|
|
1098
|
+
inner;
|
|
1099
|
+
return {
|
|
1100
|
+
name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
|
|
1101
|
+
result: stringifyToolResult(result),
|
|
1102
|
+
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
925
1107
|
function formatBlockDetails(raw) {
|
|
926
1108
|
if (!raw || typeof raw !== "object")
|
|
927
1109
|
return "";
|
|
@@ -991,6 +1173,52 @@ function codexToolResult(item) {
|
|
|
991
1173
|
}
|
|
992
1174
|
return parts.join("\n");
|
|
993
1175
|
}
|
|
1176
|
+
function stringifyToolResult(value) {
|
|
1177
|
+
if (value == null)
|
|
1178
|
+
return "";
|
|
1179
|
+
if (typeof value === "string")
|
|
1180
|
+
return value;
|
|
1181
|
+
if (Array.isArray(value)) {
|
|
1182
|
+
return value
|
|
1183
|
+
.map((c) => {
|
|
1184
|
+
if (typeof c === "string")
|
|
1185
|
+
return c;
|
|
1186
|
+
if (typeof c?.text === "string")
|
|
1187
|
+
return c.text;
|
|
1188
|
+
return stringifyToolResult(c);
|
|
1189
|
+
})
|
|
1190
|
+
.filter(Boolean)
|
|
1191
|
+
.join("\n");
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
return JSON.stringify(value, null, 2);
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
return String(value);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function parseMaybeJson(value) {
|
|
1201
|
+
if (typeof value !== "string")
|
|
1202
|
+
return value;
|
|
1203
|
+
const trimmed = value.trim();
|
|
1204
|
+
if (!trimmed)
|
|
1205
|
+
return value;
|
|
1206
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
1207
|
+
return value;
|
|
1208
|
+
try {
|
|
1209
|
+
return JSON.parse(trimmed);
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
return value;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function isEmptyRecord(value) {
|
|
1216
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
1217
|
+
}
|
|
1218
|
+
function stringField(obj, key) {
|
|
1219
|
+
const value = obj?.[key];
|
|
1220
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1221
|
+
}
|
|
994
1222
|
function extractContentText(content) {
|
|
995
1223
|
if (!content)
|
|
996
1224
|
return "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GatewayLogger } from "./log.js";
|
|
2
2
|
import { type SessionStore } from "./session-store.js";
|
|
3
3
|
import { type TranscriptWriter } from "./transcript.js";
|
|
4
|
-
import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
|
|
4
|
+
import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, RuntimeRunResult, RuntimeCircuitBreakerSnapshot, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
|
|
5
5
|
/** Factory signature for building a runtime adapter at turn dispatch time. */
|
|
6
6
|
export type RuntimeFactory = (runtimeId: string, extraArgs?: string[]) => RuntimeAdapter;
|
|
7
7
|
/** Constructor options for `Dispatcher`. */
|
|
@@ -12,6 +12,8 @@ export interface DispatcherOptions {
|
|
|
12
12
|
sessionStore: SessionStore;
|
|
13
13
|
log: GatewayLogger;
|
|
14
14
|
turnTimeoutMs?: number;
|
|
15
|
+
runtimeAuthFailureThreshold?: number;
|
|
16
|
+
runtimeAuthFailureCooldownMs?: number;
|
|
15
17
|
/**
|
|
16
18
|
* Live reference to the Gateway's managed-route map. Dispatcher reads
|
|
17
19
|
* `values()` on every `resolveRoute` call so hot-add/remove take effect
|
|
@@ -50,6 +52,24 @@ export interface DispatcherOptions {
|
|
|
50
52
|
* and suppressed so observer failures never break the turn.
|
|
51
53
|
*/
|
|
52
54
|
onOutbound?: OutboundObserver;
|
|
55
|
+
onRuntimeCircuitBreakerChange?: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Optional observer fired exactly once per turn after ``runtime.run``
|
|
58
|
+
* resolves (or throws / times out). Receives the inbound message, the
|
|
59
|
+
* raw runtime result (may be undefined on throw), the elapsed wall
|
|
60
|
+
* time in milliseconds, and any thrown error. The cloud daemon hooks
|
|
61
|
+
* this to settle ``cloud_run`` envelopes against the Hub's usage
|
|
62
|
+
* ledger; local daemons leave it unset.
|
|
63
|
+
*
|
|
64
|
+
* Errors thrown by the observer are logged and swallowed — settle
|
|
65
|
+
* failures must never break the agent reply path.
|
|
66
|
+
*/
|
|
67
|
+
onTurnComplete?: (event: {
|
|
68
|
+
message: GatewayInboundMessage;
|
|
69
|
+
result?: RuntimeRunResult;
|
|
70
|
+
wallTimeMs: number;
|
|
71
|
+
error?: unknown;
|
|
72
|
+
}) => Promise<void> | void;
|
|
53
73
|
/**
|
|
54
74
|
* Optional attention gate (PR3, design §4.2). Resolved AFTER `onInbound`
|
|
55
75
|
* runs and BEFORE the runtime turn enqueues, so working memory / activity
|
|
@@ -92,10 +112,14 @@ export declare class Dispatcher {
|
|
|
92
112
|
private readonly sessionStore;
|
|
93
113
|
private readonly log;
|
|
94
114
|
private readonly turnTimeoutMs;
|
|
115
|
+
private readonly runtimeAuthFailureThreshold;
|
|
116
|
+
private readonly runtimeAuthFailureCooldownMs;
|
|
95
117
|
private readonly buildSystemContext?;
|
|
96
118
|
private readonly buildMemoryContext?;
|
|
97
119
|
private readonly onInbound?;
|
|
98
120
|
private readonly onOutbound?;
|
|
121
|
+
private readonly onTurnComplete?;
|
|
122
|
+
private readonly onRuntimeCircuitBreakerChange?;
|
|
99
123
|
private readonly composeUserTurn?;
|
|
100
124
|
private readonly managedRoutes?;
|
|
101
125
|
private readonly attentionGate?;
|
|
@@ -103,6 +127,7 @@ export declare class Dispatcher {
|
|
|
103
127
|
private readonly transcript;
|
|
104
128
|
private readonly queues;
|
|
105
129
|
private readonly deferredMultimodal;
|
|
130
|
+
private readonly runtimeAuthFailures;
|
|
106
131
|
/**
|
|
107
132
|
* Last `/hub/typing` ping timestamp per (accountId, conversationId).
|
|
108
133
|
* Used to debounce cancel-previous bursts so we don't trip Hub's 20/min
|
|
@@ -114,10 +139,18 @@ export declare class Dispatcher {
|
|
|
114
139
|
handle(envelope: GatewayInboundEnvelope): Promise<void>;
|
|
115
140
|
/** Snapshot of currently running turns keyed by queue key. */
|
|
116
141
|
turns(): Record<string, TurnStatusSnapshot>;
|
|
142
|
+
runtimeCircuitBreakers(): Record<string, RuntimeCircuitBreakerSnapshot>;
|
|
117
143
|
private safeAck;
|
|
118
144
|
private getQueue;
|
|
119
145
|
private deferMultimodal;
|
|
120
146
|
private takeDeferredMultimodal;
|
|
147
|
+
private runtimeAuthBreakerKey;
|
|
148
|
+
private openRuntimeAuthBreaker;
|
|
149
|
+
private pruneExpiredRuntimeAuthBreakers;
|
|
150
|
+
private recordRuntimeAuthFailure;
|
|
151
|
+
private clearRuntimeAuthFailures;
|
|
152
|
+
private notifyRuntimeCircuitBreakerChange;
|
|
153
|
+
private skipRuntimeForAuthBreaker;
|
|
121
154
|
private runCancelPrevious;
|
|
122
155
|
/**
|
|
123
156
|
* Serial mode with coalesce-on-drain semantics:
|