@build-astron-co/nimbus 0.2.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 +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Agentic Loop
|
|
3
|
+
*
|
|
4
|
+
* Implements the autonomous agent loop:
|
|
5
|
+
* 1. Build context (system prompt + history + tools)
|
|
6
|
+
* 2. Send to LLM with tools enabled
|
|
7
|
+
* 3. Stream text response
|
|
8
|
+
* 4. If tool_use: check permissions → execute → collect results
|
|
9
|
+
* 5. Append messages → loop back to LLM
|
|
10
|
+
* 6. Exit when LLM returns end_turn (no more tool calls)
|
|
11
|
+
*
|
|
12
|
+
* This is the heart of the Nimbus agent. Every user message enters
|
|
13
|
+
* {@link runAgentLoop}, which orchestrates a multi-turn conversation with
|
|
14
|
+
* the LLM, executing tools on its behalf until it signals completion by
|
|
15
|
+
* returning a response with no further tool calls.
|
|
16
|
+
*
|
|
17
|
+
* @module agent/loop
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { LLMRouter } from '../llm/router';
|
|
21
|
+
import type {
|
|
22
|
+
LLMMessage,
|
|
23
|
+
ToolCall,
|
|
24
|
+
ToolCompletionRequest,
|
|
25
|
+
ToolDefinition as LLMToolDefinition,
|
|
26
|
+
} from '../llm/types';
|
|
27
|
+
import {
|
|
28
|
+
toOpenAITool,
|
|
29
|
+
type ToolDefinition,
|
|
30
|
+
type ToolResult,
|
|
31
|
+
type ToolRegistry,
|
|
32
|
+
} from '../tools/schemas/types';
|
|
33
|
+
import { buildSystemPrompt, type AgentMode } from './system-prompt';
|
|
34
|
+
import type { ContextManager, CompactionResult } from './context-manager';
|
|
35
|
+
import { runCompaction } from './compaction-agent';
|
|
36
|
+
import type { LSPManager } from '../lsp/manager';
|
|
37
|
+
import { SnapshotManager } from '../snapshots/manager';
|
|
38
|
+
import { calculateCost } from '../llm/cost-calculator';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Public Types
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/** Options for running the agent loop. */
|
|
45
|
+
export interface AgentLoopOptions {
|
|
46
|
+
/** The LLM router instance. */
|
|
47
|
+
router: LLMRouter;
|
|
48
|
+
|
|
49
|
+
/** Tool registry with available tools. */
|
|
50
|
+
toolRegistry: ToolRegistry;
|
|
51
|
+
|
|
52
|
+
/** Agent mode (plan/build/deploy). */
|
|
53
|
+
mode: AgentMode;
|
|
54
|
+
|
|
55
|
+
/** Maximum number of LLM turns before stopping (default: 50). */
|
|
56
|
+
maxTurns?: number;
|
|
57
|
+
|
|
58
|
+
/** Model to use (e.g. `'anthropic/claude-sonnet-4-20250514'`). */
|
|
59
|
+
model?: string;
|
|
60
|
+
|
|
61
|
+
/** Current working directory. */
|
|
62
|
+
cwd?: string;
|
|
63
|
+
|
|
64
|
+
/** Custom NIMBUS.md content injected into the system prompt. */
|
|
65
|
+
nimbusInstructions?: string;
|
|
66
|
+
|
|
67
|
+
/** Callback for streaming text output. */
|
|
68
|
+
onText?: (text: string) => void;
|
|
69
|
+
|
|
70
|
+
/** Callback when a tool call starts. */
|
|
71
|
+
onToolCallStart?: (toolCall: ToolCallInfo) => void;
|
|
72
|
+
|
|
73
|
+
/** Callback when a tool call completes. */
|
|
74
|
+
onToolCallEnd?: (toolCall: ToolCallInfo, result: ToolResult) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Callback to check permission before tool execution.
|
|
78
|
+
* If omitted, all tools are executed without prompting.
|
|
79
|
+
*/
|
|
80
|
+
checkPermission?: (tool: ToolDefinition, input: unknown) => Promise<PermissionDecision>;
|
|
81
|
+
|
|
82
|
+
/** AbortSignal for cancellation (Ctrl+C). */
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
|
|
85
|
+
/** Session ID for persistence (reserved for future use). */
|
|
86
|
+
sessionId?: string;
|
|
87
|
+
|
|
88
|
+
/** Optional context manager for auto-compact. When provided, the loop
|
|
89
|
+
* checks context usage after each tool-call turn and triggers
|
|
90
|
+
* compaction if the threshold is exceeded. */
|
|
91
|
+
contextManager?: ContextManager;
|
|
92
|
+
|
|
93
|
+
/** Callback fired when auto-compact is triggered. Receives the
|
|
94
|
+
* compaction result with token savings information. */
|
|
95
|
+
onCompact?: (result: CompactionResult) => void;
|
|
96
|
+
|
|
97
|
+
/** Optional LSP manager for post-edit diagnostics. When provided,
|
|
98
|
+
* the loop queries the language server after file-editing tools
|
|
99
|
+
* and appends any diagnostics to the tool result so the LLM can
|
|
100
|
+
* self-correct type errors and other issues. */
|
|
101
|
+
lspManager?: LSPManager;
|
|
102
|
+
|
|
103
|
+
/** Optional snapshot manager for auto-capture before file-editing tools.
|
|
104
|
+
* When provided, a snapshot is captured before each file-modifying tool
|
|
105
|
+
* call so users can undo/redo changes. */
|
|
106
|
+
snapshotManager?: SnapshotManager;
|
|
107
|
+
|
|
108
|
+
/** Callback fired after each LLM turn with accumulated usage and cost.
|
|
109
|
+
* Allows the TUI to update cost/token display in real-time during
|
|
110
|
+
* multi-turn agent loops, not just at the end. */
|
|
111
|
+
onUsage?: (usage: AgentLoopUsage, costUSD: number) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Information about a tool call in progress. */
|
|
115
|
+
export interface ToolCallInfo {
|
|
116
|
+
/** Provider-assigned unique ID for this tool call. */
|
|
117
|
+
id: string;
|
|
118
|
+
|
|
119
|
+
/** Tool name as it appears in the registry. */
|
|
120
|
+
name: string;
|
|
121
|
+
|
|
122
|
+
/** Parsed input arguments. */
|
|
123
|
+
input: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Result of a permission check.
|
|
128
|
+
*
|
|
129
|
+
* - `allow` -- proceed with execution.
|
|
130
|
+
* - `deny` -- skip this invocation and report denial to the LLM.
|
|
131
|
+
* - `block` -- skip and report that the tool is permanently blocked.
|
|
132
|
+
*/
|
|
133
|
+
export type PermissionDecision = 'allow' | 'deny' | 'block';
|
|
134
|
+
|
|
135
|
+
/** Aggregate token usage across all LLM turns. */
|
|
136
|
+
export interface AgentLoopUsage {
|
|
137
|
+
/** Total prompt (input) tokens consumed. */
|
|
138
|
+
promptTokens: number;
|
|
139
|
+
|
|
140
|
+
/** Total completion (output) tokens consumed. */
|
|
141
|
+
completionTokens: number;
|
|
142
|
+
|
|
143
|
+
/** Sum of prompt + completion tokens. */
|
|
144
|
+
totalTokens: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Result of running the agent loop. */
|
|
148
|
+
export interface AgentLoopResult {
|
|
149
|
+
/** The conversation messages after the loop completes. */
|
|
150
|
+
messages: LLMMessage[];
|
|
151
|
+
|
|
152
|
+
/** Number of LLM turns taken. */
|
|
153
|
+
turns: number;
|
|
154
|
+
|
|
155
|
+
/** Whether the loop was interrupted via the AbortSignal. */
|
|
156
|
+
interrupted: boolean;
|
|
157
|
+
|
|
158
|
+
/** Total token usage across all turns. */
|
|
159
|
+
usage: AgentLoopUsage;
|
|
160
|
+
|
|
161
|
+
/** Total estimated cost in USD. */
|
|
162
|
+
totalCost: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Constants
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** Default model when none is specified. */
|
|
170
|
+
const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';
|
|
171
|
+
|
|
172
|
+
/** Default max output tokens per LLM call. */
|
|
173
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
174
|
+
|
|
175
|
+
/** Default maximum number of agent turns. */
|
|
176
|
+
const DEFAULT_MAX_TURNS = 50;
|
|
177
|
+
|
|
178
|
+
/** Maximum characters of tool output to include in conversation history.
|
|
179
|
+
* Anything beyond this is truncated to prevent context window overflow. */
|
|
180
|
+
const MAX_TOOL_OUTPUT_CHARS = 100_000;
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Main Entry Point
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Run the agentic loop.
|
|
188
|
+
*
|
|
189
|
+
* Takes a user message and existing conversation history, then runs
|
|
190
|
+
* the LLM in a loop until it stops requesting tool calls.
|
|
191
|
+
*
|
|
192
|
+
* The loop terminates when any of the following conditions are met:
|
|
193
|
+
* - The LLM returns a response with no tool calls (natural end).
|
|
194
|
+
* - The maximum number of turns is reached.
|
|
195
|
+
* - The AbortSignal fires (e.g. user presses Ctrl+C).
|
|
196
|
+
* - An unrecoverable LLM API error occurs.
|
|
197
|
+
*
|
|
198
|
+
* @param userMessage - The new user message to process.
|
|
199
|
+
* @param history - Prior conversation messages (may be empty for a fresh session).
|
|
200
|
+
* @param options - Configuration for the loop.
|
|
201
|
+
* @returns The final conversation state, turn count, usage, and cost.
|
|
202
|
+
*/
|
|
203
|
+
export async function runAgentLoop(
|
|
204
|
+
userMessage: string,
|
|
205
|
+
history: LLMMessage[],
|
|
206
|
+
options: AgentLoopOptions
|
|
207
|
+
): Promise<AgentLoopResult> {
|
|
208
|
+
const {
|
|
209
|
+
router,
|
|
210
|
+
toolRegistry,
|
|
211
|
+
mode,
|
|
212
|
+
maxTurns = DEFAULT_MAX_TURNS,
|
|
213
|
+
model,
|
|
214
|
+
cwd,
|
|
215
|
+
nimbusInstructions,
|
|
216
|
+
onText,
|
|
217
|
+
onToolCallStart,
|
|
218
|
+
onToolCallEnd,
|
|
219
|
+
checkPermission,
|
|
220
|
+
signal,
|
|
221
|
+
} = options;
|
|
222
|
+
|
|
223
|
+
// -----------------------------------------------------------------------
|
|
224
|
+
// 1. Prepare tools and system prompt
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
const tools = getToolsForMode(toolRegistry.getAll(), mode);
|
|
228
|
+
|
|
229
|
+
const systemPrompt = buildSystemPrompt({
|
|
230
|
+
mode,
|
|
231
|
+
tools,
|
|
232
|
+
nimbusInstructions,
|
|
233
|
+
cwd,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Convert agentic ToolDefinitions to the LLM-level format expected by
|
|
237
|
+
// the router's routeWithTools() method (OpenAI function-calling shape).
|
|
238
|
+
const llmTools: LLMToolDefinition[] = tools.map(toOpenAITool);
|
|
239
|
+
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// 2. Initialize conversation state
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
const messages: LLMMessage[] = [...history, { role: 'user', content: userMessage }];
|
|
245
|
+
|
|
246
|
+
let turns = 0;
|
|
247
|
+
let interrupted = false;
|
|
248
|
+
const totalUsage: AgentLoopUsage = {
|
|
249
|
+
promptTokens: 0,
|
|
250
|
+
completionTokens: 0,
|
|
251
|
+
totalTokens: 0,
|
|
252
|
+
};
|
|
253
|
+
let totalCost = 0;
|
|
254
|
+
|
|
255
|
+
// -----------------------------------------------------------------------
|
|
256
|
+
// 3. Main agent loop
|
|
257
|
+
// -----------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
while (turns < maxTurns) {
|
|
260
|
+
// Check for cancellation before each turn
|
|
261
|
+
if (signal?.aborted) {
|
|
262
|
+
interrupted = true;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
turns++;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Build the completion request with tool definitions
|
|
270
|
+
const request: ToolCompletionRequest = {
|
|
271
|
+
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
272
|
+
model: model ?? DEFAULT_MODEL,
|
|
273
|
+
tools: llmTools,
|
|
274
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Stream text tokens incrementally via routeStreamWithTools.
|
|
278
|
+
// Tokens are forwarded to onText as they arrive; tool calls
|
|
279
|
+
// are accumulated from the final chunk.
|
|
280
|
+
let responseContent = '';
|
|
281
|
+
let responseToolCalls: ToolCall[] | undefined;
|
|
282
|
+
let responseUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
283
|
+
|
|
284
|
+
for await (const chunk of router.routeStreamWithTools(request)) {
|
|
285
|
+
if (chunk.content) {
|
|
286
|
+
responseContent += chunk.content;
|
|
287
|
+
if (onText) {
|
|
288
|
+
onText(chunk.content);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (chunk.toolCallStart && onText) {
|
|
292
|
+
// Show early feedback when the LLM starts composing a tool call
|
|
293
|
+
onText(`\n[Preparing tool: ${chunk.toolCallStart.name}...]\n`);
|
|
294
|
+
}
|
|
295
|
+
if (chunk.toolCalls) {
|
|
296
|
+
responseToolCalls = chunk.toolCalls;
|
|
297
|
+
}
|
|
298
|
+
if (chunk.usage) {
|
|
299
|
+
responseUsage = chunk.usage;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Accumulate usage and cost
|
|
304
|
+
totalUsage.promptTokens += responseUsage.promptTokens;
|
|
305
|
+
totalUsage.completionTokens += responseUsage.completionTokens;
|
|
306
|
+
totalUsage.totalTokens += responseUsage.totalTokens;
|
|
307
|
+
|
|
308
|
+
// Estimate cost for this turn
|
|
309
|
+
const resolvedModel = model ?? DEFAULT_MODEL;
|
|
310
|
+
const providerName = resolvedModel.includes('/') ? resolvedModel.split('/')[0] : 'anthropic';
|
|
311
|
+
const modelName = resolvedModel.includes('/')
|
|
312
|
+
? resolvedModel.split('/').slice(1).join('/')
|
|
313
|
+
: resolvedModel;
|
|
314
|
+
const turnCost = calculateCost(
|
|
315
|
+
providerName,
|
|
316
|
+
modelName,
|
|
317
|
+
responseUsage.promptTokens,
|
|
318
|
+
responseUsage.completionTokens
|
|
319
|
+
);
|
|
320
|
+
totalCost += turnCost.costUSD;
|
|
321
|
+
|
|
322
|
+
// Notify caller of accumulated usage/cost after each turn
|
|
323
|
+
if (options.onUsage) {
|
|
324
|
+
options.onUsage(totalUsage, totalCost);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// -----------------------------------------------------------------
|
|
328
|
+
// No tool calls → the LLM is done
|
|
329
|
+
// -----------------------------------------------------------------
|
|
330
|
+
if (!responseToolCalls || responseToolCalls.length === 0) {
|
|
331
|
+
messages.push({
|
|
332
|
+
role: 'assistant',
|
|
333
|
+
content: responseContent,
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// -----------------------------------------------------------------
|
|
339
|
+
// Tool calls present → execute each one
|
|
340
|
+
// -----------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
// Append the assistant message that contains the tool calls
|
|
343
|
+
messages.push({
|
|
344
|
+
role: 'assistant',
|
|
345
|
+
content: responseContent,
|
|
346
|
+
toolCalls: responseToolCalls,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Process tool calls sequentially (order may matter for side effects)
|
|
350
|
+
for (const toolCall of responseToolCalls) {
|
|
351
|
+
// Check for cancellation between tool calls
|
|
352
|
+
if (signal?.aborted) {
|
|
353
|
+
interrupted = true;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const result = await executeToolCall(
|
|
358
|
+
toolCall,
|
|
359
|
+
toolRegistry,
|
|
360
|
+
onToolCallStart,
|
|
361
|
+
onToolCallEnd,
|
|
362
|
+
checkPermission,
|
|
363
|
+
options.lspManager,
|
|
364
|
+
options.snapshotManager,
|
|
365
|
+
options.sessionId,
|
|
366
|
+
signal
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Append each tool result as a separate message so the LLM can
|
|
370
|
+
// match it to the corresponding tool_use block by toolCallId.
|
|
371
|
+
let toolContent = result.isError ? `Error: ${result.error}` : result.output;
|
|
372
|
+
|
|
373
|
+
// Truncate excessively large tool outputs to prevent context overflow
|
|
374
|
+
if (toolContent.length > MAX_TOOL_OUTPUT_CHARS) {
|
|
375
|
+
const truncatedLength = toolContent.length;
|
|
376
|
+
toolContent = `${toolContent.slice(0, MAX_TOOL_OUTPUT_CHARS)}\n\n... [Output truncated: ${truncatedLength.toLocaleString()} chars total, showing first ${MAX_TOOL_OUTPUT_CHARS.toLocaleString()}]`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
messages.push({
|
|
380
|
+
role: 'tool',
|
|
381
|
+
toolCallId: toolCall.id,
|
|
382
|
+
name: toolCall.function.name,
|
|
383
|
+
content: toolContent,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// If we broke out of the tool-call loop due to cancellation, exit
|
|
388
|
+
// the main loop as well.
|
|
389
|
+
if (interrupted) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// -----------------------------------------------------------------
|
|
394
|
+
// Auto-compact check
|
|
395
|
+
// -----------------------------------------------------------------
|
|
396
|
+
// After tool results are appended, check whether the conversation
|
|
397
|
+
// has grown past the context window threshold. If so, summarize
|
|
398
|
+
// older messages to free up space for future turns.
|
|
399
|
+
if (options.contextManager) {
|
|
400
|
+
const toolTokens = llmTools.reduce(
|
|
401
|
+
(sum, t) => sum + Math.ceil(JSON.stringify(t).length / 4),
|
|
402
|
+
0
|
|
403
|
+
);
|
|
404
|
+
if (options.contextManager.shouldCompact(systemPrompt, messages, toolTokens)) {
|
|
405
|
+
try {
|
|
406
|
+
const compactResult = await runCompaction(messages, options.contextManager, { router });
|
|
407
|
+
// Replace messages with the compacted version
|
|
408
|
+
messages.length = 0;
|
|
409
|
+
messages.push(...compactResult.messages);
|
|
410
|
+
if (options.onCompact) {
|
|
411
|
+
options.onCompact(compactResult.result);
|
|
412
|
+
}
|
|
413
|
+
} catch (compactErr) {
|
|
414
|
+
// Compaction failed — notify user visibly and continue with original messages
|
|
415
|
+
const compactErrMsg =
|
|
416
|
+
compactErr instanceof Error ? compactErr.message : String(compactErr);
|
|
417
|
+
if (onText) {
|
|
418
|
+
onText(
|
|
419
|
+
`\n[Warning: Auto-compaction failed: ${compactErrMsg}. Context may exceed budget on the next turn.]\n`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch (error: unknown) {
|
|
426
|
+
// LLM API error — report to the caller and break
|
|
427
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
428
|
+
if (onText) {
|
|
429
|
+
onText(`\n[Error: ${msg}]\n`);
|
|
430
|
+
}
|
|
431
|
+
messages.push({
|
|
432
|
+
role: 'assistant',
|
|
433
|
+
content: `I encountered an error: ${msg}`,
|
|
434
|
+
});
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// -----------------------------------------------------------------------
|
|
440
|
+
// 4. Post-loop bookkeeping
|
|
441
|
+
// -----------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
if (turns >= maxTurns && !interrupted) {
|
|
444
|
+
if (onText) {
|
|
445
|
+
onText(`\n[Agent reached maximum turns limit (${maxTurns}). Stopping.]\n`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
messages,
|
|
451
|
+
turns,
|
|
452
|
+
interrupted,
|
|
453
|
+
usage: totalUsage,
|
|
454
|
+
totalCost,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Tool Execution
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
/** Tools that modify files and should trigger LSP diagnostics. */
|
|
463
|
+
const FILE_EDITING_TOOLS = new Set(['edit_file', 'multi_edit', 'write_file']);
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Extract the file path from a tool call's parsed arguments.
|
|
467
|
+
*
|
|
468
|
+
* File-editing tools all have a `path` parameter that identifies
|
|
469
|
+
* the target file. Returns `null` for non-file tools.
|
|
470
|
+
*/
|
|
471
|
+
function extractFilePath(toolName: string, input: unknown): string | null {
|
|
472
|
+
if (!FILE_EDITING_TOOLS.has(toolName)) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
if (input && typeof input === 'object' && 'path' in input) {
|
|
476
|
+
return (input as { path: string }).path;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Execute a single tool call.
|
|
483
|
+
*
|
|
484
|
+
* Handles:
|
|
485
|
+
* - Looking up the tool in the registry.
|
|
486
|
+
* - Parsing the JSON arguments string from the LLM response.
|
|
487
|
+
* - Validating input against the Zod schema.
|
|
488
|
+
* - Checking permissions via the caller-supplied callback.
|
|
489
|
+
* - Invoking the tool and returning the result.
|
|
490
|
+
* - Notifying start/end callbacks.
|
|
491
|
+
* - Querying the LSP for diagnostics after file edits.
|
|
492
|
+
*
|
|
493
|
+
* @param toolCall - The raw tool call from the LLM response.
|
|
494
|
+
* @param registry - The tool registry to look up the tool definition.
|
|
495
|
+
* @param onStart - Optional callback fired before execution.
|
|
496
|
+
* @param onEnd - Optional callback fired after execution (or error).
|
|
497
|
+
* @param checkPermission - Optional permission gate.
|
|
498
|
+
* @param lspManager - Optional LSP manager for post-edit diagnostics.
|
|
499
|
+
* @returns The tool result (always succeeds; errors are captured inside the result).
|
|
500
|
+
*/
|
|
501
|
+
async function executeToolCall(
|
|
502
|
+
toolCall: ToolCall,
|
|
503
|
+
registry: ToolRegistry,
|
|
504
|
+
onStart?: (info: ToolCallInfo) => void,
|
|
505
|
+
onEnd?: (info: ToolCallInfo, result: ToolResult) => void,
|
|
506
|
+
checkPermission?: (tool: ToolDefinition, input: unknown) => Promise<PermissionDecision>,
|
|
507
|
+
lspManager?: LSPManager,
|
|
508
|
+
snapshotManager?: SnapshotManager,
|
|
509
|
+
sessionId?: string,
|
|
510
|
+
signal?: AbortSignal
|
|
511
|
+
): Promise<ToolResult> {
|
|
512
|
+
const toolName = toolCall.function.name;
|
|
513
|
+
|
|
514
|
+
// Parse the JSON arguments string from the LLM
|
|
515
|
+
let parsedArgs: unknown;
|
|
516
|
+
try {
|
|
517
|
+
parsedArgs = JSON.parse(toolCall.function.arguments);
|
|
518
|
+
} catch {
|
|
519
|
+
const result: ToolResult = {
|
|
520
|
+
output: '',
|
|
521
|
+
error: `Failed to parse tool arguments as JSON for '${toolName}': ${toolCall.function.arguments}`,
|
|
522
|
+
isError: true,
|
|
523
|
+
};
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const callInfo: ToolCallInfo = {
|
|
528
|
+
id: toolCall.id,
|
|
529
|
+
name: toolName,
|
|
530
|
+
input: parsedArgs,
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Look up the tool definition
|
|
534
|
+
const tool = registry.get(toolName);
|
|
535
|
+
if (!tool) {
|
|
536
|
+
const result: ToolResult = {
|
|
537
|
+
output: '',
|
|
538
|
+
error: `Unknown tool: ${toolName}`,
|
|
539
|
+
isError: true,
|
|
540
|
+
};
|
|
541
|
+
if (onEnd) {
|
|
542
|
+
onEnd(callInfo, result);
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Notify start
|
|
548
|
+
if (onStart) {
|
|
549
|
+
onStart(callInfo);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Permission check
|
|
553
|
+
if (checkPermission) {
|
|
554
|
+
const decision = await checkPermission(tool, parsedArgs);
|
|
555
|
+
if (decision === 'deny' || decision === 'block') {
|
|
556
|
+
const result: ToolResult = {
|
|
557
|
+
output: '',
|
|
558
|
+
error:
|
|
559
|
+
decision === 'block'
|
|
560
|
+
? `Tool '${toolName}' is blocked by permission policy.`
|
|
561
|
+
: `User denied permission for tool '${toolName}'.`,
|
|
562
|
+
isError: true,
|
|
563
|
+
};
|
|
564
|
+
if (onEnd) {
|
|
565
|
+
onEnd(callInfo, result);
|
|
566
|
+
}
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Capture snapshot before file-modifying tools for undo/redo support
|
|
572
|
+
if (
|
|
573
|
+
snapshotManager &&
|
|
574
|
+
SnapshotManager.shouldSnapshot(toolName, parsedArgs as Record<string, unknown>)
|
|
575
|
+
) {
|
|
576
|
+
try {
|
|
577
|
+
await snapshotManager.captureSnapshot({
|
|
578
|
+
sessionId: sessionId || 'default',
|
|
579
|
+
messageId: toolCall.id,
|
|
580
|
+
toolCallId: toolCall.id,
|
|
581
|
+
description: `${toolName}: ${extractFilePath(toolName, parsedArgs) || '(bash command)'}`,
|
|
582
|
+
});
|
|
583
|
+
} catch {
|
|
584
|
+
// Snapshot failure should never block the tool call
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Validate input against the tool's Zod schema and execute
|
|
589
|
+
let result: ToolResult;
|
|
590
|
+
try {
|
|
591
|
+
const validatedInput = tool.inputSchema.parse(parsedArgs);
|
|
592
|
+
|
|
593
|
+
// Thread AbortSignal into bash tool for Ctrl+C child process killing
|
|
594
|
+
if (signal && toolName === 'bash' && validatedInput && typeof validatedInput === 'object') {
|
|
595
|
+
(validatedInput as Record<string, unknown>)._signal = signal;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
result = await tool.execute(validatedInput);
|
|
599
|
+
} catch (error: unknown) {
|
|
600
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
601
|
+
result = {
|
|
602
|
+
output: '',
|
|
603
|
+
error: `Tool execution failed: ${msg}`,
|
|
604
|
+
isError: true,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// -----------------------------------------------------------------------
|
|
609
|
+
// LSP diagnostics injection
|
|
610
|
+
// -----------------------------------------------------------------------
|
|
611
|
+
// After a successful file edit, notify the language server and collect
|
|
612
|
+
// any diagnostics (type errors, lint issues). If errors exist they are
|
|
613
|
+
// appended to the tool output so the LLM sees them on its next turn
|
|
614
|
+
// and can self-correct.
|
|
615
|
+
if (lspManager && !result.isError) {
|
|
616
|
+
const filePath = extractFilePath(toolName, parsedArgs);
|
|
617
|
+
if (filePath) {
|
|
618
|
+
try {
|
|
619
|
+
await lspManager.touchFile(filePath);
|
|
620
|
+
const diagnostics = await lspManager.getDiagnostics(filePath);
|
|
621
|
+
if (diagnostics.length > 0) {
|
|
622
|
+
const formatted = lspManager.formatDiagnosticsForAgent(diagnostics);
|
|
623
|
+
if (formatted) {
|
|
624
|
+
result = {
|
|
625
|
+
...result,
|
|
626
|
+
output: result.output ? `${result.output}\n\n${formatted}` : formatted,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} catch (lspErr) {
|
|
631
|
+
// LSP errors should never block the agent loop.
|
|
632
|
+
// Append a note to the tool result so the LLM (and user) can see it.
|
|
633
|
+
const lspErrMsg = lspErr instanceof Error ? lspErr.message : String(lspErr);
|
|
634
|
+
result = {
|
|
635
|
+
...result,
|
|
636
|
+
output: result.output
|
|
637
|
+
? `${result.output}\n\n[Note: LSP diagnostics unavailable: ${lspErrMsg}]`
|
|
638
|
+
: `[Note: LSP diagnostics unavailable: ${lspErrMsg}]`,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Notify end
|
|
645
|
+
if (onEnd) {
|
|
646
|
+
onEnd(callInfo, result);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Mode-Based Tool Filtering
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Set of tool names allowed in `plan` mode.
|
|
658
|
+
*
|
|
659
|
+
* Plan mode is strictly read-only: the agent can inspect files, search
|
|
660
|
+
* the codebase, read tasks, estimate costs, and detect drift -- but it
|
|
661
|
+
* cannot write files, run commands, or mutate infrastructure.
|
|
662
|
+
*/
|
|
663
|
+
const PLAN_MODE_TOOLS = new Set([
|
|
664
|
+
'read_file',
|
|
665
|
+
'glob',
|
|
666
|
+
'grep',
|
|
667
|
+
'list_dir',
|
|
668
|
+
'webfetch',
|
|
669
|
+
'todo_read',
|
|
670
|
+
'todo_write',
|
|
671
|
+
'task',
|
|
672
|
+
'cost_estimate',
|
|
673
|
+
'drift_detect',
|
|
674
|
+
'cloud_discover',
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Set of tool names blocked in `build` mode.
|
|
679
|
+
*
|
|
680
|
+
* Build mode allows reads and writes (file edits, code generation) but
|
|
681
|
+
* blocks infrastructure-mutating operations that could affect live
|
|
682
|
+
* environments. The permission engine provides fine-grained control on
|
|
683
|
+
* top of this coarse filter.
|
|
684
|
+
*/
|
|
685
|
+
const BUILD_MODE_BLOCKED_TOOLS = new Set(['terraform', 'kubectl', 'helm']);
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Filter tools based on the current agent mode.
|
|
689
|
+
*
|
|
690
|
+
* - **plan**: Only read-only tools + cost/drift analysis.
|
|
691
|
+
* - **build**: All tools except infrastructure mutation commands.
|
|
692
|
+
* - **deploy**: All tools are available.
|
|
693
|
+
*
|
|
694
|
+
* @param allTools - Every tool registered in the system.
|
|
695
|
+
* @param mode - The active agent mode.
|
|
696
|
+
* @returns The subset of tools available in the given mode.
|
|
697
|
+
*/
|
|
698
|
+
export function getToolsForMode(allTools: ToolDefinition[], mode: AgentMode): ToolDefinition[] {
|
|
699
|
+
switch (mode) {
|
|
700
|
+
case 'plan':
|
|
701
|
+
return allTools.filter(t => PLAN_MODE_TOOLS.has(t.name));
|
|
702
|
+
|
|
703
|
+
case 'build':
|
|
704
|
+
return allTools.filter(t => !BUILD_MODE_BLOCKED_TOOLS.has(t.name));
|
|
705
|
+
|
|
706
|
+
case 'deploy':
|
|
707
|
+
// All tools available
|
|
708
|
+
return allTools;
|
|
709
|
+
|
|
710
|
+
default: {
|
|
711
|
+
// Exhaustive check -- if a new mode is added this becomes a compile
|
|
712
|
+
// error (assuming AgentMode is a union type).
|
|
713
|
+
const _exhaustive: never = mode;
|
|
714
|
+
return allTools;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|