@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,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { EXPLORE_TOOL_NAME, EXPLORE_TOOL_SCHEMA, validateExploreRequest, resolveExploreModel, FailoverProvider, storeToJsonSchema, storeToToolName, findStoreByToolName, } from '@amodalai/core';
|
|
7
|
+
import { resolveKey } from '../stores/key-resolver.js';
|
|
8
|
+
import { SSEEventType } from '../types.js';
|
|
9
|
+
import { makeApiRequest } from './request-helper.js';
|
|
10
|
+
import { buildToolContext } from './tool-context-builder.js';
|
|
11
|
+
import { LocalToolExecutor } from './tool-executor-local.js';
|
|
12
|
+
const MAX_TURNS = 15;
|
|
13
|
+
/**
|
|
14
|
+
* Runs an agent turn as a ReAct loop, yielding SSE events.
|
|
15
|
+
*
|
|
16
|
+
* Uses FailoverProvider for multi-provider support with retry + fallback.
|
|
17
|
+
*/
|
|
18
|
+
export async function* runAgentTurn(session, message, signal) {
|
|
19
|
+
// Append user message
|
|
20
|
+
session.conversationHistory.push({ role: 'user', content: message });
|
|
21
|
+
const modelConfig = session.runtime.repo.config.models.main;
|
|
22
|
+
let provider;
|
|
23
|
+
try {
|
|
24
|
+
provider = new FailoverProvider(modelConfig);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
yield {
|
|
29
|
+
type: SSEEventType.Error,
|
|
30
|
+
message: `Provider initialization failed: ${errMsg}`,
|
|
31
|
+
timestamp: ts(),
|
|
32
|
+
};
|
|
33
|
+
yield { type: SSEEventType.Done, timestamp: ts() };
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const tools = buildTools(session);
|
|
37
|
+
const systemPrompt = buildSystemPrompt(session);
|
|
38
|
+
let turns = 0;
|
|
39
|
+
while (turns < MAX_TURNS) {
|
|
40
|
+
if (signal.aborted) {
|
|
41
|
+
yield { type: SSEEventType.Error, message: 'Request aborted', timestamp: ts() };
|
|
42
|
+
yield { type: SSEEventType.Done, timestamp: ts() };
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
turns++;
|
|
46
|
+
try {
|
|
47
|
+
const chatRequest = {
|
|
48
|
+
model: modelConfig.model,
|
|
49
|
+
systemPrompt,
|
|
50
|
+
messages: session.conversationHistory,
|
|
51
|
+
tools,
|
|
52
|
+
maxTokens: 4096,
|
|
53
|
+
signal,
|
|
54
|
+
};
|
|
55
|
+
// Use streaming when available for real-time text delivery
|
|
56
|
+
if (provider.chatStream) {
|
|
57
|
+
const { content, hasToolUse, toolResults } = yield* processStream(provider.chatStream(chatRequest), session, signal);
|
|
58
|
+
// Store assistant message
|
|
59
|
+
session.conversationHistory.push({ role: 'assistant', content });
|
|
60
|
+
if (hasToolUse && toolResults.length > 0) {
|
|
61
|
+
session.conversationHistory.push(...toolResults);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
// Non-streaming fallback
|
|
67
|
+
const response = await provider.chat(chatRequest);
|
|
68
|
+
let hasToolUse = false;
|
|
69
|
+
const toolResults = [];
|
|
70
|
+
for (const block of response.content) {
|
|
71
|
+
if (block.type === 'text') {
|
|
72
|
+
const processed = processTextOutput(session, block.text);
|
|
73
|
+
yield { type: SSEEventType.TextDelta, content: processed, timestamp: ts() };
|
|
74
|
+
}
|
|
75
|
+
else if (block.type === 'tool_use') {
|
|
76
|
+
hasToolUse = true;
|
|
77
|
+
yield {
|
|
78
|
+
type: SSEEventType.ToolCallStart,
|
|
79
|
+
tool_name: block.name,
|
|
80
|
+
tool_id: block.id,
|
|
81
|
+
parameters: block.input,
|
|
82
|
+
timestamp: ts(),
|
|
83
|
+
};
|
|
84
|
+
// Emit ExploreStart before execution
|
|
85
|
+
if (block.name === EXPLORE_TOOL_NAME) {
|
|
86
|
+
yield {
|
|
87
|
+
type: SSEEventType.ExploreStart,
|
|
88
|
+
query: String(block.input['query'] ?? ''),
|
|
89
|
+
timestamp: ts(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const startMs = Date.now();
|
|
93
|
+
const execResult = await executeTool(session, block.name, block.input, block.id, signal);
|
|
94
|
+
const durationMs = Date.now() - startMs;
|
|
95
|
+
// Emit subagent events from explore
|
|
96
|
+
if (execResult.subagentEvents) {
|
|
97
|
+
for (const evt of execResult.subagentEvents) {
|
|
98
|
+
yield evt;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Emit ExploreEnd after execution
|
|
102
|
+
if (block.name === EXPLORE_TOOL_NAME && execResult.exploreResult) {
|
|
103
|
+
yield {
|
|
104
|
+
type: SSEEventType.ExploreEnd,
|
|
105
|
+
summary: execResult.exploreResult.summary,
|
|
106
|
+
tokens_used: execResult.exploreResult.tokensUsed,
|
|
107
|
+
timestamp: ts(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
yield {
|
|
111
|
+
type: SSEEventType.ToolCallResult,
|
|
112
|
+
tool_id: block.id,
|
|
113
|
+
status: execResult.result.error ? 'error' : 'success',
|
|
114
|
+
result: execResult.result.output,
|
|
115
|
+
error: execResult.result.error,
|
|
116
|
+
duration_ms: durationMs,
|
|
117
|
+
timestamp: ts(),
|
|
118
|
+
};
|
|
119
|
+
toolResults.push({
|
|
120
|
+
role: 'tool_result',
|
|
121
|
+
toolCallId: block.id,
|
|
122
|
+
content: execResult.result.error ?? execResult.result.output ?? '',
|
|
123
|
+
isError: !!execResult.result.error,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
session.conversationHistory.push({ role: 'assistant', content: response.content });
|
|
128
|
+
if (hasToolUse && toolResults.length > 0) {
|
|
129
|
+
session.conversationHistory.push(...toolResults);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
yield { type: SSEEventType.Error, message: `LLM error: ${errMsg}`, timestamp: ts() };
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (turns >= MAX_TURNS) {
|
|
141
|
+
yield {
|
|
142
|
+
type: SSEEventType.Error,
|
|
143
|
+
message: `Agent loop exceeded max turns (${MAX_TURNS})`,
|
|
144
|
+
timestamp: ts(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
yield { type: SSEEventType.Done, timestamp: ts() };
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Helpers
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
function ts() {
|
|
153
|
+
return new Date().toISOString();
|
|
154
|
+
}
|
|
155
|
+
function buildSystemPrompt(session) {
|
|
156
|
+
const parts = [session.runtime.compiledContext.systemPrompt];
|
|
157
|
+
const planReminder = session.planModeManager.getPlanningReminder();
|
|
158
|
+
if (planReminder) {
|
|
159
|
+
parts.push(planReminder);
|
|
160
|
+
}
|
|
161
|
+
const approvedPlan = session.planModeManager.getApprovedPlanContext();
|
|
162
|
+
if (approvedPlan) {
|
|
163
|
+
parts.push(approvedPlan);
|
|
164
|
+
}
|
|
165
|
+
return parts.join('\n\n');
|
|
166
|
+
}
|
|
167
|
+
function buildTools(session) {
|
|
168
|
+
const tools = [];
|
|
169
|
+
// Request tool (for connected systems)
|
|
170
|
+
tools.push({
|
|
171
|
+
name: 'request',
|
|
172
|
+
description: 'Make HTTP requests to connected systems. Specify connection name, method, endpoint, and optional params/data.',
|
|
173
|
+
parameters: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
connection: { type: 'string', description: 'Connection name' },
|
|
177
|
+
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
|
|
178
|
+
endpoint: { type: 'string', description: 'API endpoint path' },
|
|
179
|
+
params: { type: 'object', description: 'Query parameters' },
|
|
180
|
+
data: { description: 'Request body' },
|
|
181
|
+
intent: { type: 'string', enum: ['read', 'write', 'confirmed_write'] },
|
|
182
|
+
},
|
|
183
|
+
required: ['connection', 'method', 'endpoint', 'intent'],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
// Explore tool
|
|
187
|
+
tools.push({
|
|
188
|
+
name: EXPLORE_TOOL_NAME,
|
|
189
|
+
description: EXPLORE_TOOL_SCHEMA.description,
|
|
190
|
+
parameters: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
query: { type: 'string', description: 'What to investigate' },
|
|
194
|
+
endpoint_hints: {
|
|
195
|
+
type: 'array',
|
|
196
|
+
items: { type: 'string' },
|
|
197
|
+
description: 'Optional endpoint paths to prioritize',
|
|
198
|
+
},
|
|
199
|
+
model: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Optional: "simple" for lightweight, "default" for standard, "advanced" for primary model, or "provider:model" for specific',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ['query'],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
// Plan mode tools
|
|
208
|
+
tools.push({
|
|
209
|
+
name: 'enter_plan_mode',
|
|
210
|
+
description: 'Enter planning mode. Write operations will be blocked until a plan is approved.',
|
|
211
|
+
parameters: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
reason: { type: 'string', description: 'Why planning mode is needed' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
tools.push({
|
|
219
|
+
name: 'exit_plan_mode',
|
|
220
|
+
description: 'Exit planning mode.',
|
|
221
|
+
parameters: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
// Custom tools from tools/ directory
|
|
227
|
+
for (const tool of session.runtime.repo.tools) {
|
|
228
|
+
// Skip tools marked as hidden from the LLM
|
|
229
|
+
if (tool.confirm === 'never') {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
tools.push({
|
|
233
|
+
name: tool.name,
|
|
234
|
+
description: tool.description,
|
|
235
|
+
parameters: tool.parameters,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// MCP tools from connected MCP servers
|
|
239
|
+
if (session.mcpManager) {
|
|
240
|
+
for (const mcpTool of session.mcpManager.getDiscoveredTools()) {
|
|
241
|
+
tools.push({
|
|
242
|
+
name: mcpTool.name,
|
|
243
|
+
description: mcpTool.description,
|
|
244
|
+
parameters: mcpTool.parameters,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Store write tools (one per store — schema as parameters for structured output)
|
|
249
|
+
for (const store of session.runtime.repo.stores) {
|
|
250
|
+
tools.push({
|
|
251
|
+
name: storeToToolName(store.name),
|
|
252
|
+
description: `Store a ${store.entity.name} to the ${store.name} collection.`,
|
|
253
|
+
parameters: storeToJsonSchema(store),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Store query tool (single tool for reading from any store)
|
|
257
|
+
if (session.runtime.repo.stores.length > 0) {
|
|
258
|
+
tools.push({
|
|
259
|
+
name: 'query_store',
|
|
260
|
+
description: 'Query documents from a store collection. Use "key" for a single document or "filter" for a list.',
|
|
261
|
+
parameters: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
store: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
enum: session.runtime.repo.stores.map((s) => s.name),
|
|
267
|
+
description: 'The store to query',
|
|
268
|
+
},
|
|
269
|
+
key: { type: 'string', description: 'Get a specific document by key' },
|
|
270
|
+
filter: { type: 'object', description: 'Filter by field values (equality match)' },
|
|
271
|
+
sort: { type: 'string', description: 'Sort field, prefix with - for descending' },
|
|
272
|
+
limit: { type: 'number', description: 'Max documents to return (default: 20)' },
|
|
273
|
+
},
|
|
274
|
+
required: ['store'],
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Shell execution tool (opt-in via config.sandbox.shellExec)
|
|
279
|
+
if (session.runtime.repo.config.sandbox?.shellExec) {
|
|
280
|
+
tools.push({
|
|
281
|
+
name: 'shell_exec',
|
|
282
|
+
description: 'Execute a shell command. Use for data transformation, computation, scripting, or anything that benefits from code execution.',
|
|
283
|
+
parameters: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
command: { type: 'string', description: 'The shell command to execute' },
|
|
287
|
+
},
|
|
288
|
+
required: ['command'],
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return tools;
|
|
293
|
+
}
|
|
294
|
+
async function executeTool(session, toolName, args, toolId, signal) {
|
|
295
|
+
switch (toolName) {
|
|
296
|
+
case 'request':
|
|
297
|
+
return { result: await executeRequestTool(session, args, signal) };
|
|
298
|
+
case EXPLORE_TOOL_NAME:
|
|
299
|
+
return executeExploreTool(session, args, toolId, signal);
|
|
300
|
+
case 'enter_plan_mode':
|
|
301
|
+
return { result: await executePlanModeEnter(session, args) };
|
|
302
|
+
case 'exit_plan_mode':
|
|
303
|
+
return { result: await executePlanModeExit(session) };
|
|
304
|
+
case 'shell_exec':
|
|
305
|
+
return { result: await executeShellExecTool(session, args, signal) };
|
|
306
|
+
case 'query_store':
|
|
307
|
+
return { result: await executeQueryStore(session, args) };
|
|
308
|
+
default: {
|
|
309
|
+
// Check store write tools (store_* prefix)
|
|
310
|
+
if (toolName.startsWith('store_')) {
|
|
311
|
+
const store = findStoreByToolName(session.runtime.repo.stores, toolName);
|
|
312
|
+
if (store) {
|
|
313
|
+
return { result: await executeStorePut(session, store, args) };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Check custom tools
|
|
317
|
+
const customTool = session.runtime.repo.tools.find((t) => t.name === toolName);
|
|
318
|
+
if (customTool) {
|
|
319
|
+
return { result: await executeCustomTool(session, customTool, args, signal) };
|
|
320
|
+
}
|
|
321
|
+
// Check MCP tools (namespaced as serverName__toolName)
|
|
322
|
+
if (session.mcpManager?.isMcpTool(toolName)) {
|
|
323
|
+
return { result: await executeMcpTool(session, toolName, args) };
|
|
324
|
+
}
|
|
325
|
+
return { result: { error: `Unknown tool: ${toolName}` } };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function executeRequestTool(session, args, signal) {
|
|
330
|
+
const connectionName = String(args['connection'] ?? '');
|
|
331
|
+
const method = String(args['method'] ?? 'GET');
|
|
332
|
+
const endpoint = String(args['endpoint'] ?? '');
|
|
333
|
+
const intent = String(args['intent'] ?? 'read');
|
|
334
|
+
const params = args['params'];
|
|
335
|
+
const data = args['data'];
|
|
336
|
+
// Action gate for writes
|
|
337
|
+
if (intent === 'write' || intent === 'confirmed_write') {
|
|
338
|
+
if (session.planModeManager.isActive()) {
|
|
339
|
+
return { error: 'Write operations are blocked in plan mode. Present your plan for approval first.' };
|
|
340
|
+
}
|
|
341
|
+
const gateResult = session.runtime.actionGate.evaluate(endpoint, connectionName);
|
|
342
|
+
if (gateResult['decision'] === 'never') {
|
|
343
|
+
return { error: `Write to ${endpoint} is blocked: ${gateResult['reason'] ?? 'policy'}` };
|
|
344
|
+
}
|
|
345
|
+
if (gateResult['decision'] === 'confirm' && intent !== 'confirmed_write') {
|
|
346
|
+
return { error: `Write to ${endpoint} requires confirmation. Re-call with intent: "confirmed_write".` };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return makeApiRequest(session, connectionName, method, endpoint, params, data, signal);
|
|
350
|
+
}
|
|
351
|
+
// Lazy-initialized executor (shared across all custom tool calls)
|
|
352
|
+
let localToolExecutor = null;
|
|
353
|
+
async function executeCustomTool(session, tool, args, signal) {
|
|
354
|
+
// Confirmation gating for tools that require it
|
|
355
|
+
if (tool.confirm === true || tool.confirm === 'review') {
|
|
356
|
+
if (session.planModeManager.isActive()) {
|
|
357
|
+
return { error: 'Custom tool writes are blocked in plan mode. Present your plan for approval first.' };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const ctx = buildToolContext(session, tool, signal);
|
|
361
|
+
if (!localToolExecutor) {
|
|
362
|
+
localToolExecutor = new LocalToolExecutor();
|
|
363
|
+
}
|
|
364
|
+
const executor = session.toolExecutor ?? localToolExecutor;
|
|
365
|
+
try {
|
|
366
|
+
const result = await executor.execute(tool, args, ctx);
|
|
367
|
+
return { output: JSON.stringify(result) };
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
371
|
+
return { error: message };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function executeMcpTool(session, toolName, args) {
|
|
375
|
+
if (!session.mcpManager) {
|
|
376
|
+
return { error: 'MCP is not configured' };
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const result = await session.mcpManager.callTool(toolName, args);
|
|
380
|
+
if (result.isError) {
|
|
381
|
+
const errorText = result.content
|
|
382
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
383
|
+
.map((c) => c.text)
|
|
384
|
+
.join('\n');
|
|
385
|
+
return { error: errorText || 'MCP tool returned an error' };
|
|
386
|
+
}
|
|
387
|
+
const output = result.content
|
|
388
|
+
.map((c) => {
|
|
389
|
+
if (c.type === 'text' && c.text)
|
|
390
|
+
return c.text;
|
|
391
|
+
if (c.type === 'image' && c.data)
|
|
392
|
+
return `[image: ${c.mimeType ?? 'unknown'}]`;
|
|
393
|
+
return `[${c.type}]`;
|
|
394
|
+
})
|
|
395
|
+
.join('\n');
|
|
396
|
+
return { output };
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
400
|
+
return { error: `MCP tool call failed: ${message}` };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function executeShellExecTool(session, args, signal) {
|
|
404
|
+
const command = String(args['command'] ?? '');
|
|
405
|
+
if (!command) {
|
|
406
|
+
return { error: 'Missing required parameter: command' };
|
|
407
|
+
}
|
|
408
|
+
const shellExecutor = session.shellExecutor;
|
|
409
|
+
if (!shellExecutor) {
|
|
410
|
+
return { error: 'Shell execution is not available' };
|
|
411
|
+
}
|
|
412
|
+
const maxTimeout = session.runtime.repo.config.sandbox?.maxTimeout ?? 30000;
|
|
413
|
+
try {
|
|
414
|
+
const result = await shellExecutor.exec(command, maxTimeout, signal);
|
|
415
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
416
|
+
return { output: `Exit code: ${result.exitCode}\n${output}` };
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
if (signal.aborted) {
|
|
420
|
+
return { error: 'Shell execution aborted' };
|
|
421
|
+
}
|
|
422
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Store tools
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
async function executeStorePut(session, store, args) {
|
|
429
|
+
if (!session.storeBackend) {
|
|
430
|
+
return { error: 'Store backend is not configured' };
|
|
431
|
+
}
|
|
432
|
+
// Block writes in plan mode
|
|
433
|
+
if (session.planModeManager.isActive()) {
|
|
434
|
+
return { error: 'Store writes are blocked in plan mode. Present your plan for approval first.' };
|
|
435
|
+
}
|
|
436
|
+
// Resolve key from template
|
|
437
|
+
let key;
|
|
438
|
+
try {
|
|
439
|
+
key = resolveKey(store.entity.key, args);
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const result = await session.storeBackend.put(session.tenantId, store.name, key, args, {});
|
|
446
|
+
return { output: JSON.stringify(result) };
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function executeQueryStore(session, args) {
|
|
453
|
+
if (!session.storeBackend) {
|
|
454
|
+
return { error: 'Store backend is not configured' };
|
|
455
|
+
}
|
|
456
|
+
const storeName = String(args['store'] ?? '');
|
|
457
|
+
if (!storeName) {
|
|
458
|
+
return { error: 'Missing required parameter: store' };
|
|
459
|
+
}
|
|
460
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- tool args from LLM
|
|
461
|
+
const key = args['key'];
|
|
462
|
+
try {
|
|
463
|
+
if (key) {
|
|
464
|
+
// Single document lookup
|
|
465
|
+
const doc = await session.storeBackend.get(session.tenantId, storeName, key);
|
|
466
|
+
if (!doc) {
|
|
467
|
+
return { output: JSON.stringify({ found: false, key }) };
|
|
468
|
+
}
|
|
469
|
+
return { output: JSON.stringify({ found: true, ...doc }) };
|
|
470
|
+
}
|
|
471
|
+
// List with optional filtering
|
|
472
|
+
const result = await session.storeBackend.list(session.tenantId, storeName, {
|
|
473
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
474
|
+
filter: args['filter'],
|
|
475
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- tool args from LLM
|
|
476
|
+
sort: args['sort'],
|
|
477
|
+
limit: typeof args['limit'] === 'number' ? args['limit'] : 20,
|
|
478
|
+
});
|
|
479
|
+
return { output: JSON.stringify(result) };
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Explore tool
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
async function executeExploreTool(session, args, parentToolId, signal) {
|
|
489
|
+
const query = String(args['query'] ?? '');
|
|
490
|
+
const endpointHints = Array.isArray(args['endpoint_hints'])
|
|
491
|
+
? args['endpoint_hints'].filter((h) => typeof h === 'string')
|
|
492
|
+
: undefined;
|
|
493
|
+
const modelOverride = typeof args['model'] === 'string' ? args['model'] : undefined;
|
|
494
|
+
const validationError = validateExploreRequest({ query, endpointHints, parentDepth: 0 }, session.exploreConfig);
|
|
495
|
+
if (validationError) {
|
|
496
|
+
return { result: { error: validationError } };
|
|
497
|
+
}
|
|
498
|
+
// Resolve model override against available models
|
|
499
|
+
const effectiveModel = resolveExploreModel(session.exploreConfig, modelOverride);
|
|
500
|
+
const { result, events, tokensUsed } = await runExploreAgent(session, query, endpointHints, 0, parentToolId, signal, effectiveModel);
|
|
501
|
+
return {
|
|
502
|
+
result,
|
|
503
|
+
subagentEvents: events,
|
|
504
|
+
exploreResult: { summary: result.output ?? '', tokensUsed },
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// Explore sub-agent
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
async function runExploreAgent(session, query, endpointHints, parentDepth, parentToolId, signal, modelOverride) {
|
|
511
|
+
const config = session.exploreConfig;
|
|
512
|
+
const events = [];
|
|
513
|
+
let tokensUsed = 0;
|
|
514
|
+
const effectiveModel = modelOverride ?? config.model;
|
|
515
|
+
let provider;
|
|
516
|
+
try {
|
|
517
|
+
provider = new FailoverProvider(effectiveModel);
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
521
|
+
events.push({
|
|
522
|
+
type: SSEEventType.SubagentEvent,
|
|
523
|
+
parent_tool_id: parentToolId,
|
|
524
|
+
agent_name: 'explore',
|
|
525
|
+
event_type: 'error',
|
|
526
|
+
error: `Provider init failed: ${errMsg}`,
|
|
527
|
+
timestamp: ts(),
|
|
528
|
+
});
|
|
529
|
+
return { result: { error: `Explore provider failed: ${errMsg}` }, events, tokensUsed };
|
|
530
|
+
}
|
|
531
|
+
// Build sub-agent tools: always request (read-only)
|
|
532
|
+
const subTools = [{
|
|
533
|
+
name: 'request',
|
|
534
|
+
description: 'Make HTTP requests to connected systems (read-only).',
|
|
535
|
+
parameters: {
|
|
536
|
+
type: 'object',
|
|
537
|
+
properties: {
|
|
538
|
+
connection: { type: 'string', description: 'Connection name' },
|
|
539
|
+
method: { type: 'string', enum: ['GET'] },
|
|
540
|
+
endpoint: { type: 'string', description: 'API endpoint path' },
|
|
541
|
+
params: { type: 'object', description: 'Query parameters' },
|
|
542
|
+
intent: { type: 'string', enum: ['read'] },
|
|
543
|
+
},
|
|
544
|
+
required: ['connection', 'method', 'endpoint', 'intent'],
|
|
545
|
+
},
|
|
546
|
+
}];
|
|
547
|
+
// Allow nested explore if depth permits
|
|
548
|
+
if (parentDepth + 1 < config.maxDepth) {
|
|
549
|
+
subTools.push({
|
|
550
|
+
name: EXPLORE_TOOL_NAME,
|
|
551
|
+
description: EXPLORE_TOOL_SCHEMA.description,
|
|
552
|
+
parameters: {
|
|
553
|
+
type: 'object',
|
|
554
|
+
properties: {
|
|
555
|
+
query: { type: 'string', description: 'What to investigate' },
|
|
556
|
+
endpoint_hints: {
|
|
557
|
+
type: 'array',
|
|
558
|
+
items: { type: 'string' },
|
|
559
|
+
description: 'Optional endpoint paths to prioritize',
|
|
560
|
+
},
|
|
561
|
+
model: {
|
|
562
|
+
type: 'string',
|
|
563
|
+
description: 'Optional: "simple", "default", "complex", or "provider:model"',
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
required: ['query'],
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
// Build initial message
|
|
571
|
+
const hintsStr = endpointHints?.length ? `\nEndpoint hints: ${endpointHints.join(', ')}` : '';
|
|
572
|
+
const conversation = [
|
|
573
|
+
{ role: 'user', content: `Investigate: ${query}${hintsStr}` },
|
|
574
|
+
];
|
|
575
|
+
let summaryParts = [];
|
|
576
|
+
for (let turn = 0; turn < config.maxTurns; turn++) {
|
|
577
|
+
if (signal.aborted) {
|
|
578
|
+
events.push({
|
|
579
|
+
type: SSEEventType.SubagentEvent,
|
|
580
|
+
parent_tool_id: parentToolId,
|
|
581
|
+
agent_name: 'explore',
|
|
582
|
+
event_type: 'error',
|
|
583
|
+
error: 'Aborted',
|
|
584
|
+
timestamp: ts(),
|
|
585
|
+
});
|
|
586
|
+
return { result: { error: 'Explore sub-agent aborted' }, events, tokensUsed };
|
|
587
|
+
}
|
|
588
|
+
let response;
|
|
589
|
+
try {
|
|
590
|
+
response = await provider.chat({
|
|
591
|
+
model: effectiveModel.model,
|
|
592
|
+
systemPrompt: config.systemPrompt,
|
|
593
|
+
messages: conversation,
|
|
594
|
+
tools: subTools,
|
|
595
|
+
maxTokens: 4096,
|
|
596
|
+
signal,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
601
|
+
events.push({
|
|
602
|
+
type: SSEEventType.SubagentEvent,
|
|
603
|
+
parent_tool_id: parentToolId,
|
|
604
|
+
agent_name: 'explore',
|
|
605
|
+
event_type: 'error',
|
|
606
|
+
error: errMsg,
|
|
607
|
+
timestamp: ts(),
|
|
608
|
+
});
|
|
609
|
+
return { result: { error: `Explore LLM error: ${errMsg}` }, events, tokensUsed };
|
|
610
|
+
}
|
|
611
|
+
if (response.usage) {
|
|
612
|
+
tokensUsed += response.usage.inputTokens + response.usage.outputTokens;
|
|
613
|
+
}
|
|
614
|
+
let hasToolUse = false;
|
|
615
|
+
const toolResults = [];
|
|
616
|
+
for (const block of response.content) {
|
|
617
|
+
if (block.type === 'text') {
|
|
618
|
+
summaryParts.push(block.text);
|
|
619
|
+
events.push({
|
|
620
|
+
type: SSEEventType.SubagentEvent,
|
|
621
|
+
parent_tool_id: parentToolId,
|
|
622
|
+
agent_name: 'explore',
|
|
623
|
+
event_type: 'thought',
|
|
624
|
+
text: block.text,
|
|
625
|
+
timestamp: ts(),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
else if (block.type === 'tool_use') {
|
|
629
|
+
hasToolUse = true;
|
|
630
|
+
events.push({
|
|
631
|
+
type: SSEEventType.SubagentEvent,
|
|
632
|
+
parent_tool_id: parentToolId,
|
|
633
|
+
agent_name: 'explore',
|
|
634
|
+
event_type: 'tool_call_start',
|
|
635
|
+
tool_name: block.name,
|
|
636
|
+
tool_args: block.input,
|
|
637
|
+
timestamp: ts(),
|
|
638
|
+
});
|
|
639
|
+
let toolResult;
|
|
640
|
+
if (block.name === 'request') {
|
|
641
|
+
toolResult = await executeSubAgentRequest(session, block.input, signal);
|
|
642
|
+
}
|
|
643
|
+
else if (block.name === EXPLORE_TOOL_NAME) {
|
|
644
|
+
// Nested explore
|
|
645
|
+
const nestedQuery = String(block.input['query'] ?? '');
|
|
646
|
+
const nestedHints = Array.isArray(block.input['endpoint_hints'])
|
|
647
|
+
? block.input['endpoint_hints'].filter((h) => typeof h === 'string')
|
|
648
|
+
: undefined;
|
|
649
|
+
const nestedModelParam = typeof block.input['model'] === 'string' ? block.input['model'] : undefined;
|
|
650
|
+
const nestedModel = resolveExploreModel(config, nestedModelParam);
|
|
651
|
+
const nested = await runExploreAgent(session, nestedQuery, nestedHints, parentDepth + 1, parentToolId, signal, nestedModel);
|
|
652
|
+
events.push(...nested.events);
|
|
653
|
+
tokensUsed += nested.tokensUsed;
|
|
654
|
+
toolResult = nested.result;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
toolResult = { error: `Unknown tool in explore: ${block.name}` };
|
|
658
|
+
}
|
|
659
|
+
events.push({
|
|
660
|
+
type: SSEEventType.SubagentEvent,
|
|
661
|
+
parent_tool_id: parentToolId,
|
|
662
|
+
agent_name: 'explore',
|
|
663
|
+
event_type: 'tool_call_end',
|
|
664
|
+
tool_name: block.name,
|
|
665
|
+
result: toolResult.error ?? toolResult.output,
|
|
666
|
+
timestamp: ts(),
|
|
667
|
+
});
|
|
668
|
+
toolResults.push({
|
|
669
|
+
role: 'tool_result',
|
|
670
|
+
toolCallId: block.id,
|
|
671
|
+
content: toolResult.error ?? toolResult.output ?? '',
|
|
672
|
+
isError: !!toolResult.error,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
conversation.push({ role: 'assistant', content: response.content });
|
|
677
|
+
if (hasToolUse && toolResults.length > 0) {
|
|
678
|
+
conversation.push(...toolResults);
|
|
679
|
+
summaryParts = []; // Reset — final text is the real summary
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
// No tool use — sub-agent is done
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
const summary = summaryParts.join('\n').trim() || 'No findings.';
|
|
686
|
+
events.push({
|
|
687
|
+
type: SSEEventType.SubagentEvent,
|
|
688
|
+
parent_tool_id: parentToolId,
|
|
689
|
+
agent_name: 'explore',
|
|
690
|
+
event_type: 'complete',
|
|
691
|
+
result: summary,
|
|
692
|
+
timestamp: ts(),
|
|
693
|
+
});
|
|
694
|
+
return { result: { output: summary }, events, tokensUsed };
|
|
695
|
+
}
|
|
696
|
+
async function executeSubAgentRequest(session, args, signal) {
|
|
697
|
+
// Force read-only intent
|
|
698
|
+
const readOnlyArgs = { ...args, intent: 'read', method: 'GET' };
|
|
699
|
+
return executeRequestTool(session, readOnlyArgs, signal);
|
|
700
|
+
}
|
|
701
|
+
function executePlanModeEnter(session, args) {
|
|
702
|
+
const reason = typeof args['reason'] === 'string' ? args['reason'] : undefined;
|
|
703
|
+
session.planModeManager.enter(reason);
|
|
704
|
+
return Promise.resolve({ output: 'Plan mode activated. Present your plan for approval.' });
|
|
705
|
+
}
|
|
706
|
+
function executePlanModeExit(session) {
|
|
707
|
+
session.planModeManager.exit();
|
|
708
|
+
return Promise.resolve({ output: 'Plan mode deactivated.' });
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Process a streaming LLM response, yielding SSE events and accumulating
|
|
712
|
+
* the complete response for conversation history.
|
|
713
|
+
*/
|
|
714
|
+
async function* processStream(stream, session, signal) {
|
|
715
|
+
const content = [];
|
|
716
|
+
const toolResults = [];
|
|
717
|
+
let hasToolUse = false;
|
|
718
|
+
// Accumulate text and tool call data from stream events
|
|
719
|
+
let currentText = '';
|
|
720
|
+
const toolInputBuffers = new Map();
|
|
721
|
+
for await (const event of stream) {
|
|
722
|
+
switch (event.type) {
|
|
723
|
+
case 'text_delta': {
|
|
724
|
+
currentText += event.text;
|
|
725
|
+
const processed = processTextOutput(session, event.text);
|
|
726
|
+
yield { type: SSEEventType.TextDelta, content: processed, timestamp: ts() };
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case 'tool_use_start':
|
|
730
|
+
hasToolUse = true;
|
|
731
|
+
toolInputBuffers.set(event.id, { name: event.name, json: '' });
|
|
732
|
+
yield {
|
|
733
|
+
type: SSEEventType.ToolCallStart,
|
|
734
|
+
tool_name: event.name,
|
|
735
|
+
tool_id: event.id,
|
|
736
|
+
parameters: {},
|
|
737
|
+
timestamp: ts(),
|
|
738
|
+
};
|
|
739
|
+
break;
|
|
740
|
+
case 'tool_use_delta': {
|
|
741
|
+
const buf = toolInputBuffers.get(event.id);
|
|
742
|
+
if (buf) {
|
|
743
|
+
buf.json += event.inputDelta;
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case 'tool_use_end': {
|
|
748
|
+
// Flush any accumulated text
|
|
749
|
+
if (currentText) {
|
|
750
|
+
content.push({ type: 'text', text: currentText });
|
|
751
|
+
currentText = '';
|
|
752
|
+
}
|
|
753
|
+
const toolName = toolInputBuffers.get(event.id)?.name ?? '';
|
|
754
|
+
content.push({ type: 'tool_use', id: event.id, name: toolName, input: event.input });
|
|
755
|
+
toolInputBuffers.delete(event.id);
|
|
756
|
+
// Emit ExploreStart before execution
|
|
757
|
+
if (toolName === EXPLORE_TOOL_NAME) {
|
|
758
|
+
yield {
|
|
759
|
+
type: SSEEventType.ExploreStart,
|
|
760
|
+
query: String(event.input['query'] ?? ''),
|
|
761
|
+
timestamp: ts(),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
// Execute the tool
|
|
765
|
+
const startMs = Date.now();
|
|
766
|
+
const execResult = await executeTool(session, toolName, event.input, event.id, signal);
|
|
767
|
+
const durationMs = Date.now() - startMs;
|
|
768
|
+
// Emit subagent events from explore
|
|
769
|
+
if (execResult.subagentEvents) {
|
|
770
|
+
for (const evt of execResult.subagentEvents) {
|
|
771
|
+
yield evt;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// Emit ExploreEnd after execution
|
|
775
|
+
if (toolName === EXPLORE_TOOL_NAME && execResult.exploreResult) {
|
|
776
|
+
yield {
|
|
777
|
+
type: SSEEventType.ExploreEnd,
|
|
778
|
+
summary: execResult.exploreResult.summary,
|
|
779
|
+
tokens_used: execResult.exploreResult.tokensUsed,
|
|
780
|
+
timestamp: ts(),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
yield {
|
|
784
|
+
type: SSEEventType.ToolCallResult,
|
|
785
|
+
tool_id: event.id,
|
|
786
|
+
status: execResult.result.error ? 'error' : 'success',
|
|
787
|
+
result: execResult.result.output,
|
|
788
|
+
error: execResult.result.error,
|
|
789
|
+
duration_ms: durationMs,
|
|
790
|
+
timestamp: ts(),
|
|
791
|
+
};
|
|
792
|
+
toolResults.push({
|
|
793
|
+
role: 'tool_result',
|
|
794
|
+
toolCallId: event.id,
|
|
795
|
+
content: execResult.result.error ?? execResult.result.output ?? '',
|
|
796
|
+
isError: !!execResult.result.error,
|
|
797
|
+
});
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case 'message_end':
|
|
801
|
+
// Flush any remaining text
|
|
802
|
+
if (currentText) {
|
|
803
|
+
content.push({ type: 'text', text: currentText });
|
|
804
|
+
currentText = '';
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
default:
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// Flush any remaining text not flushed by message_end
|
|
812
|
+
if (currentText) {
|
|
813
|
+
content.push({ type: 'text', text: currentText });
|
|
814
|
+
}
|
|
815
|
+
return { content, hasToolUse, toolResults };
|
|
816
|
+
}
|
|
817
|
+
function processTextOutput(session, text) {
|
|
818
|
+
const result = session.runtime.outputPipeline.process(text);
|
|
819
|
+
session.runtime.telemetry.logGuard({
|
|
820
|
+
output: result.output,
|
|
821
|
+
modified: result.modified,
|
|
822
|
+
blocked: result.blocked,
|
|
823
|
+
findings: result.findings,
|
|
824
|
+
});
|
|
825
|
+
return result.output;
|
|
826
|
+
}
|
|
827
|
+
//# sourceMappingURL=agent-runner.js.map
|