@amodalai/runtime 0.1.0
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/LICENSE +21 -0
- package/dist/.last_build +0 -0
- package/dist/src/agent/agent-runner.d.ts +13 -0
- package/dist/src/agent/agent-runner.js +827 -0
- package/dist/src/agent/agent-runner.js.map +1 -0
- package/dist/src/agent/agent-runner.test.d.ts +6 -0
- package/dist/src/agent/agent-runner.test.js +552 -0
- package/dist/src/agent/agent-runner.test.js.map +1 -0
- package/dist/src/agent/agent-types.d.ts +57 -0
- package/dist/src/agent/agent-types.js +17 -0
- package/dist/src/agent/agent-types.js.map +1 -0
- package/dist/src/agent/agent-types.test.d.ts +6 -0
- package/dist/src/agent/agent-types.test.js +44 -0
- package/dist/src/agent/agent-types.test.js.map +1 -0
- package/dist/src/agent/automation-bridge.d.ts +24 -0
- package/dist/src/agent/automation-bridge.js +24 -0
- package/dist/src/agent/automation-bridge.js.map +1 -0
- package/dist/src/agent/automation-bridge.test.d.ts +6 -0
- package/dist/src/agent/automation-bridge.test.js +67 -0
- package/dist/src/agent/automation-bridge.test.js.map +1 -0
- package/dist/src/agent/config-watcher.d.ts +20 -0
- package/dist/src/agent/config-watcher.js +68 -0
- package/dist/src/agent/config-watcher.js.map +1 -0
- package/dist/src/agent/config-watcher.test.d.ts +6 -0
- package/dist/src/agent/config-watcher.test.js +83 -0
- package/dist/src/agent/config-watcher.test.js.map +1 -0
- package/dist/src/agent/custom-tools-e2e.test.d.ts +6 -0
- package/dist/src/agent/custom-tools-e2e.test.js +566 -0
- package/dist/src/agent/custom-tools-e2e.test.js.map +1 -0
- package/dist/src/agent/local-server.d.ts +15 -0
- package/dist/src/agent/local-server.js +158 -0
- package/dist/src/agent/local-server.js.map +1 -0
- package/dist/src/agent/local-server.test.d.ts +6 -0
- package/dist/src/agent/local-server.test.js +126 -0
- package/dist/src/agent/local-server.test.js.map +1 -0
- package/dist/src/agent/proactive/delivery.d.ts +21 -0
- package/dist/src/agent/proactive/delivery.js +68 -0
- package/dist/src/agent/proactive/delivery.js.map +1 -0
- package/dist/src/agent/proactive/delivery.test.d.ts +6 -0
- package/dist/src/agent/proactive/delivery.test.js +65 -0
- package/dist/src/agent/proactive/delivery.test.js.map +1 -0
- package/dist/src/agent/proactive/proactive-runner.d.ts +76 -0
- package/dist/src/agent/proactive/proactive-runner.js +201 -0
- package/dist/src/agent/proactive/proactive-runner.js.map +1 -0
- package/dist/src/agent/proactive/proactive-runner.test.d.ts +6 -0
- package/dist/src/agent/proactive/proactive-runner.test.js +265 -0
- package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -0
- package/dist/src/agent/request-helper.d.ts +16 -0
- package/dist/src/agent/request-helper.js +87 -0
- package/dist/src/agent/request-helper.js.map +1 -0
- package/dist/src/agent/routes/automations.d.ts +19 -0
- package/dist/src/agent/routes/automations.js +58 -0
- package/dist/src/agent/routes/automations.js.map +1 -0
- package/dist/src/agent/routes/automations.test.d.ts +6 -0
- package/dist/src/agent/routes/automations.test.js +117 -0
- package/dist/src/agent/routes/automations.test.js.map +1 -0
- package/dist/src/agent/routes/chat.d.ts +35 -0
- package/dist/src/agent/routes/chat.js +88 -0
- package/dist/src/agent/routes/chat.js.map +1 -0
- package/dist/src/agent/routes/chat.test.d.ts +6 -0
- package/dist/src/agent/routes/chat.test.js +115 -0
- package/dist/src/agent/routes/chat.test.js.map +1 -0
- package/dist/src/agent/routes/inspect.d.ts +12 -0
- package/dist/src/agent/routes/inspect.js +40 -0
- package/dist/src/agent/routes/inspect.js.map +1 -0
- package/dist/src/agent/routes/inspect.test.d.ts +6 -0
- package/dist/src/agent/routes/inspect.test.js +80 -0
- package/dist/src/agent/routes/inspect.test.js.map +1 -0
- package/dist/src/agent/routes/stores.d.ts +20 -0
- package/dist/src/agent/routes/stores.js +137 -0
- package/dist/src/agent/routes/stores.js.map +1 -0
- package/dist/src/agent/routes/stores.test.d.ts +6 -0
- package/dist/src/agent/routes/stores.test.js +191 -0
- package/dist/src/agent/routes/stores.test.js.map +1 -0
- package/dist/src/agent/routes/task.d.ts +11 -0
- package/dist/src/agent/routes/task.js +116 -0
- package/dist/src/agent/routes/task.js.map +1 -0
- package/dist/src/agent/routes/task.test.d.ts +6 -0
- package/dist/src/agent/routes/task.test.js +91 -0
- package/dist/src/agent/routes/task.test.js.map +1 -0
- package/dist/src/agent/routes/webhooks.d.ts +17 -0
- package/dist/src/agent/routes/webhooks.js +53 -0
- package/dist/src/agent/routes/webhooks.js.map +1 -0
- package/dist/src/agent/routes/webhooks.test.d.ts +6 -0
- package/dist/src/agent/routes/webhooks.test.js +100 -0
- package/dist/src/agent/routes/webhooks.test.js.map +1 -0
- package/dist/src/agent/session-manager.d.ts +72 -0
- package/dist/src/agent/session-manager.js +214 -0
- package/dist/src/agent/session-manager.js.map +1 -0
- package/dist/src/agent/session-manager.test.d.ts +6 -0
- package/dist/src/agent/session-manager.test.js +145 -0
- package/dist/src/agent/session-manager.test.js.map +1 -0
- package/dist/src/agent/shell-executor-local.d.ts +16 -0
- package/dist/src/agent/shell-executor-local.js +51 -0
- package/dist/src/agent/shell-executor-local.js.map +1 -0
- package/dist/src/agent/shell-executor-local.test.d.ts +6 -0
- package/dist/src/agent/shell-executor-local.test.js +46 -0
- package/dist/src/agent/shell-executor-local.test.js.map +1 -0
- package/dist/src/agent/snapshot-server.d.ts +38 -0
- package/dist/src/agent/snapshot-server.js +114 -0
- package/dist/src/agent/snapshot-server.js.map +1 -0
- package/dist/src/agent/stores-e2e.test.d.ts +6 -0
- package/dist/src/agent/stores-e2e.test.js +433 -0
- package/dist/src/agent/stores-e2e.test.js.map +1 -0
- package/dist/src/agent/tool-context-builder.d.ts +11 -0
- package/dist/src/agent/tool-context-builder.js +84 -0
- package/dist/src/agent/tool-context-builder.js.map +1 -0
- package/dist/src/agent/tool-context-builder.test.d.ts +6 -0
- package/dist/src/agent/tool-context-builder.test.js +152 -0
- package/dist/src/agent/tool-context-builder.test.js.map +1 -0
- package/dist/src/agent/tool-executor-local.d.ts +15 -0
- package/dist/src/agent/tool-executor-local.js +74 -0
- package/dist/src/agent/tool-executor-local.js.map +1 -0
- package/dist/src/agent/tool-executor-local.test.d.ts +6 -0
- package/dist/src/agent/tool-executor-local.test.js +116 -0
- package/dist/src/agent/tool-executor-local.test.js.map +1 -0
- package/dist/src/agent/tool-harness-template.d.ts +23 -0
- package/dist/src/agent/tool-harness-template.js +94 -0
- package/dist/src/agent/tool-harness-template.js.map +1 -0
- package/dist/src/agent/user-context-fetcher.d.ts +25 -0
- package/dist/src/agent/user-context-fetcher.js +79 -0
- package/dist/src/agent/user-context-fetcher.js.map +1 -0
- package/dist/src/agent/user-context-fetcher.test.d.ts +6 -0
- package/dist/src/agent/user-context-fetcher.test.js +121 -0
- package/dist/src/agent/user-context-fetcher.test.js.map +1 -0
- package/dist/src/audit/audit-client.d.ts +46 -0
- package/dist/src/audit/audit-client.js +83 -0
- package/dist/src/audit/audit-client.js.map +1 -0
- package/dist/src/cron/heartbeat-runner.d.ts +24 -0
- package/dist/src/cron/heartbeat-runner.js +87 -0
- package/dist/src/cron/heartbeat-runner.js.map +1 -0
- package/dist/src/cron/heartbeat-runner.test.d.ts +6 -0
- package/dist/src/cron/heartbeat-runner.test.js +120 -0
- package/dist/src/cron/heartbeat-runner.test.js.map +1 -0
- package/dist/src/cron/heartbeat-scheduler.d.ts +26 -0
- package/dist/src/cron/heartbeat-scheduler.js +54 -0
- package/dist/src/cron/heartbeat-scheduler.js.map +1 -0
- package/dist/src/cron/heartbeat-scheduler.test.d.ts +6 -0
- package/dist/src/cron/heartbeat-scheduler.test.js +61 -0
- package/dist/src/cron/heartbeat-scheduler.test.js.map +1 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +118 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/middleware/auth.d.ts +40 -0
- package/dist/src/middleware/auth.js +135 -0
- package/dist/src/middleware/auth.js.map +1 -0
- package/dist/src/middleware/auth.test.d.ts +6 -0
- package/dist/src/middleware/auth.test.js +268 -0
- package/dist/src/middleware/auth.test.js.map +1 -0
- package/dist/src/middleware/error-handler.d.ts +20 -0
- package/dist/src/middleware/error-handler.js +48 -0
- package/dist/src/middleware/error-handler.js.map +1 -0
- package/dist/src/middleware/error-handler.test.d.ts +6 -0
- package/dist/src/middleware/error-handler.test.js +68 -0
- package/dist/src/middleware/error-handler.test.js.map +1 -0
- package/dist/src/middleware/request-validation.d.ts +13 -0
- package/dist/src/middleware/request-validation.js +26 -0
- package/dist/src/middleware/request-validation.js.map +1 -0
- package/dist/src/middleware/request-validation.test.d.ts +6 -0
- package/dist/src/middleware/request-validation.test.js +57 -0
- package/dist/src/middleware/request-validation.test.js.map +1 -0
- package/dist/src/output/email-output.d.ts +10 -0
- package/dist/src/output/email-output.js +12 -0
- package/dist/src/output/email-output.js.map +1 -0
- package/dist/src/output/output-router.d.ts +12 -0
- package/dist/src/output/output-router.js +36 -0
- package/dist/src/output/output-router.js.map +1 -0
- package/dist/src/output/output-router.test.d.ts +6 -0
- package/dist/src/output/output-router.test.js +132 -0
- package/dist/src/output/output-router.test.js.map +1 -0
- package/dist/src/output/slack-output.d.ts +10 -0
- package/dist/src/output/slack-output.js +54 -0
- package/dist/src/output/slack-output.js.map +1 -0
- package/dist/src/output/webhook-output.d.ts +10 -0
- package/dist/src/output/webhook-output.js +25 -0
- package/dist/src/output/webhook-output.js.map +1 -0
- package/dist/src/routes/ai-stream.d.ts +159 -0
- package/dist/src/routes/ai-stream.js +309 -0
- package/dist/src/routes/ai-stream.js.map +1 -0
- package/dist/src/routes/ai-stream.test.d.ts +6 -0
- package/dist/src/routes/ai-stream.test.js +586 -0
- package/dist/src/routes/ai-stream.test.js.map +1 -0
- package/dist/src/routes/ask-user-response.d.ts +30 -0
- package/dist/src/routes/ask-user-response.js +61 -0
- package/dist/src/routes/ask-user-response.js.map +1 -0
- package/dist/src/routes/ask-user-response.test.d.ts +6 -0
- package/dist/src/routes/ask-user-response.test.js +88 -0
- package/dist/src/routes/ask-user-response.test.js.map +1 -0
- package/dist/src/routes/chat-stream.d.ts +14 -0
- package/dist/src/routes/chat-stream.js +84 -0
- package/dist/src/routes/chat-stream.js.map +1 -0
- package/dist/src/routes/chat-stream.test.d.ts +6 -0
- package/dist/src/routes/chat-stream.test.js +155 -0
- package/dist/src/routes/chat-stream.test.js.map +1 -0
- package/dist/src/routes/chat.d.ts +13 -0
- package/dist/src/routes/chat.js +55 -0
- package/dist/src/routes/chat.js.map +1 -0
- package/dist/src/routes/chat.test.d.ts +6 -0
- package/dist/src/routes/chat.test.js +99 -0
- package/dist/src/routes/chat.test.js.map +1 -0
- package/dist/src/routes/health.d.ts +13 -0
- package/dist/src/routes/health.js +23 -0
- package/dist/src/routes/health.js.map +1 -0
- package/dist/src/routes/health.test.d.ts +6 -0
- package/dist/src/routes/health.test.js +45 -0
- package/dist/src/routes/health.test.js.map +1 -0
- package/dist/src/routes/sessions.d.ts +14 -0
- package/dist/src/routes/sessions.js +82 -0
- package/dist/src/routes/sessions.js.map +1 -0
- package/dist/src/routes/webhooks.d.ts +13 -0
- package/dist/src/routes/webhooks.js +43 -0
- package/dist/src/routes/webhooks.js.map +1 -0
- package/dist/src/routes/webhooks.test.d.ts +6 -0
- package/dist/src/routes/webhooks.test.js +80 -0
- package/dist/src/routes/webhooks.test.js.map +1 -0
- package/dist/src/routes/widget-actions.d.ts +49 -0
- package/dist/src/routes/widget-actions.js +78 -0
- package/dist/src/routes/widget-actions.js.map +1 -0
- package/dist/src/server.d.ts +31 -0
- package/dist/src/server.js +129 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/server.test.d.ts +6 -0
- package/dist/src/server.test.js +153 -0
- package/dist/src/server.test.js.map +1 -0
- package/dist/src/session/history-converter.d.ts +21 -0
- package/dist/src/session/history-converter.js +59 -0
- package/dist/src/session/history-converter.js.map +1 -0
- package/dist/src/session/history-converter.test.d.ts +6 -0
- package/dist/src/session/history-converter.test.js +130 -0
- package/dist/src/session/history-converter.test.js.map +1 -0
- package/dist/src/session/session-manager.d.ts +117 -0
- package/dist/src/session/session-manager.js +480 -0
- package/dist/src/session/session-manager.js.map +1 -0
- package/dist/src/session/session-manager.test.d.ts +6 -0
- package/dist/src/session/session-manager.test.js +586 -0
- package/dist/src/session/session-manager.test.js.map +1 -0
- package/dist/src/session/session-runner.d.ts +30 -0
- package/dist/src/session/session-runner.js +771 -0
- package/dist/src/session/session-runner.js.map +1 -0
- package/dist/src/session/session-runner.test.d.ts +6 -0
- package/dist/src/session/session-runner.test.js +842 -0
- package/dist/src/session/session-runner.test.js.map +1 -0
- package/dist/src/stores/index.d.ts +8 -0
- package/dist/src/stores/index.js +9 -0
- package/dist/src/stores/index.js.map +1 -0
- package/dist/src/stores/key-resolver.d.ts +21 -0
- package/dist/src/stores/key-resolver.js +30 -0
- package/dist/src/stores/key-resolver.js.map +1 -0
- package/dist/src/stores/key-resolver.test.d.ts +6 -0
- package/dist/src/stores/key-resolver.test.js +31 -0
- package/dist/src/stores/key-resolver.test.js.map +1 -0
- package/dist/src/stores/pglite-store-backend.d.ts +36 -0
- package/dist/src/stores/pglite-store-backend.js +227 -0
- package/dist/src/stores/pglite-store-backend.js.map +1 -0
- package/dist/src/stores/pglite-store-backend.test.d.ts +6 -0
- package/dist/src/stores/pglite-store-backend.test.js +150 -0
- package/dist/src/stores/pglite-store-backend.test.js.map +1 -0
- package/dist/src/stores/ttl-resolver.d.ts +24 -0
- package/dist/src/stores/ttl-resolver.js +64 -0
- package/dist/src/stores/ttl-resolver.js.map +1 -0
- package/dist/src/stores/ttl-resolver.test.d.ts +6 -0
- package/dist/src/stores/ttl-resolver.test.js +68 -0
- package/dist/src/stores/ttl-resolver.test.js.map +1 -0
- package/dist/src/types.d.ts +227 -0
- package/dist/src/types.js +50 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/types.test.d.ts +6 -0
- package/dist/src/types.test.js +68 -0
- package/dist/src/types.test.js.map +1 -0
- package/dist/src/utils/jwt-verify.d.ts +20 -0
- package/dist/src/utils/jwt-verify.js +34 -0
- package/dist/src/utils/jwt-verify.js.map +1 -0
- package/dist/src/utils/jwt-verify.test.d.ts +6 -0
- package/dist/src/utils/jwt-verify.test.js +156 -0
- package/dist/src/utils/jwt-verify.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { GeminiEventType, ToolErrorType, PRESENT_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, ASK_USER_TOOL_NAME, } from '@amodalai/core';
|
|
7
|
+
/**
|
|
8
|
+
* Custom message bus event key for subagent activity.
|
|
9
|
+
* Upstream MessageBusType doesn't include this — our dispatch tool emits on
|
|
10
|
+
* this string key, which works because MessageBus extends EventEmitter.
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- custom event key for our dispatch tool
|
|
13
|
+
const SUBAGENT_ACTIVITY_EVENT = 'subagent-activity';
|
|
14
|
+
import { SSEEventType, } from '../types.js';
|
|
15
|
+
const MAX_TURNS = 50;
|
|
16
|
+
const MAX_RESULT_LENGTH = 2000;
|
|
17
|
+
/**
|
|
18
|
+
* Extract text from tool response parts, truncated for audit logging.
|
|
19
|
+
* Handles both plain text parts and functionResponse parts (from task agents).
|
|
20
|
+
*/
|
|
21
|
+
function extractResultText(parts) {
|
|
22
|
+
if (!parts || parts.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
const segments = [];
|
|
25
|
+
for (const p of parts) {
|
|
26
|
+
if (p.text) {
|
|
27
|
+
segments.push(p.text);
|
|
28
|
+
}
|
|
29
|
+
else if (p.functionResponse?.response) {
|
|
30
|
+
try {
|
|
31
|
+
segments.push(JSON.stringify(p.functionResponse.response));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
segments.push('[unserializable response]');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const text = segments.join('');
|
|
39
|
+
if (!text)
|
|
40
|
+
return undefined;
|
|
41
|
+
return text.length > MAX_RESULT_LENGTH
|
|
42
|
+
? text.slice(0, MAX_RESULT_LENGTH) + '...[truncated]'
|
|
43
|
+
: text;
|
|
44
|
+
}
|
|
45
|
+
const MAX_SUBAGENT_RESULT_LENGTH = 300;
|
|
46
|
+
/**
|
|
47
|
+
* Map SubagentActivityEvent type strings to SSE event_type values.
|
|
48
|
+
*/
|
|
49
|
+
function mapSubagentEventType(type) {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case 'TOOL_CALL_START': return 'tool_call_start';
|
|
52
|
+
case 'TOOL_CALL_END': return 'tool_call_end';
|
|
53
|
+
case 'THOUGHT_CHUNK': return 'thought';
|
|
54
|
+
case 'COMPLETE': return 'complete';
|
|
55
|
+
case 'ERROR': return 'error';
|
|
56
|
+
default: return 'error';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Truncate a string to the given max length, appending '...' if truncated.
|
|
61
|
+
*/
|
|
62
|
+
function truncateSubagentResult(text, maxLen) {
|
|
63
|
+
if (!text)
|
|
64
|
+
return undefined;
|
|
65
|
+
return text.length > maxLen ? text.slice(0, maxLen) + '...' : text;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert a real-time SubagentActivityMessage to an SSESubagentEvent.
|
|
69
|
+
*/
|
|
70
|
+
function subagentMessageToSSE(msg, parentToolId) {
|
|
71
|
+
return {
|
|
72
|
+
type: SSEEventType.SubagentEvent,
|
|
73
|
+
parent_tool_id: parentToolId,
|
|
74
|
+
agent_name: msg.agentName,
|
|
75
|
+
event_type: mapSubagentEventType(msg.eventType),
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- name from subagent activity data
|
|
77
|
+
tool_name: msg.data['name'],
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- args from subagent activity data
|
|
79
|
+
tool_args: msg.data['args'],
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- output from subagent activity data
|
|
81
|
+
result: truncateSubagentResult(msg.data['output'], MAX_SUBAGENT_RESULT_LENGTH),
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- text from subagent COMPLETE event
|
|
83
|
+
text: msg.data['text'],
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- error from subagent activity data
|
|
85
|
+
error: msg.data['error'],
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fire-and-forget POST to platform-api to report usage.
|
|
91
|
+
* Never blocks the chat stream — errors are logged to stderr.
|
|
92
|
+
*/
|
|
93
|
+
function reportUsage(audit, model, taskAgentRuns, tokens) {
|
|
94
|
+
if (!audit.platformApiUrl || !audit.orgId)
|
|
95
|
+
return;
|
|
96
|
+
const url = `${audit.platformApiUrl}/api/orgs/${audit.orgId}/usage`;
|
|
97
|
+
const body = JSON.stringify({
|
|
98
|
+
model,
|
|
99
|
+
api_calls: 1,
|
|
100
|
+
chat_sessions: 1,
|
|
101
|
+
task_agent_runs: taskAgentRuns,
|
|
102
|
+
input_tokens: tokens.inputTokens,
|
|
103
|
+
output_tokens: tokens.outputTokens,
|
|
104
|
+
cached_tokens: tokens.cachedTokens,
|
|
105
|
+
});
|
|
106
|
+
fetch(url, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
...(process.env['INTERNAL_API_KEY']
|
|
111
|
+
? { 'X-Internal-Key': process.env['INTERNAL_API_KEY'] }
|
|
112
|
+
: {}),
|
|
113
|
+
},
|
|
114
|
+
body,
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
process.stderr.write(`[USAGE] Failed to report usage for org ${audit.orgId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fire-and-forget POST to platform-api to persist session history.
|
|
121
|
+
* Never blocks the chat stream — errors are logged to stderr.
|
|
122
|
+
*/
|
|
123
|
+
function saveSessionHistory(audit, sessionId, messages, status, sessionMeta) {
|
|
124
|
+
if (!audit.platformApiUrl || !audit.tenantId)
|
|
125
|
+
return;
|
|
126
|
+
const url = `${audit.platformApiUrl}/api/tenants/${audit.tenantId}/sessions`;
|
|
127
|
+
const body = JSON.stringify({
|
|
128
|
+
id: sessionId,
|
|
129
|
+
tenant_id: audit.tenantId,
|
|
130
|
+
app_id: audit.appId,
|
|
131
|
+
actor: audit.actor,
|
|
132
|
+
messages,
|
|
133
|
+
status,
|
|
134
|
+
// Persist model/provider so hydrated sessions use the same model
|
|
135
|
+
...(sessionMeta?.model ? { model: sessionMeta.model } : {}),
|
|
136
|
+
...(sessionMeta?.provider ? { provider: sessionMeta.provider } : {}),
|
|
137
|
+
});
|
|
138
|
+
fetch(url, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
Authorization: `Bearer ${audit.token}`,
|
|
143
|
+
},
|
|
144
|
+
body,
|
|
145
|
+
}).catch((err) => {
|
|
146
|
+
process.stderr.write(`[SESSION-HISTORY] Failed to save session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Run a synchronous (non-streaming) message through the agentic loop.
|
|
151
|
+
* Collects all text and tool calls, returns a ChatResponse.
|
|
152
|
+
*/
|
|
153
|
+
export async function runMessage(session, message, signal, audit) {
|
|
154
|
+
const { geminiClient, scheduler, config } = session;
|
|
155
|
+
const promptId = `msg-${Date.now()}`;
|
|
156
|
+
const sessionStartMs = Date.now();
|
|
157
|
+
let currentMessages = [
|
|
158
|
+
{ role: 'user', parts: [{ text: message }] },
|
|
159
|
+
];
|
|
160
|
+
let responseText = '';
|
|
161
|
+
const toolCalls = [];
|
|
162
|
+
const skillsActivated = [];
|
|
163
|
+
let turnCount = 0;
|
|
164
|
+
let status = 'completed';
|
|
165
|
+
let errorMessage;
|
|
166
|
+
const tokens = { inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
167
|
+
try {
|
|
168
|
+
while (true) {
|
|
169
|
+
turnCount++;
|
|
170
|
+
if (turnCount > MAX_TURNS) {
|
|
171
|
+
status = 'max_turns';
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (signal.aborted)
|
|
175
|
+
break;
|
|
176
|
+
const toolCallRequests = [];
|
|
177
|
+
const responseStream = geminiClient.sendMessageStream(currentMessages[0]?.parts ?? [], signal, promptId, undefined, false, turnCount === 1 ? message : undefined);
|
|
178
|
+
for await (const event of responseStream) {
|
|
179
|
+
if (signal.aborted)
|
|
180
|
+
break;
|
|
181
|
+
if (event.type === GeminiEventType.Content) {
|
|
182
|
+
responseText += event.value;
|
|
183
|
+
}
|
|
184
|
+
else if (event.type === GeminiEventType.ToolCallRequest) {
|
|
185
|
+
toolCallRequests.push(event.value);
|
|
186
|
+
}
|
|
187
|
+
else if (event.type === GeminiEventType.Finished) {
|
|
188
|
+
const meta = event.value.usageMetadata;
|
|
189
|
+
if (meta) {
|
|
190
|
+
tokens.inputTokens += meta.promptTokenCount ?? 0;
|
|
191
|
+
tokens.outputTokens += meta.candidatesTokenCount ?? 0;
|
|
192
|
+
tokens.cachedTokens += meta.cachedContentTokenCount ?? 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (event.type === GeminiEventType.Error) {
|
|
196
|
+
status = 'error';
|
|
197
|
+
const errObj = event.value.error;
|
|
198
|
+
const errMsg = errObj instanceof Error ? errObj.message : (typeof errObj === 'object' && errObj !== null && 'message' in errObj) ? String(errObj['message']) : String(errObj);
|
|
199
|
+
errorMessage = errMsg;
|
|
200
|
+
throw new Error(errMsg);
|
|
201
|
+
}
|
|
202
|
+
else if (event.type === GeminiEventType.AgentExecutionStopped) {
|
|
203
|
+
return { session_id: session.id, response: responseText, tool_calls: toolCalls };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (toolCallRequests.length > 0) {
|
|
207
|
+
const completedToolCalls = await scheduler.schedule(toolCallRequests, signal);
|
|
208
|
+
const toolResponseParts = [];
|
|
209
|
+
for (const completed of completedToolCalls) {
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- accessing optional field on CompletedToolCall union
|
|
211
|
+
const duration = 'durationMs' in completed ? completed['durationMs'] : undefined;
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- inner_tool_calls from subagent data
|
|
213
|
+
const innerCalls = completed.response.data?.['inner_tool_calls'];
|
|
214
|
+
toolCalls.push({
|
|
215
|
+
tool_name: completed.request.name,
|
|
216
|
+
tool_id: completed.request.callId,
|
|
217
|
+
args: completed.request.args,
|
|
218
|
+
status: completed.response.error ? 'error' : 'success',
|
|
219
|
+
duration_ms: duration,
|
|
220
|
+
error: completed.response.error?.message,
|
|
221
|
+
result: extractResultText(completed.response.responseParts),
|
|
222
|
+
inner_tool_calls: innerCalls,
|
|
223
|
+
});
|
|
224
|
+
// Track skill activations
|
|
225
|
+
if (completed.request.name === ACTIVATE_SKILL_TOOL_NAME &&
|
|
226
|
+
!completed.response.error) {
|
|
227
|
+
const skillName = String(completed.request.args['name'] ?? '');
|
|
228
|
+
if (skillName) {
|
|
229
|
+
skillsActivated.push(skillName);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (completed.response.responseParts) {
|
|
233
|
+
toolResponseParts.push(...completed.response.responseParts);
|
|
234
|
+
}
|
|
235
|
+
// Record tool calls
|
|
236
|
+
try {
|
|
237
|
+
const currentModel = geminiClient.getCurrentSequenceModel() ?? config.getModel();
|
|
238
|
+
geminiClient
|
|
239
|
+
.getChat()
|
|
240
|
+
.recordCompletedToolCalls(currentModel, completedToolCalls);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Non-critical — continue
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Check for stop execution
|
|
247
|
+
const stopTool = completedToolCalls.find((tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION);
|
|
248
|
+
if (stopTool) {
|
|
249
|
+
return { session_id: session.id, response: responseText, tool_calls: toolCalls };
|
|
250
|
+
}
|
|
251
|
+
currentMessages = [{ role: 'user', parts: toolResponseParts }];
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { session_id: session.id, response: responseText, tool_calls: toolCalls };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
status = 'error';
|
|
261
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
if (audit) {
|
|
266
|
+
const model = geminiClient.getCurrentSequenceModel() ?? config.getModel();
|
|
267
|
+
logSessionCompleted(audit, session.id, message, responseText, turnCount, toolCalls, skillsActivated, status, errorMessage, sessionStartMs, model, tokens);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Stream a message through the agentic loop, yielding SSE events.
|
|
273
|
+
*/
|
|
274
|
+
export async function* streamMessage(session, message, signal, audit, sessionManager) {
|
|
275
|
+
const { geminiClient, scheduler, config } = session;
|
|
276
|
+
const promptId = `msg-${Date.now()}`;
|
|
277
|
+
const sessionStartMs = Date.now();
|
|
278
|
+
let currentMessages = [
|
|
279
|
+
{ role: 'user', parts: [{ text: message }] },
|
|
280
|
+
];
|
|
281
|
+
// Accumulators for the consolidated audit event
|
|
282
|
+
const auditToolCalls = [];
|
|
283
|
+
const skillsActivated = [];
|
|
284
|
+
const widgetEvents = [];
|
|
285
|
+
let responseText = '';
|
|
286
|
+
let auditStatus = 'completed';
|
|
287
|
+
const tokens = { inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
288
|
+
// Track content block ordering so history can reconstruct the correct interleave
|
|
289
|
+
const contentBlockOrder = [];
|
|
290
|
+
function trackText(text) {
|
|
291
|
+
const last = contentBlockOrder[contentBlockOrder.length - 1];
|
|
292
|
+
if (last && last['type'] === 'text') {
|
|
293
|
+
last['text'] = String(last['text'] ?? '') + text;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
contentBlockOrder.push({ type: 'text', text });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function trackToolCall(callId) {
|
|
300
|
+
const last = contentBlockOrder[contentBlockOrder.length - 1];
|
|
301
|
+
if (last && last['type'] === 'tool_calls') {
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- extending callIds array
|
|
303
|
+
last['callIds'].push(callId);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
contentBlockOrder.push({ type: 'tool_calls', callIds: [callId] });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let auditError;
|
|
310
|
+
// Accumulate user message for session history
|
|
311
|
+
const userMsg = {
|
|
312
|
+
type: 'user',
|
|
313
|
+
id: `msg-${Date.now()}`,
|
|
314
|
+
text: message,
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
session.accumulatedMessages.push(userMsg);
|
|
318
|
+
yield {
|
|
319
|
+
type: SSEEventType.Init,
|
|
320
|
+
session_id: session.id,
|
|
321
|
+
timestamp: new Date().toISOString(),
|
|
322
|
+
};
|
|
323
|
+
let turnCount = 0;
|
|
324
|
+
while (true) {
|
|
325
|
+
turnCount++;
|
|
326
|
+
if (turnCount > MAX_TURNS) {
|
|
327
|
+
auditStatus = 'max_turns';
|
|
328
|
+
auditError = 'Maximum turns exceeded';
|
|
329
|
+
yield {
|
|
330
|
+
type: SSEEventType.Error,
|
|
331
|
+
message: 'Maximum turns exceeded',
|
|
332
|
+
timestamp: new Date().toISOString(),
|
|
333
|
+
};
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (signal.aborted)
|
|
337
|
+
break;
|
|
338
|
+
const toolCallRequests = [];
|
|
339
|
+
const responseStream = geminiClient.sendMessageStream(currentMessages[0]?.parts ?? [], signal, promptId, undefined, false, turnCount === 1 ? message : undefined);
|
|
340
|
+
for await (const event of responseStream) {
|
|
341
|
+
if (signal.aborted)
|
|
342
|
+
break;
|
|
343
|
+
if (event.type === GeminiEventType.Content) {
|
|
344
|
+
responseText += event.value;
|
|
345
|
+
trackText(event.value);
|
|
346
|
+
yield {
|
|
347
|
+
type: SSEEventType.TextDelta,
|
|
348
|
+
content: event.value,
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
else if (event.type === GeminiEventType.ToolCallRequest) {
|
|
353
|
+
// Suppress tool_call_start for present (widget events replace it) and ask_user (intercepted below)
|
|
354
|
+
if (event.value.name !== PRESENT_TOOL_NAME && event.value.name !== ASK_USER_TOOL_NAME) {
|
|
355
|
+
yield {
|
|
356
|
+
type: SSEEventType.ToolCallStart,
|
|
357
|
+
tool_name: event.value.name,
|
|
358
|
+
tool_id: event.value.callId,
|
|
359
|
+
parameters: event.value.args,
|
|
360
|
+
timestamp: new Date().toISOString(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
toolCallRequests.push(event.value);
|
|
364
|
+
}
|
|
365
|
+
else if (event.type === GeminiEventType.Finished) {
|
|
366
|
+
const meta = event.value.usageMetadata;
|
|
367
|
+
if (meta) {
|
|
368
|
+
tokens.inputTokens += meta.promptTokenCount ?? 0;
|
|
369
|
+
tokens.outputTokens += meta.candidatesTokenCount ?? 0;
|
|
370
|
+
tokens.cachedTokens += meta.cachedContentTokenCount ?? 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (event.type === GeminiEventType.Error) {
|
|
374
|
+
auditStatus = 'error';
|
|
375
|
+
const errObj = event.value.error;
|
|
376
|
+
const errMsg = errObj instanceof Error ? errObj.message : (typeof errObj === 'object' && errObj !== null && 'message' in errObj) ? String(errObj['message']) : String(errObj);
|
|
377
|
+
auditError = errMsg;
|
|
378
|
+
yield {
|
|
379
|
+
type: SSEEventType.Error,
|
|
380
|
+
message: errMsg,
|
|
381
|
+
timestamp: new Date().toISOString(),
|
|
382
|
+
};
|
|
383
|
+
logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
|
|
384
|
+
accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'error');
|
|
385
|
+
yield {
|
|
386
|
+
type: SSEEventType.Done,
|
|
387
|
+
timestamp: new Date().toISOString(),
|
|
388
|
+
usage: {
|
|
389
|
+
input_tokens: tokens.inputTokens,
|
|
390
|
+
output_tokens: tokens.outputTokens,
|
|
391
|
+
cached_tokens: tokens.cachedTokens,
|
|
392
|
+
total_tokens: tokens.inputTokens + tokens.outputTokens,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
else if (event.type === GeminiEventType.AgentExecutionStopped) {
|
|
398
|
+
logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
|
|
399
|
+
accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'completed');
|
|
400
|
+
yield {
|
|
401
|
+
type: SSEEventType.Done,
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
usage: {
|
|
404
|
+
input_tokens: tokens.inputTokens,
|
|
405
|
+
output_tokens: tokens.outputTokens,
|
|
406
|
+
cached_tokens: tokens.cachedTokens,
|
|
407
|
+
total_tokens: tokens.inputTokens + tokens.outputTokens,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (toolCallRequests.length > 0) {
|
|
414
|
+
// Partition: separate ask_user requests from regular tool calls
|
|
415
|
+
const askUserRequests = toolCallRequests.filter((req) => req.name === ASK_USER_TOOL_NAME);
|
|
416
|
+
const otherRequests = toolCallRequests.filter((req) => req.name !== ASK_USER_TOOL_NAME);
|
|
417
|
+
const toolResponseParts = [];
|
|
418
|
+
// Handle ask_user requests: yield SSE event, wait for user response
|
|
419
|
+
if (askUserRequests.length > 0 && sessionManager) {
|
|
420
|
+
for (const askReq of askUserRequests) {
|
|
421
|
+
const askId = askReq.callId;
|
|
422
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- LLM-provided questions array
|
|
423
|
+
const questions = askReq.args['questions'] ?? [];
|
|
424
|
+
const askStartMs = Date.now();
|
|
425
|
+
// Emit tool_call_start for the ask_user tool
|
|
426
|
+
yield {
|
|
427
|
+
type: SSEEventType.ToolCallStart,
|
|
428
|
+
tool_name: askReq.name,
|
|
429
|
+
tool_id: askId,
|
|
430
|
+
parameters: askReq.args,
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
// Track ask_user in content block ordering
|
|
434
|
+
contentBlockOrder.push({ type: 'ask_user', askId });
|
|
435
|
+
// Emit ask_user SSE event
|
|
436
|
+
yield {
|
|
437
|
+
type: SSEEventType.AskUser,
|
|
438
|
+
ask_id: askId,
|
|
439
|
+
questions,
|
|
440
|
+
timestamp: new Date().toISOString(),
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
const answers = await sessionManager.waitForAskUserResponse(session, askId, signal);
|
|
444
|
+
const askDuration = Date.now() - askStartMs;
|
|
445
|
+
const resultOutput = JSON.stringify({ answers });
|
|
446
|
+
// Audit
|
|
447
|
+
auditToolCalls.push({
|
|
448
|
+
tool_name: askReq.name,
|
|
449
|
+
tool_id: askId,
|
|
450
|
+
args: askReq.args,
|
|
451
|
+
status: 'success',
|
|
452
|
+
duration_ms: askDuration,
|
|
453
|
+
});
|
|
454
|
+
// Emit tool_call_result
|
|
455
|
+
yield {
|
|
456
|
+
type: SSEEventType.ToolCallResult,
|
|
457
|
+
tool_id: askId,
|
|
458
|
+
status: 'success',
|
|
459
|
+
result: resultOutput.slice(0, 500),
|
|
460
|
+
duration_ms: askDuration,
|
|
461
|
+
timestamp: new Date().toISOString(),
|
|
462
|
+
};
|
|
463
|
+
// Build function response part
|
|
464
|
+
toolResponseParts.push({
|
|
465
|
+
functionResponse: {
|
|
466
|
+
id: askId,
|
|
467
|
+
name: ASK_USER_TOOL_NAME,
|
|
468
|
+
response: { output: resultOutput },
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
const askDuration = Date.now() - askStartMs;
|
|
474
|
+
const errorMsg = err instanceof Error ? err.message : 'ask_user failed';
|
|
475
|
+
auditToolCalls.push({
|
|
476
|
+
tool_name: askReq.name,
|
|
477
|
+
tool_id: askId,
|
|
478
|
+
args: askReq.args,
|
|
479
|
+
status: 'error',
|
|
480
|
+
duration_ms: askDuration,
|
|
481
|
+
error: errorMsg,
|
|
482
|
+
});
|
|
483
|
+
yield {
|
|
484
|
+
type: SSEEventType.ToolCallResult,
|
|
485
|
+
tool_id: askId,
|
|
486
|
+
status: 'error',
|
|
487
|
+
error: errorMsg,
|
|
488
|
+
duration_ms: askDuration,
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
};
|
|
491
|
+
toolResponseParts.push({
|
|
492
|
+
functionResponse: {
|
|
493
|
+
id: askId,
|
|
494
|
+
name: ASK_USER_TOOL_NAME,
|
|
495
|
+
response: { output: JSON.stringify({ error: errorMsg }) },
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (askUserRequests.length > 0) {
|
|
502
|
+
// No sessionManager — return fallback response
|
|
503
|
+
for (const askReq of askUserRequests) {
|
|
504
|
+
auditToolCalls.push({
|
|
505
|
+
tool_name: askReq.name,
|
|
506
|
+
tool_id: askReq.callId,
|
|
507
|
+
args: askReq.args,
|
|
508
|
+
status: 'error',
|
|
509
|
+
error: 'ask_user not supported in this session mode',
|
|
510
|
+
});
|
|
511
|
+
toolResponseParts.push({
|
|
512
|
+
functionResponse: {
|
|
513
|
+
id: askReq.callId,
|
|
514
|
+
name: ASK_USER_TOOL_NAME,
|
|
515
|
+
response: { output: JSON.stringify({ error: 'ask_user not supported in this session mode' }) },
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Handle other tool calls through the scheduler normally
|
|
521
|
+
if (otherRequests.length > 0) {
|
|
522
|
+
// --- Real-time subagent event streaming ---
|
|
523
|
+
// Subscribe to SUBAGENT_ACTIVITY on the message bus so we can yield
|
|
524
|
+
// subagent events as they happen (instead of waiting for dispatch to finish).
|
|
525
|
+
const messageBus = config.getMessageBus();
|
|
526
|
+
const subagentEventQueue = [];
|
|
527
|
+
let notifyNewEvent = null;
|
|
528
|
+
// Track dispatch tool call IDs for correlation
|
|
529
|
+
const unmatchedDispatchCallIds = new Set();
|
|
530
|
+
const dispatchIdToCallId = new Map();
|
|
531
|
+
for (const req of otherRequests) {
|
|
532
|
+
if (req.name === 'dispatch') {
|
|
533
|
+
unmatchedDispatchCallIds.add(req.callId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const hasDispatchCalls = unmatchedDispatchCallIds.size > 0;
|
|
537
|
+
const subagentListener = (msg) => {
|
|
538
|
+
// Map dispatchId to the parent tool callId
|
|
539
|
+
let parentToolId = dispatchIdToCallId.get(msg.dispatchId);
|
|
540
|
+
if (!parentToolId && unmatchedDispatchCallIds.size > 0) {
|
|
541
|
+
// Assign to the first unmatched dispatch call
|
|
542
|
+
const firstUnmatched = unmatchedDispatchCallIds.values().next().value;
|
|
543
|
+
if (firstUnmatched) {
|
|
544
|
+
unmatchedDispatchCallIds.delete(firstUnmatched);
|
|
545
|
+
dispatchIdToCallId.set(msg.dispatchId, firstUnmatched);
|
|
546
|
+
parentToolId = firstUnmatched;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!parentToolId)
|
|
550
|
+
return;
|
|
551
|
+
subagentEventQueue.push(subagentMessageToSSE(msg, parentToolId));
|
|
552
|
+
// Wake up the yield loop
|
|
553
|
+
notifyNewEvent?.();
|
|
554
|
+
notifyNewEvent = null;
|
|
555
|
+
};
|
|
556
|
+
if (hasDispatchCalls) {
|
|
557
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any -- custom event not in upstream Message union; EventEmitter accepts any string
|
|
558
|
+
messageBus.subscribe(SUBAGENT_ACTIVITY_EVENT, subagentListener);
|
|
559
|
+
}
|
|
560
|
+
// Start the scheduler (don't await yet — we'll yield events concurrently)
|
|
561
|
+
let completedToolCalls;
|
|
562
|
+
const schedulerPromise = scheduler.schedule(otherRequests, signal).then((result) => {
|
|
563
|
+
completedToolCalls = result;
|
|
564
|
+
// Wake up the yield loop so it can exit
|
|
565
|
+
notifyNewEvent?.();
|
|
566
|
+
notifyNewEvent = null;
|
|
567
|
+
});
|
|
568
|
+
// Yield subagent events in real-time while the scheduler is running
|
|
569
|
+
if (hasDispatchCalls) {
|
|
570
|
+
while (completedToolCalls === undefined || subagentEventQueue.length > 0) {
|
|
571
|
+
// Drain any pending events
|
|
572
|
+
while (subagentEventQueue.length > 0) {
|
|
573
|
+
yield subagentEventQueue.shift();
|
|
574
|
+
}
|
|
575
|
+
// If scheduler isn't done, wait for a new event or completion
|
|
576
|
+
if (completedToolCalls === undefined) {
|
|
577
|
+
await new Promise((resolve) => {
|
|
578
|
+
notifyNewEvent = resolve;
|
|
579
|
+
// Also resolve if the scheduler finishes while we're waiting
|
|
580
|
+
schedulerPromise.then(() => {
|
|
581
|
+
resolve();
|
|
582
|
+
return;
|
|
583
|
+
}, () => {
|
|
584
|
+
resolve();
|
|
585
|
+
return;
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any -- custom event not in upstream Message union; EventEmitter accepts any string
|
|
591
|
+
messageBus.unsubscribe(SUBAGENT_ACTIVITY_EVENT, subagentListener);
|
|
592
|
+
}
|
|
593
|
+
// Ensure scheduler has completed (no-op if already resolved)
|
|
594
|
+
await schedulerPromise;
|
|
595
|
+
// Process completed tool calls
|
|
596
|
+
for (const completed of completedToolCalls) {
|
|
597
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- accessing optional field on CompletedToolCall union
|
|
598
|
+
const duration = 'durationMs' in completed ? completed['durationMs'] : undefined;
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- inner_tool_calls from subagent data
|
|
600
|
+
const innerCalls = completed.response.data?.['inner_tool_calls'];
|
|
601
|
+
// Accumulate tool call summary for audit
|
|
602
|
+
auditToolCalls.push({
|
|
603
|
+
tool_name: completed.request.name,
|
|
604
|
+
tool_id: completed.request.callId,
|
|
605
|
+
args: completed.request.args,
|
|
606
|
+
status: completed.response.error ? 'error' : 'success',
|
|
607
|
+
duration_ms: duration,
|
|
608
|
+
error: completed.response.error?.message,
|
|
609
|
+
result: extractResultText(completed.response.responseParts),
|
|
610
|
+
inner_tool_calls: innerCalls,
|
|
611
|
+
});
|
|
612
|
+
// Most subagent events were streamed in real-time above.
|
|
613
|
+
// The COMPLETE event (agent's final summary) may be missed due to race
|
|
614
|
+
// conditions, so emit it from the batch data as a fallback.
|
|
615
|
+
if (completed.request.name === 'dispatch' && completed.response.data) {
|
|
616
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- subagent_events from dispatch tool data
|
|
617
|
+
const batchEvents = completed.response.data['subagent_events'];
|
|
618
|
+
if (batchEvents) {
|
|
619
|
+
for (const evt of batchEvents) {
|
|
620
|
+
if (evt.type === 'COMPLETE' && typeof evt.data['text'] === 'string') {
|
|
621
|
+
yield {
|
|
622
|
+
type: SSEEventType.SubagentEvent,
|
|
623
|
+
parent_tool_id: completed.request.callId,
|
|
624
|
+
agent_name: evt.agentName,
|
|
625
|
+
event_type: 'complete',
|
|
626
|
+
text: evt.data['text'],
|
|
627
|
+
timestamp: new Date().toISOString(),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (completed.request.name === PRESENT_TOOL_NAME && !completed.response.error) {
|
|
634
|
+
// Emit a widget event instead of tool_call_result for the present tool
|
|
635
|
+
const args = completed.request.args;
|
|
636
|
+
const widgetType = String(args['widget'] ?? 'unknown');
|
|
637
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- LLM-provided data object
|
|
638
|
+
const widgetData = args['data'] ?? {};
|
|
639
|
+
widgetEvents.push({ widgetType, data: widgetData });
|
|
640
|
+
contentBlockOrder.push({ type: 'widget', widgetType, data: widgetData });
|
|
641
|
+
yield {
|
|
642
|
+
type: SSEEventType.Widget,
|
|
643
|
+
widget_type: widgetType,
|
|
644
|
+
data: widgetData,
|
|
645
|
+
timestamp: new Date().toISOString(),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
trackToolCall(completed.request.callId);
|
|
650
|
+
yield {
|
|
651
|
+
type: SSEEventType.ToolCallResult,
|
|
652
|
+
tool_id: completed.request.callId,
|
|
653
|
+
status: completed.response.error ? 'error' : 'success',
|
|
654
|
+
result: extractResultText(completed.response.responseParts)?.slice(0, 500),
|
|
655
|
+
duration_ms: duration,
|
|
656
|
+
error: completed.response.error?.message,
|
|
657
|
+
timestamp: new Date().toISOString(),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// Track skill activations
|
|
661
|
+
if (completed.request.name === ACTIVATE_SKILL_TOOL_NAME &&
|
|
662
|
+
!completed.response.error) {
|
|
663
|
+
const skillName = String(completed.request.args['name'] ?? '');
|
|
664
|
+
if (skillName) {
|
|
665
|
+
skillsActivated.push(skillName);
|
|
666
|
+
yield {
|
|
667
|
+
type: SSEEventType.SkillActivated,
|
|
668
|
+
skill_name: skillName,
|
|
669
|
+
timestamp: new Date().toISOString(),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (completed.response.responseParts) {
|
|
674
|
+
toolResponseParts.push(...completed.response.responseParts);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Record tool calls
|
|
678
|
+
try {
|
|
679
|
+
const currentModel = geminiClient.getCurrentSequenceModel() ?? config.getModel();
|
|
680
|
+
geminiClient
|
|
681
|
+
.getChat()
|
|
682
|
+
.recordCompletedToolCalls(currentModel, completedToolCalls);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// Non-critical
|
|
686
|
+
}
|
|
687
|
+
const stopTool = completedToolCalls.find((tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION);
|
|
688
|
+
if (stopTool) {
|
|
689
|
+
logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
|
|
690
|
+
accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'completed');
|
|
691
|
+
yield {
|
|
692
|
+
type: SSEEventType.Done,
|
|
693
|
+
timestamp: new Date().toISOString(),
|
|
694
|
+
};
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
currentMessages = [{ role: 'user', parts: toolResponseParts }];
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
|
|
705
|
+
accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, auditStatus === 'completed' ? 'completed' : 'error');
|
|
706
|
+
yield {
|
|
707
|
+
type: SSEEventType.Done,
|
|
708
|
+
timestamp: new Date().toISOString(),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Accumulate the assistant response message and fire-and-forget save to platform-api.
|
|
713
|
+
*/
|
|
714
|
+
function accumulateAssistantAndSave(session, audit, responseText, toolCalls, skillsActivated, widgetEvents, contentBlocks, status) {
|
|
715
|
+
const assistantMsg = {
|
|
716
|
+
type: 'assistant_text',
|
|
717
|
+
id: `msg-${Date.now()}`,
|
|
718
|
+
text: responseText,
|
|
719
|
+
timestamp: new Date().toISOString(),
|
|
720
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
721
|
+
toolName: tc.tool_name,
|
|
722
|
+
toolId: tc.tool_id,
|
|
723
|
+
args: tc.args,
|
|
724
|
+
status: tc.status,
|
|
725
|
+
duration_ms: tc.duration_ms,
|
|
726
|
+
error: tc.error,
|
|
727
|
+
result: tc.result,
|
|
728
|
+
inner_tool_calls: tc.inner_tool_calls,
|
|
729
|
+
})),
|
|
730
|
+
skillActivations: skillsActivated,
|
|
731
|
+
widgets: widgetEvents.map((w) => ({
|
|
732
|
+
widgetType: w.widgetType,
|
|
733
|
+
data: w.data,
|
|
734
|
+
})),
|
|
735
|
+
contentBlocks,
|
|
736
|
+
};
|
|
737
|
+
session.accumulatedMessages.push(assistantMsg);
|
|
738
|
+
if (audit) {
|
|
739
|
+
saveSessionHistory(audit, session.id, session.accumulatedMessages, status, {
|
|
740
|
+
model: session.model,
|
|
741
|
+
provider: session.provider,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function logSessionCompleted(audit, sessionId, message, response, turns, toolCalls, skillsActivated, status, error, startMs, model, tokens) {
|
|
746
|
+
if (!audit)
|
|
747
|
+
return;
|
|
748
|
+
const details = {
|
|
749
|
+
message,
|
|
750
|
+
response,
|
|
751
|
+
tenant_id: audit.tenantId,
|
|
752
|
+
org_id: audit.orgId,
|
|
753
|
+
turns,
|
|
754
|
+
duration_ms: Date.now() - startMs,
|
|
755
|
+
status,
|
|
756
|
+
tool_calls: toolCalls,
|
|
757
|
+
skills_activated: skillsActivated,
|
|
758
|
+
};
|
|
759
|
+
if (error) {
|
|
760
|
+
details['error'] = error;
|
|
761
|
+
}
|
|
762
|
+
audit.auditClient.log(audit.appId, audit.token, {
|
|
763
|
+
event: 'session_completed',
|
|
764
|
+
resource_name: sessionId,
|
|
765
|
+
details,
|
|
766
|
+
});
|
|
767
|
+
// Report usage to platform API
|
|
768
|
+
const taskAgentRuns = toolCalls.filter((tc) => tc.tool_name === 'dispatch').length;
|
|
769
|
+
reportUsage(audit, model ?? 'unknown', taskAgentRuns, tokens ?? { inputTokens: 0, outputTokens: 0, cachedTokens: 0 });
|
|
770
|
+
}
|
|
771
|
+
//# sourceMappingURL=session-runner.js.map
|