moderado 0.3.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/README.md +208 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +108 -0
- package/dist/args.js.map +1 -0
- package/dist/commands/chat.d.ts +121 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +1484 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +61 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +27 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +172 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/interactive_menu.d.ts +3 -0
- package/dist/commands/interactive_menu.d.ts.map +1 -0
- package/dist/commands/interactive_menu.js +98 -0
- package/dist/commands/interactive_menu.js.map +1 -0
- package/dist/commands/models.d.ts +8 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/commands/models.js +81 -0
- package/dist/commands/models.js.map +1 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +140 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +23 -0
- package/dist/credentials.js.map +1 -0
- package/dist/diagnostic_log.d.ts +2 -0
- package/dist/diagnostic_log.d.ts.map +1 -0
- package/dist/diagnostic_log.js +21 -0
- package/dist/diagnostic_log.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/model_pricing.d.ts +4 -0
- package/dist/model_pricing.d.ts.map +1 -0
- package/dist/model_pricing.js +10 -0
- package/dist/model_pricing.js.map +1 -0
- package/dist/sessions.d.ts +203 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +82 -0
- package/dist/sessions.js.map +1 -0
- package/dist/ui/box.d.ts +14 -0
- package/dist/ui/box.d.ts.map +1 -0
- package/dist/ui/box.js +52 -0
- package/dist/ui/box.js.map +1 -0
- package/dist/ui/file_mentions.d.ts +38 -0
- package/dist/ui/file_mentions.d.ts.map +1 -0
- package/dist/ui/file_mentions.js +119 -0
- package/dist/ui/file_mentions.js.map +1 -0
- package/dist/ui/help_modal.d.ts +2 -0
- package/dist/ui/help_modal.d.ts.map +1 -0
- package/dist/ui/help_modal.js +101 -0
- package/dist/ui/help_modal.js.map +1 -0
- package/dist/ui/model_selector.d.ts +48 -0
- package/dist/ui/model_selector.d.ts.map +1 -0
- package/dist/ui/model_selector.js +829 -0
- package/dist/ui/model_selector.js.map +1 -0
- package/dist/ui/popup.d.ts +78 -0
- package/dist/ui/popup.d.ts.map +1 -0
- package/dist/ui/popup.js +274 -0
- package/dist/ui/popup.js.map +1 -0
- package/dist/ui/prompt.d.ts +21 -0
- package/dist/ui/prompt.d.ts.map +1 -0
- package/dist/ui/prompt.js +128 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/ui/provider_connect.d.ts +33 -0
- package/dist/ui/provider_connect.d.ts.map +1 -0
- package/dist/ui/provider_connect.js +175 -0
- package/dist/ui/provider_connect.js.map +1 -0
- package/dist/ui/renderer.d.ts +19 -0
- package/dist/ui/renderer.d.ts.map +1 -0
- package/dist/ui/renderer.js +147 -0
- package/dist/ui/renderer.js.map +1 -0
- package/dist/ui/session_screen.d.ts +32 -0
- package/dist/ui/session_screen.d.ts.map +1 -0
- package/dist/ui/session_screen.js +148 -0
- package/dist/ui/session_screen.js.map +1 -0
- package/dist/ui/session_terminal.d.ts +46 -0
- package/dist/ui/session_terminal.d.ts.map +1 -0
- package/dist/ui/session_terminal.js +342 -0
- package/dist/ui/session_terminal.js.map +1 -0
- package/dist/ui/terminal_approval.d.ts +14 -0
- package/dist/ui/terminal_approval.d.ts.map +1 -0
- package/dist/ui/terminal_approval.js +98 -0
- package/dist/ui/terminal_approval.js.map +1 -0
- package/dist/ui/welcome.d.ts +130 -0
- package/dist/ui/welcome.d.ts.map +1 -0
- package/dist/ui/welcome.js +990 -0
- package/dist/ui/welcome.js.map +1 -0
- package/dist/vendor/contracts/approvals.d.ts +91 -0
- package/dist/vendor/contracts/approvals.d.ts.map +1 -0
- package/dist/vendor/contracts/approvals.js +22 -0
- package/dist/vendor/contracts/approvals.js.map +1 -0
- package/dist/vendor/contracts/events.d.ts +1215 -0
- package/dist/vendor/contracts/events.d.ts.map +1 -0
- package/dist/vendor/contracts/events.js +94 -0
- package/dist/vendor/contracts/events.js.map +1 -0
- package/dist/vendor/contracts/index.d.ts +7 -0
- package/dist/vendor/contracts/index.d.ts.map +1 -0
- package/dist/vendor/contracts/index.js +7 -0
- package/dist/vendor/contracts/index.js.map +1 -0
- package/dist/vendor/contracts/messages.d.ts +181 -0
- package/dist/vendor/contracts/messages.d.ts.map +1 -0
- package/dist/vendor/contracts/messages.js +40 -0
- package/dist/vendor/contracts/messages.js.map +1 -0
- package/dist/vendor/contracts/models.d.ts +117 -0
- package/dist/vendor/contracts/models.d.ts.map +1 -0
- package/dist/vendor/contracts/models.js +37 -0
- package/dist/vendor/contracts/models.js.map +1 -0
- package/dist/vendor/contracts/provider.d.ts +121 -0
- package/dist/vendor/contracts/provider.d.ts.map +1 -0
- package/dist/vendor/contracts/provider.js +64 -0
- package/dist/vendor/contracts/provider.js.map +1 -0
- package/dist/vendor/contracts/tools.d.ts +305 -0
- package/dist/vendor/contracts/tools.d.ts.map +1 -0
- package/dist/vendor/contracts/tools.js +65 -0
- package/dist/vendor/contracts/tools.js.map +1 -0
- package/dist/vendor/core/agent.d.ts +39 -0
- package/dist/vendor/core/agent.d.ts.map +1 -0
- package/dist/vendor/core/agent.js +710 -0
- package/dist/vendor/core/agent.js.map +1 -0
- package/dist/vendor/core/host_event_stream.d.ts +9 -0
- package/dist/vendor/core/host_event_stream.d.ts.map +1 -0
- package/dist/vendor/core/host_event_stream.js +12 -0
- package/dist/vendor/core/host_event_stream.js.map +1 -0
- package/dist/vendor/core/index.d.ts +7 -0
- package/dist/vendor/core/index.d.ts.map +1 -0
- package/dist/vendor/core/index.js +7 -0
- package/dist/vendor/core/index.js.map +1 -0
- package/dist/vendor/core/policy.d.ts +19 -0
- package/dist/vendor/core/policy.d.ts.map +1 -0
- package/dist/vendor/core/policy.js +31 -0
- package/dist/vendor/core/policy.js.map +1 -0
- package/dist/vendor/core/router.d.ts +26 -0
- package/dist/vendor/core/router.d.ts.map +1 -0
- package/dist/vendor/core/router.js +222 -0
- package/dist/vendor/core/router.js.map +1 -0
- package/dist/vendor/core/subagent.d.ts +29 -0
- package/dist/vendor/core/subagent.d.ts.map +1 -0
- package/dist/vendor/core/subagent.js +66 -0
- package/dist/vendor/core/subagent.js.map +1 -0
- package/dist/vendor/core/think_filter.d.ts +14 -0
- package/dist/vendor/core/think_filter.d.ts.map +1 -0
- package/dist/vendor/core/think_filter.js +136 -0
- package/dist/vendor/core/think_filter.js.map +1 -0
- package/dist/vendor/providers/fake/fake_provider.d.ts +16 -0
- package/dist/vendor/providers/fake/fake_provider.d.ts.map +1 -0
- package/dist/vendor/providers/fake/fake_provider.js +89 -0
- package/dist/vendor/providers/fake/fake_provider.js.map +1 -0
- package/dist/vendor/providers/index.d.ts +6 -0
- package/dist/vendor/providers/index.d.ts.map +1 -0
- package/dist/vendor/providers/index.js +6 -0
- package/dist/vendor/providers/index.js.map +1 -0
- package/dist/vendor/providers/model_discovery.d.ts +28 -0
- package/dist/vendor/providers/model_discovery.d.ts.map +1 -0
- package/dist/vendor/providers/model_discovery.js +106 -0
- package/dist/vendor/providers/model_discovery.js.map +1 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts +23 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.js +197 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -0
- package/dist/vendor/providers/nvidia/sse_parser.d.ts +3 -0
- package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -0
- package/dist/vendor/providers/nvidia/sse_parser.js +84 -0
- package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -0
- package/dist/vendor/providers/openai_compatible_adapter.d.ts +10 -0
- package/dist/vendor/providers/openai_compatible_adapter.d.ts.map +1 -0
- package/dist/vendor/providers/openai_compatible_adapter.js +8 -0
- package/dist/vendor/providers/openai_compatible_adapter.js.map +1 -0
- package/dist/vendor/tools/checkpoints.d.ts +27 -0
- package/dist/vendor/tools/checkpoints.d.ts.map +1 -0
- package/dist/vendor/tools/checkpoints.js +145 -0
- package/dist/vendor/tools/checkpoints.js.map +1 -0
- package/dist/vendor/tools/diagnostics.d.ts +11 -0
- package/dist/vendor/tools/diagnostics.d.ts.map +1 -0
- package/dist/vendor/tools/diagnostics.js +16 -0
- package/dist/vendor/tools/diagnostics.js.map +1 -0
- package/dist/vendor/tools/diff.d.ts +7 -0
- package/dist/vendor/tools/diff.d.ts.map +1 -0
- package/dist/vendor/tools/diff.js +48 -0
- package/dist/vendor/tools/diff.js.map +1 -0
- package/dist/vendor/tools/errors.d.ts +17 -0
- package/dist/vendor/tools/errors.d.ts.map +1 -0
- package/dist/vendor/tools/errors.js +33 -0
- package/dist/vendor/tools/errors.js.map +1 -0
- package/dist/vendor/tools/git_workspace.d.ts +14 -0
- package/dist/vendor/tools/git_workspace.d.ts.map +1 -0
- package/dist/vendor/tools/git_workspace.js +42 -0
- package/dist/vendor/tools/git_workspace.js.map +1 -0
- package/dist/vendor/tools/index.d.ts +22 -0
- package/dist/vendor/tools/index.d.ts.map +1 -0
- package/dist/vendor/tools/index.js +22 -0
- package/dist/vendor/tools/index.js.map +1 -0
- package/dist/vendor/tools/jail.d.ts +17 -0
- package/dist/vendor/tools/jail.d.ts.map +1 -0
- package/dist/vendor/tools/jail.js +221 -0
- package/dist/vendor/tools/jail.js.map +1 -0
- package/dist/vendor/tools/lsp.d.ts +3 -0
- package/dist/vendor/tools/lsp.d.ts.map +1 -0
- package/dist/vendor/tools/lsp.js +43 -0
- package/dist/vendor/tools/lsp.js.map +1 -0
- package/dist/vendor/tools/mcp.d.ts +9 -0
- package/dist/vendor/tools/mcp.d.ts.map +1 -0
- package/dist/vendor/tools/mcp.js +54 -0
- package/dist/vendor/tools/mcp.js.map +1 -0
- package/dist/vendor/tools/mcp_adapter.d.ts +15 -0
- package/dist/vendor/tools/mcp_adapter.d.ts.map +1 -0
- package/dist/vendor/tools/mcp_adapter.js +29 -0
- package/dist/vendor/tools/mcp_adapter.js.map +1 -0
- package/dist/vendor/tools/registry.d.ts +10 -0
- package/dist/vendor/tools/registry.d.ts.map +1 -0
- package/dist/vendor/tools/registry.js +126 -0
- package/dist/vendor/tools/registry.js.map +1 -0
- package/dist/vendor/tools/tools/apply_patch.d.ts +3 -0
- package/dist/vendor/tools/tools/apply_patch.d.ts.map +1 -0
- package/dist/vendor/tools/tools/apply_patch.js +49 -0
- package/dist/vendor/tools/tools/apply_patch.js.map +1 -0
- package/dist/vendor/tools/tools/edit_file.d.ts +3 -0
- package/dist/vendor/tools/tools/edit_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/edit_file.js +71 -0
- package/dist/vendor/tools/tools/edit_file.js.map +1 -0
- package/dist/vendor/tools/tools/git_diff.d.ts +3 -0
- package/dist/vendor/tools/tools/git_diff.d.ts.map +1 -0
- package/dist/vendor/tools/tools/git_diff.js +91 -0
- package/dist/vendor/tools/tools/git_diff.js.map +1 -0
- package/dist/vendor/tools/tools/language_intelligence.d.ts +4 -0
- package/dist/vendor/tools/tools/language_intelligence.d.ts.map +1 -0
- package/dist/vendor/tools/tools/language_intelligence.js +13 -0
- package/dist/vendor/tools/tools/language_intelligence.js.map +1 -0
- package/dist/vendor/tools/tools/list_files.d.ts +9 -0
- package/dist/vendor/tools/tools/list_files.d.ts.map +1 -0
- package/dist/vendor/tools/tools/list_files.js +165 -0
- package/dist/vendor/tools/tools/list_files.js.map +1 -0
- package/dist/vendor/tools/tools/read_file.d.ts +3 -0
- package/dist/vendor/tools/tools/read_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/read_file.js +72 -0
- package/dist/vendor/tools/tools/read_file.js.map +1 -0
- package/dist/vendor/tools/tools/run_command.d.ts +12 -0
- package/dist/vendor/tools/tools/run_command.d.ts.map +1 -0
- package/dist/vendor/tools/tools/run_command.js +421 -0
- package/dist/vendor/tools/tools/run_command.js.map +1 -0
- package/dist/vendor/tools/tools/run_diagnostics.d.ts +3 -0
- package/dist/vendor/tools/tools/run_diagnostics.d.ts.map +1 -0
- package/dist/vendor/tools/tools/run_diagnostics.js +11 -0
- package/dist/vendor/tools/tools/run_diagnostics.js.map +1 -0
- package/dist/vendor/tools/tools/search_files.d.ts +3 -0
- package/dist/vendor/tools/tools/search_files.d.ts.map +1 -0
- package/dist/vendor/tools/tools/search_files.js +120 -0
- package/dist/vendor/tools/tools/search_files.js.map +1 -0
- package/dist/vendor/tools/tools/web_search.d.ts +46 -0
- package/dist/vendor/tools/tools/web_search.d.ts.map +1 -0
- package/dist/vendor/tools/tools/web_search.js +187 -0
- package/dist/vendor/tools/tools/web_search.js.map +1 -0
- package/dist/vendor/tools/tools/write_file.d.ts +3 -0
- package/dist/vendor/tools/tools/write_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/write_file.js +52 -0
- package/dist/vendor/tools/tools/write_file.js.map +1 -0
- package/dist/windows_credentials.d.ts +15 -0
- package/dist/windows_credentials.d.ts.map +1 -0
- package/dist/windows_credentials.js +23 -0
- package/dist/windows_credentials.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EmptyResponseError, ModelUnavailableError, RateLimitError, } from '../contracts/index.js';
|
|
4
|
+
import { Router } from './router.js';
|
|
5
|
+
import { PolicyManager } from './policy.js';
|
|
6
|
+
import { HostEventStream } from './host_event_stream.js';
|
|
7
|
+
import { SubagentDelegator } from './subagent.js';
|
|
8
|
+
import { ThinkTagStreamFilter } from './think_filter.js';
|
|
9
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
10
|
+
export const DEFAULT_SYSTEM_PROMPT = `CRITICAL DIRECTIVE — EXTREME BREVITY (DEFAULT MODE):
|
|
11
|
+
- Extreme brevity is mandatory. Answer in 1 to 2 short sentences or under 35 words.
|
|
12
|
+
- Zero conversational filler: Never output greetings, pleasantries, preambles ("Sure", "Here is", "Certainly", "I'd be happy to"), or sign-offs ("Hope this helps", "Let me know").
|
|
13
|
+
- Never restate, rephrase, or echo the user's question before answering. Start immediately with the direct answer.
|
|
14
|
+
- Zero markdown headers (no ## or ###), no bullet lists unless specifically requested, no conclusion sections.
|
|
15
|
+
- For code, commands, or file edits: Output ONLY the raw code or command block. Do NOT explain what it does or how it works.
|
|
16
|
+
- State only direct, factual answers. Do NOT provide unsolicited background, tips, or commentary.
|
|
17
|
+
|
|
18
|
+
You are Moderado, a lightweight, pragmatic, bloat-free AI coding agent.
|
|
19
|
+
You follow the Ponytail Decision Ladder: YAGNI, standard library first, zero unnecessary dependencies, and minimal code.
|
|
20
|
+
Use the provided workspace tools to inspect, read, search, modify, and test files within the workspace.
|
|
21
|
+
|
|
22
|
+
CORE OPERATIONAL RULES:
|
|
23
|
+
- If the user prompt is a greeting, question, explanation request, or conversational query, output regular markdown text directly without calling any tools.
|
|
24
|
+
- ONLY invoke tools when explicitly needed to inspect or modify the workspace as requested by the user.
|
|
25
|
+
- NEVER create, write, or overwrite any files (including documentation, project summaries, or boilerplate) unless the user explicitly commanded you to create or modify that file in their prompt.
|
|
26
|
+
- Do NOT create unsolicited files on your own initiative.
|
|
27
|
+
- Always inspect existing code (with list_files, read_file, search_files) before editing. Keep edits focused, clean, and test-driven.
|
|
28
|
+
- Do NOT invent tool names.
|
|
29
|
+
- For anything that changes over time (weather, news, scores, prices, schedules, releases), call the web_search tool immediately with a clear query. Never fetch this with run_command, and never ask the user to look it up themselves. Answer with the facts and values only, in the fewest readable lines, without listing sources or URLs.`;
|
|
30
|
+
export function buildSystemPrompt(modelId, workspaceRoot) {
|
|
31
|
+
return `${DEFAULT_SYSTEM_PROMPT}
|
|
32
|
+
|
|
33
|
+
SYSTEM RUNTIME CONTEXT:
|
|
34
|
+
- Active Model: ${modelId}
|
|
35
|
+
- Workspace Root: ${workspaceRoot}`;
|
|
36
|
+
}
|
|
37
|
+
export function cleanConversationalFiller(text) {
|
|
38
|
+
if (!text)
|
|
39
|
+
return text;
|
|
40
|
+
let cleaned = text.trim();
|
|
41
|
+
// Strip complete or unclosed thinking/reasoning blocks
|
|
42
|
+
cleaned = cleaned.replace(/<\s*(?:think|thought|reasoning)(?:\s+[^>]*)?>[\s\S]*?<\/\s*(?:think|thought|reasoning)\s*>\s*/gi, '');
|
|
43
|
+
cleaned = cleaned.replace(/<\s*(?:think|thought|reasoning)(?:\s+[^>]*)?>[\s\S]*$/gi, '');
|
|
44
|
+
// Strip untagged thinking process blocks (either extract subsequent answer or clear if unclosed)
|
|
45
|
+
if (/^(?:Here's\s+(?:a\s+|my\s+)?thinking\s+process|Thinking\s+process)/i.test(cleaned)) {
|
|
46
|
+
const parts = cleaned.split(/\r?\n\r?\n/);
|
|
47
|
+
const answerIndex = parts.findIndex((p, idx) => idx > 0 && !/^\s*(?:\d+\.|\*|-)/.test(p));
|
|
48
|
+
cleaned = answerIndex !== -1 ? parts.slice(answerIndex).join('\n\n').trim() : '';
|
|
49
|
+
}
|
|
50
|
+
// Strip common multi-line leading filler
|
|
51
|
+
cleaned = cleaned.replace(/^(?:Sure(?: thing)?[!,.]?|Certainly[!,.]?|Of course[!,.]?|Here is[^\n:]*[:.]?|Here's[^\n:]*[:.]?|I would be happy to[^\n:]*[:.]?|I'd be happy to[^\n:]*[:.]?|Great[!,.]?|Okay[!,.]?|Alright[!,.]?)\s*(?:\r?\n)+/i, '');
|
|
52
|
+
// Strip single-line leading filler prefix like "Sure! Here is the answer: " or "Sure, ..."
|
|
53
|
+
cleaned = cleaned.replace(/^(?:Sure(?: thing)?[!,.]?|Certainly[!,.]?|Of course[!,.]?|I'd be happy to help[!,.]?)\s+(?:Here (?:is|are)[^:\n]*:\s*)?/i, '');
|
|
54
|
+
// Strip common trailing sign-offs
|
|
55
|
+
cleaned = cleaned.replace(/(?:\r?\n)+(?:Hope this helps[^\n]*|Let me know if (?:you need|you have)[^\n]*|Feel free to ask[^\n]*)\.?\s*$/i, '');
|
|
56
|
+
return cleaned.trim();
|
|
57
|
+
}
|
|
58
|
+
const PSEUDO_ANSWER_TOOLS = new Set([
|
|
59
|
+
'answer_directly',
|
|
60
|
+
'answer',
|
|
61
|
+
'respond',
|
|
62
|
+
'response',
|
|
63
|
+
'chat',
|
|
64
|
+
'message',
|
|
65
|
+
'final_answer',
|
|
66
|
+
'reply',
|
|
67
|
+
'subagent',
|
|
68
|
+
]);
|
|
69
|
+
const SUBAGENT_DECLARATION = {
|
|
70
|
+
name: 'subagent',
|
|
71
|
+
description: 'Delegate one focused sub-task to a bounded child agent that follows the same workspace and approval policies.',
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
detail: { type: 'string' },
|
|
76
|
+
},
|
|
77
|
+
required: ['detail'],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const SubagentDetailSchema = z.object({
|
|
81
|
+
detail: z.string().trim().min(1).max(2000),
|
|
82
|
+
});
|
|
83
|
+
export class AgentLoop {
|
|
84
|
+
async run(task, options) {
|
|
85
|
+
const hostStream = options.hostEventListener ? new HostEventStream(options.hostEventListener) : undefined;
|
|
86
|
+
const emit = (event) => { options.eventListener?.(event); hostStream?.emit(event); };
|
|
87
|
+
const policy = options.policy ?? new PolicyManager();
|
|
88
|
+
const router = options.router ?? new Router();
|
|
89
|
+
const signal = options.signal;
|
|
90
|
+
const delegator = new SubagentDelegator(options.workspaceRoot, options.tools, options.approvalHandler, (event) => emit(event), policy, options.onMutationApproved, options.onMutationCompleted);
|
|
91
|
+
let latestUsage;
|
|
92
|
+
if (signal?.aborted) {
|
|
93
|
+
emit({ type: 'cancellation', reason: 'Aborted by user', timestamp: Date.now() });
|
|
94
|
+
return {
|
|
95
|
+
status: 'cancelled',
|
|
96
|
+
totalSteps: 0,
|
|
97
|
+
finalMessage: null,
|
|
98
|
+
selectedModel: {
|
|
99
|
+
id: 'aborted',
|
|
100
|
+
ownedBy: 'system',
|
|
101
|
+
classification: {
|
|
102
|
+
modelId: 'aborted',
|
|
103
|
+
accessTier: 'unknown',
|
|
104
|
+
toolSupport: 'unknown',
|
|
105
|
+
source: 'heuristic',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
messages: options.conversationHistory ? [...options.conversationHistory] : [],
|
|
109
|
+
usage: latestUsage,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// 1. Model Discovery & Routing
|
|
113
|
+
let inventory = options.modelInventory;
|
|
114
|
+
if (!inventory || inventory.length === 0) {
|
|
115
|
+
if (options.routeOptions?.pinnedModelId) {
|
|
116
|
+
try {
|
|
117
|
+
inventory = await options.provider.discoverModels(signal);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A pinned model can still be usable when its provider does not
|
|
121
|
+
// expose a model catalogue. Continue without dynamic metadata.
|
|
122
|
+
inventory = [{ id: options.routeOptions.pinnedModelId, object: 'model', owned_by: options.provider.id }];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
emit({
|
|
127
|
+
type: 'progress',
|
|
128
|
+
step: 0,
|
|
129
|
+
maxSteps: policy.maxSteps,
|
|
130
|
+
status: 'Discovering and selecting models...',
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
});
|
|
133
|
+
inventory = await options.provider.discoverModels(signal);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const { selectedModel: initialModel, rankedCandidates } = router.selectModel(inventory, options.routeOptions);
|
|
137
|
+
let currentModel = initialModel;
|
|
138
|
+
emit({
|
|
139
|
+
type: 'model_change',
|
|
140
|
+
newModelId: currentModel.id,
|
|
141
|
+
reason: options.routeOptions?.pinnedModelId ? 'user_pinned' : 'initial_selection',
|
|
142
|
+
accessClass: currentModel.classification.accessTier,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
});
|
|
145
|
+
// 2. Initialize Conversation Context
|
|
146
|
+
const systemPromptMessage = {
|
|
147
|
+
role: 'system',
|
|
148
|
+
content: buildSystemPrompt(currentModel.id, options.workspaceRoot),
|
|
149
|
+
};
|
|
150
|
+
let baseHistory = options.conversationHistory ? [...options.conversationHistory] : [];
|
|
151
|
+
// Ensure system prompt is at the head and updated with current runtime context
|
|
152
|
+
if (baseHistory.length > 0 && baseHistory[0].role === 'system') {
|
|
153
|
+
baseHistory[0] = systemPromptMessage;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
baseHistory = [systemPromptMessage, ...baseHistory];
|
|
157
|
+
}
|
|
158
|
+
// Layer 4: Truncate oversized tool outputs from older turns in history to prevent token ballooning
|
|
159
|
+
const messages = [
|
|
160
|
+
...baseHistory.map((msg, idx) => {
|
|
161
|
+
if (msg.role === 'tool' && idx < baseHistory.length - 1 && typeof msg.content === 'string' && msg.content.length > 1500) {
|
|
162
|
+
return {
|
|
163
|
+
...msg,
|
|
164
|
+
content: msg.content.slice(0, 1500) + '\n... [earlier tool output truncated for token efficiency]',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return msg;
|
|
168
|
+
}),
|
|
169
|
+
{ role: 'user', content: task },
|
|
170
|
+
];
|
|
171
|
+
let step = 0;
|
|
172
|
+
let finalAssistantText = null;
|
|
173
|
+
while (policy.isStepWithinLimit(step)) {
|
|
174
|
+
if (signal?.aborted) {
|
|
175
|
+
emit({ type: 'cancellation', reason: 'Aborted by user', timestamp: Date.now() });
|
|
176
|
+
return {
|
|
177
|
+
status: 'cancelled',
|
|
178
|
+
totalSteps: step,
|
|
179
|
+
finalMessage: finalAssistantText,
|
|
180
|
+
selectedModel: currentModel,
|
|
181
|
+
messages,
|
|
182
|
+
usage: latestUsage,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
step++;
|
|
186
|
+
emit({
|
|
187
|
+
type: 'progress',
|
|
188
|
+
step,
|
|
189
|
+
maxSteps: policy.maxSteps,
|
|
190
|
+
status: `Step ${step}/${policy.maxSteps}: Inferring next action with ${currentModel.id}...`,
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
// 3. Inference with Streaming
|
|
194
|
+
let assistantText = '';
|
|
195
|
+
const toolCallDeltas = new Map();
|
|
196
|
+
try {
|
|
197
|
+
const stream = options.provider.streamChat({
|
|
198
|
+
modelId: currentModel.id,
|
|
199
|
+
messages,
|
|
200
|
+
tools: currentModel.classification.toolSupport === 'unsupported'
|
|
201
|
+
? undefined
|
|
202
|
+
: [
|
|
203
|
+
...options.tools.getDeclarations(),
|
|
204
|
+
...(options.allowSubagentDelegation === false ? [] : [SUBAGENT_DECLARATION]),
|
|
205
|
+
],
|
|
206
|
+
maxTokens: options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
207
|
+
signal,
|
|
208
|
+
});
|
|
209
|
+
const thinkFilter = new ThinkTagStreamFilter();
|
|
210
|
+
for await (const chunk of stream) {
|
|
211
|
+
if (signal?.aborted) {
|
|
212
|
+
emit({ type: 'cancellation', reason: 'Aborted by user', timestamp: Date.now() });
|
|
213
|
+
return {
|
|
214
|
+
status: 'cancelled',
|
|
215
|
+
totalSteps: step,
|
|
216
|
+
finalMessage: finalAssistantText,
|
|
217
|
+
selectedModel: currentModel,
|
|
218
|
+
messages,
|
|
219
|
+
usage: latestUsage,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (chunk.reasoningDelta) {
|
|
223
|
+
emit({
|
|
224
|
+
type: 'reasoning_delta',
|
|
225
|
+
delta: chunk.reasoningDelta,
|
|
226
|
+
timestamp: Date.now(),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (chunk.contentDelta) {
|
|
230
|
+
const filtered = thinkFilter.process(chunk.contentDelta);
|
|
231
|
+
for (const item of filtered) {
|
|
232
|
+
if (item.type === 'reasoning') {
|
|
233
|
+
emit({
|
|
234
|
+
type: 'reasoning_delta',
|
|
235
|
+
delta: item.delta,
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
assistantText += item.delta;
|
|
241
|
+
emit({
|
|
242
|
+
type: 'assistant_delta',
|
|
243
|
+
delta: item.delta,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (chunk.usage) {
|
|
250
|
+
latestUsage = chunk.usage;
|
|
251
|
+
}
|
|
252
|
+
if (chunk.toolCallChunks) {
|
|
253
|
+
for (const delta of chunk.toolCallChunks) {
|
|
254
|
+
const current = toolCallDeltas.get(delta.index) ?? { args: '' };
|
|
255
|
+
if (delta.id)
|
|
256
|
+
current.id = delta.id;
|
|
257
|
+
if (delta.name)
|
|
258
|
+
current.name = delta.name;
|
|
259
|
+
if (delta.argumentsDelta)
|
|
260
|
+
current.args += delta.argumentsDelta;
|
|
261
|
+
toolCallDeltas.set(delta.index, current);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const item of thinkFilter.flush()) {
|
|
266
|
+
if (item.type === 'reasoning') {
|
|
267
|
+
emit({
|
|
268
|
+
type: 'reasoning_delta',
|
|
269
|
+
delta: item.delta,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
assistantText += item.delta;
|
|
275
|
+
emit({
|
|
276
|
+
type: 'assistant_delta',
|
|
277
|
+
delta: item.delta,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
// Handle transient errors & failover cascades in AUTO mode
|
|
285
|
+
const isTransient = err instanceof RateLimitError || err instanceof ModelUnavailableError;
|
|
286
|
+
const isAutoMode = !options.routeOptions?.pinnedModelId;
|
|
287
|
+
if (isTransient && isAutoMode) {
|
|
288
|
+
const fallback = router.getNextFallback(rankedCandidates, currentModel.id);
|
|
289
|
+
if (fallback) {
|
|
290
|
+
emit({
|
|
291
|
+
type: 'model_change',
|
|
292
|
+
previousModelId: currentModel.id,
|
|
293
|
+
newModelId: fallback.id,
|
|
294
|
+
reason: err instanceof RateLimitError ? 'fallback_rate_limit' : 'fallback_unavailable',
|
|
295
|
+
accessClass: fallback.classification.accessTier,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
});
|
|
298
|
+
currentModel = fallback;
|
|
299
|
+
step--; // Retry current step without consuming step limit
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
emit({
|
|
304
|
+
type: 'error',
|
|
305
|
+
code: err.code ?? 'ERR_INFERENCE_FAILED',
|
|
306
|
+
message: err.message,
|
|
307
|
+
recoverable: false,
|
|
308
|
+
timestamp: Date.now(),
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
status: 'failed',
|
|
312
|
+
totalSteps: step,
|
|
313
|
+
finalMessage: null,
|
|
314
|
+
selectedModel: currentModel,
|
|
315
|
+
messages,
|
|
316
|
+
usage: latestUsage,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
finalAssistantText = cleanConversationalFiller(assistantText) || null;
|
|
320
|
+
// 4. Assemble Completed Tool Calls
|
|
321
|
+
const completedToolCalls = [];
|
|
322
|
+
for (const [, delta] of Array.from(toolCallDeltas.entries()).sort(([a], [b]) => a - b)) {
|
|
323
|
+
if (delta.name) {
|
|
324
|
+
let parsedArgs = {};
|
|
325
|
+
try {
|
|
326
|
+
parsedArgs = delta.args ? JSON.parse(delta.args) : {};
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
parsedArgs = { _raw: delta.args };
|
|
330
|
+
}
|
|
331
|
+
completedToolCalls.push({
|
|
332
|
+
id: delta.id ?? `call_${crypto.randomUUID()}`,
|
|
333
|
+
name: delta.name,
|
|
334
|
+
arguments: parsedArgs,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (!assistantText.trim() && completedToolCalls.length === 0) {
|
|
339
|
+
const emptyResponse = new EmptyResponseError(`${options.provider.name} returned no assistant content or tool calls for ${currentModel.id}.`);
|
|
340
|
+
const isAutoMode = !options.routeOptions?.pinnedModelId;
|
|
341
|
+
const fallback = isAutoMode
|
|
342
|
+
? router.getNextFallback(rankedCandidates, currentModel.id)
|
|
343
|
+
: undefined;
|
|
344
|
+
if (fallback) {
|
|
345
|
+
emit({
|
|
346
|
+
type: 'model_change',
|
|
347
|
+
previousModelId: currentModel.id,
|
|
348
|
+
newModelId: fallback.id,
|
|
349
|
+
reason: 'fallback_unavailable',
|
|
350
|
+
accessClass: fallback.classification.accessTier,
|
|
351
|
+
timestamp: Date.now(),
|
|
352
|
+
});
|
|
353
|
+
currentModel = fallback;
|
|
354
|
+
step--;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
emit({
|
|
358
|
+
type: 'error',
|
|
359
|
+
code: emptyResponse.code,
|
|
360
|
+
message: emptyResponse.message,
|
|
361
|
+
recoverable: false,
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
status: 'failed',
|
|
366
|
+
totalSteps: step,
|
|
367
|
+
finalMessage: null,
|
|
368
|
+
selectedModel: currentModel,
|
|
369
|
+
messages,
|
|
370
|
+
usage: latestUsage,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Record Assistant message
|
|
374
|
+
const assistantMessage = {
|
|
375
|
+
role: 'assistant',
|
|
376
|
+
content: assistantText || null,
|
|
377
|
+
toolCalls: completedToolCalls.length > 0 ? completedToolCalls : undefined,
|
|
378
|
+
};
|
|
379
|
+
messages.push(assistantMessage);
|
|
380
|
+
// If no tool calls, task is finished!
|
|
381
|
+
if (completedToolCalls.length === 0) {
|
|
382
|
+
emit({
|
|
383
|
+
type: 'completion',
|
|
384
|
+
status: 'completed',
|
|
385
|
+
totalSteps: step,
|
|
386
|
+
summary: assistantText || undefined,
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
status: 'completed',
|
|
391
|
+
totalSteps: step,
|
|
392
|
+
finalMessage: assistantText,
|
|
393
|
+
selectedModel: currentModel,
|
|
394
|
+
messages,
|
|
395
|
+
usage: latestUsage,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// 5. Execute Each Tool Call
|
|
399
|
+
for (const call of completedToolCalls) {
|
|
400
|
+
if (signal?.aborted) {
|
|
401
|
+
emit({ type: 'cancellation', reason: 'Aborted by user', timestamp: Date.now() });
|
|
402
|
+
return {
|
|
403
|
+
status: 'cancelled',
|
|
404
|
+
totalSteps: step,
|
|
405
|
+
finalMessage: finalAssistantText,
|
|
406
|
+
selectedModel: currentModel,
|
|
407
|
+
messages,
|
|
408
|
+
usage: latestUsage,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
emit({
|
|
412
|
+
type: 'tool_call_initiated',
|
|
413
|
+
toolCallId: call.id,
|
|
414
|
+
toolName: call.name,
|
|
415
|
+
parameters: call.arguments,
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
});
|
|
418
|
+
const isSubagentCall = call.name.toLowerCase() === 'subagent';
|
|
419
|
+
let subagentTask;
|
|
420
|
+
if (isSubagentCall) {
|
|
421
|
+
if (options.allowSubagentDelegation === false) {
|
|
422
|
+
const nestedResult = {
|
|
423
|
+
toolName: call.name,
|
|
424
|
+
status: 'error',
|
|
425
|
+
output: 'Nested subagent delegation is not allowed.',
|
|
426
|
+
};
|
|
427
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: nestedResult, timestamp: Date.now() });
|
|
428
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: nestedResult.output, status: 'error' });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const parsedTask = SubagentDetailSchema.safeParse(call.arguments);
|
|
432
|
+
if (!parsedTask.success) {
|
|
433
|
+
const validationResult = {
|
|
434
|
+
toolName: call.name,
|
|
435
|
+
status: 'error',
|
|
436
|
+
output: `Invalid subagent detail: ${parsedTask.error.message}`,
|
|
437
|
+
};
|
|
438
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: validationResult, timestamp: Date.now() });
|
|
439
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: validationResult.output, status: 'error' });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
subagentTask = parsedTask.data.detail;
|
|
443
|
+
}
|
|
444
|
+
if (PSEUDO_ANSWER_TOOLS.has(call.name.toLowerCase())) {
|
|
445
|
+
const answerText = (typeof call.arguments.text === 'string' && call.arguments.text) ||
|
|
446
|
+
(typeof call.arguments.message === 'string' && call.arguments.message) ||
|
|
447
|
+
(typeof call.arguments.content === 'string' && call.arguments.content) ||
|
|
448
|
+
(typeof call.arguments.response === 'string' && call.arguments.response) ||
|
|
449
|
+
(typeof call.arguments.answer === 'string' && call.arguments.answer) ||
|
|
450
|
+
(typeof call.arguments._raw === 'string' && call.arguments._raw) ||
|
|
451
|
+
subagentTask ||
|
|
452
|
+
JSON.stringify(call.arguments);
|
|
453
|
+
// Subagent tool: delegate to a bounded child agent loop
|
|
454
|
+
if (isSubagentCall && subagentTask) {
|
|
455
|
+
const task = subagentTask;
|
|
456
|
+
emit({ type: 'progress', step: -1, status: `Delegating sub-task: ${task.slice(0, 80)}...`, timestamp: Date.now() });
|
|
457
|
+
const subagentResult = await delegator.delegate(task, options.provider, { maxSteps: 5, signal });
|
|
458
|
+
const subResultText = subagentResult.finalMessage ?? subagentResult.status;
|
|
459
|
+
emit({
|
|
460
|
+
type: 'assistant_delta',
|
|
461
|
+
delta: `\n**Subagent result:**\n${subResultText}`,
|
|
462
|
+
timestamp: Date.now(),
|
|
463
|
+
});
|
|
464
|
+
assistantText = (assistantText ? assistantText + '\n' : '') + `\n**Subagent result:**\n${subResultText}`;
|
|
465
|
+
finalAssistantText = assistantText;
|
|
466
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: { toolName: call.name, status: 'success', output: subResultText }, timestamp: Date.now() });
|
|
467
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: subResultText, status: 'success' });
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
emit({
|
|
471
|
+
type: 'assistant_delta',
|
|
472
|
+
delta: (assistantText ? '\n' : '') + answerText,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
});
|
|
475
|
+
assistantText = (assistantText ? assistantText + '\n' : '') + answerText;
|
|
476
|
+
finalAssistantText = assistantText;
|
|
477
|
+
const handledResult = {
|
|
478
|
+
toolName: call.name,
|
|
479
|
+
status: 'success',
|
|
480
|
+
output: 'Response delivered.',
|
|
481
|
+
};
|
|
482
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: handledResult, timestamp: Date.now() });
|
|
483
|
+
messages.push({
|
|
484
|
+
role: 'tool',
|
|
485
|
+
toolCallId: call.id,
|
|
486
|
+
name: call.name,
|
|
487
|
+
content: handledResult.output,
|
|
488
|
+
status: 'success',
|
|
489
|
+
});
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const tool = options.tools.get(call.name);
|
|
493
|
+
if (!tool) {
|
|
494
|
+
const notFoundResult = {
|
|
495
|
+
toolName: call.name,
|
|
496
|
+
status: 'error',
|
|
497
|
+
output: `Unknown tool: '${call.name}'.`,
|
|
498
|
+
};
|
|
499
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: notFoundResult, timestamp: Date.now() });
|
|
500
|
+
messages.push({
|
|
501
|
+
role: 'tool',
|
|
502
|
+
toolCallId: call.id,
|
|
503
|
+
name: call.name,
|
|
504
|
+
content: notFoundResult.output,
|
|
505
|
+
status: 'error',
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// Validate parameter schema
|
|
510
|
+
const parseResult = tool.parametersSchema.safeParse(call.arguments);
|
|
511
|
+
if (!parseResult.success) {
|
|
512
|
+
const validationResult = {
|
|
513
|
+
toolName: call.name,
|
|
514
|
+
status: 'error',
|
|
515
|
+
output: `Invalid arguments for tool '${call.name}': ${parseResult.error.message}`,
|
|
516
|
+
};
|
|
517
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: validationResult, timestamp: Date.now() });
|
|
518
|
+
messages.push({
|
|
519
|
+
role: 'tool',
|
|
520
|
+
toolCallId: call.id,
|
|
521
|
+
name: call.name,
|
|
522
|
+
content: validationResult.output,
|
|
523
|
+
status: 'error',
|
|
524
|
+
});
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
// Intercept unsolicited README/documentation write operations before approval gate
|
|
528
|
+
if (call.name === 'write_file' || call.name === 'edit_file') {
|
|
529
|
+
const rawPath = String(call.arguments?.path ?? '').trim();
|
|
530
|
+
const normPath = rawPath.toLowerCase().replace(/\\/g, '/');
|
|
531
|
+
const isReadmeTarget = normPath === 'readme.md' || normPath.endsWith('/readme.md');
|
|
532
|
+
const taskLower = task.toLowerCase();
|
|
533
|
+
if (isReadmeTarget && !taskLower.includes('readme')) {
|
|
534
|
+
const rejectedResult = {
|
|
535
|
+
toolName: call.name,
|
|
536
|
+
status: 'error',
|
|
537
|
+
output: `Unsolicited file modification rejected: Do not create or edit README.md unless explicitly commanded by the user's prompt. Please answer the user directly or address their actual request.`,
|
|
538
|
+
};
|
|
539
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: rejectedResult, timestamp: Date.now() });
|
|
540
|
+
messages.push({
|
|
541
|
+
role: 'tool',
|
|
542
|
+
toolCallId: call.id,
|
|
543
|
+
name: call.name,
|
|
544
|
+
content: rejectedResult.output,
|
|
545
|
+
status: 'error',
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Check policy constraints (read-only, non-interactive)
|
|
551
|
+
const policyCheck = policy.validateToolAction(call.name, tool.requiresApproval);
|
|
552
|
+
if (!policyCheck.allowed) {
|
|
553
|
+
const deniedResult = {
|
|
554
|
+
toolName: call.name,
|
|
555
|
+
status: 'denied',
|
|
556
|
+
output: policyCheck.reason ?? 'Denied by security policy.',
|
|
557
|
+
};
|
|
558
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: deniedResult, timestamp: Date.now() });
|
|
559
|
+
messages.push({
|
|
560
|
+
role: 'tool',
|
|
561
|
+
toolCallId: call.id,
|
|
562
|
+
name: call.name,
|
|
563
|
+
content: deniedResult.output,
|
|
564
|
+
status: 'denied',
|
|
565
|
+
});
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
// Approval Boundary
|
|
569
|
+
if (tool.requiresApproval) {
|
|
570
|
+
const requestId = `req_${crypto.randomUUID()}`;
|
|
571
|
+
let diffPreview;
|
|
572
|
+
try {
|
|
573
|
+
diffPreview = await tool.preview?.(parseResult.data, { workspaceRoot: options.workspaceRoot, abortSignal: signal });
|
|
574
|
+
}
|
|
575
|
+
catch { /* execution returns the actionable validation error */ }
|
|
576
|
+
const requestPayload = {
|
|
577
|
+
requestId,
|
|
578
|
+
toolName: call.name,
|
|
579
|
+
actionSummary: `Execute ${call.name} with ${JSON.stringify(call.arguments)}`,
|
|
580
|
+
exactPayload: {
|
|
581
|
+
targetFile: call.arguments.path,
|
|
582
|
+
diffPreview,
|
|
583
|
+
command: call.arguments.command
|
|
584
|
+
? [call.arguments.command, ...(call.arguments.args ?? [])]
|
|
585
|
+
: undefined,
|
|
586
|
+
cwd: options.workspaceRoot,
|
|
587
|
+
},
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
};
|
|
590
|
+
emit({ type: 'approval_request', request: requestPayload, timestamp: Date.now() });
|
|
591
|
+
let decision;
|
|
592
|
+
try {
|
|
593
|
+
decision = await options.approvalHandler.requestApproval(requestPayload, signal);
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
decision = { requestId, status: 'aborted', reason: err.message };
|
|
597
|
+
}
|
|
598
|
+
emit({
|
|
599
|
+
type: 'approval_resolved',
|
|
600
|
+
requestId,
|
|
601
|
+
status: decision.status,
|
|
602
|
+
reason: decision.reason,
|
|
603
|
+
timestamp: Date.now(),
|
|
604
|
+
});
|
|
605
|
+
if (decision.status !== 'approved') {
|
|
606
|
+
const userDeniedResult = {
|
|
607
|
+
toolName: call.name,
|
|
608
|
+
status: 'denied',
|
|
609
|
+
output: `Action '${call.name}' was denied by the user: ${decision.reason ?? 'Approval denied'}`,
|
|
610
|
+
};
|
|
611
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: userDeniedResult, timestamp: Date.now() });
|
|
612
|
+
messages.push({
|
|
613
|
+
role: 'tool',
|
|
614
|
+
toolCallId: call.id,
|
|
615
|
+
name: call.name,
|
|
616
|
+
content: userDeniedResult.output,
|
|
617
|
+
status: 'denied',
|
|
618
|
+
});
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Capture a recovery point only after the human approved this mutation.
|
|
623
|
+
if (tool.requiresApproval && options.onMutationApproved) {
|
|
624
|
+
try {
|
|
625
|
+
await options.onMutationApproved(call.name, parseResult.data);
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
const checkpointResult = { toolName: call.name, status: 'error', output: `Unable to prepare recovery checkpoint: ${err.message}` };
|
|
629
|
+
emit({ type: 'tool_result', toolCallId: call.id, result: checkpointResult, timestamp: Date.now() });
|
|
630
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: checkpointResult.output, status: 'error' });
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Execute tool inside workspace
|
|
635
|
+
let result;
|
|
636
|
+
try {
|
|
637
|
+
result = await tool.execute(parseResult.data, {
|
|
638
|
+
workspaceRoot: options.workspaceRoot,
|
|
639
|
+
abortSignal: signal,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
result = {
|
|
644
|
+
toolName: call.name,
|
|
645
|
+
status: 'error',
|
|
646
|
+
output: `Tool execution failed: ${err.message}`,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (tool.requiresApproval && result.status === 'success' && options.onMutationCompleted) {
|
|
650
|
+
try {
|
|
651
|
+
await options.onMutationCompleted(call.name, parseResult.data, result);
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
result = { ...result, status: 'error', output: `${result.output}\nRecovery checkpoint could not be finalized: ${err.message}` };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (call.name === 'run_diagnostics' && result.status === 'success') {
|
|
658
|
+
const diagnostics = result.metadata?.diagnostics ?? [];
|
|
659
|
+
const exitCode = result.metadata?.exitCode;
|
|
660
|
+
if (typeof exitCode === 'number')
|
|
661
|
+
emit({ type: 'diagnostic_result', toolCallId: call.id, diagnostics: diagnostics, exitCode, timestamp: Date.now() });
|
|
662
|
+
}
|
|
663
|
+
emit({ type: 'tool_result', toolCallId: call.id, result, timestamp: Date.now() });
|
|
664
|
+
messages.push({
|
|
665
|
+
role: 'tool',
|
|
666
|
+
toolCallId: call.id,
|
|
667
|
+
name: call.name,
|
|
668
|
+
content: result.output,
|
|
669
|
+
status: result.status,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
// If all executed tools in this turn were pseudo-answering tools, task is complete!
|
|
673
|
+
const hasRealTool = completedToolCalls.some((c) => !PSEUDO_ANSWER_TOOLS.has(c.name.toLowerCase()));
|
|
674
|
+
if (!hasRealTool) {
|
|
675
|
+
emit({
|
|
676
|
+
type: 'completion',
|
|
677
|
+
status: 'completed',
|
|
678
|
+
totalSteps: step,
|
|
679
|
+
summary: assistantText || undefined,
|
|
680
|
+
timestamp: Date.now(),
|
|
681
|
+
});
|
|
682
|
+
return {
|
|
683
|
+
status: 'completed',
|
|
684
|
+
totalSteps: step,
|
|
685
|
+
finalMessage: assistantText,
|
|
686
|
+
selectedModel: currentModel,
|
|
687
|
+
messages,
|
|
688
|
+
usage: latestUsage,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Max steps reached
|
|
693
|
+
emit({
|
|
694
|
+
type: 'completion',
|
|
695
|
+
status: 'step_limit_reached',
|
|
696
|
+
totalSteps: step,
|
|
697
|
+
summary: `Reached maximum step limit (${policy.maxSteps}).`,
|
|
698
|
+
timestamp: Date.now(),
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
status: 'step_limit_reached',
|
|
702
|
+
totalSteps: step,
|
|
703
|
+
finalMessage: finalAssistantText,
|
|
704
|
+
selectedModel: currentModel,
|
|
705
|
+
messages,
|
|
706
|
+
usage: latestUsage,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
//# sourceMappingURL=agent.js.map
|