@bahulam/code 2.6.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/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Agent — T18: Direct LLM API calls, <100ms startup, offline.
|
|
3
|
+
* Replaces the SSE backend for --local mode.
|
|
4
|
+
* Yields events matching the same format as TarangStreamClient.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ContextRetriever } from '../context/retriever.mjs';
|
|
8
|
+
import { createStagnationTracker, stagnationMessage } from './stagnation.mjs';
|
|
9
|
+
import { PromptCache } from './cache.mjs';
|
|
10
|
+
import {
|
|
11
|
+
ANTHROPIC_BETA_HEADER,
|
|
12
|
+
cacheableSystem,
|
|
13
|
+
cacheableTools,
|
|
14
|
+
withMessageBreakpoint,
|
|
15
|
+
needsExplicitCacheControl,
|
|
16
|
+
} from './cache-control.mjs';
|
|
17
|
+
|
|
18
|
+
const MAX_ITERATIONS = 50;
|
|
19
|
+
|
|
20
|
+
/** Tool schemas for the LLM — proper parameter definitions. */
|
|
21
|
+
const TOOL_SCHEMAS = [
|
|
22
|
+
{
|
|
23
|
+
name: 'shell',
|
|
24
|
+
description: 'Run a shell command and return stdout/stderr. Use for: installing packages, running tests, builds, git operations, or any terminal command.',
|
|
25
|
+
input_schema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
command: { type: 'string', description: 'The shell command to execute' },
|
|
29
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000)' },
|
|
30
|
+
},
|
|
31
|
+
required: ['command'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'read_file',
|
|
36
|
+
description: 'Read a file and return its contents. Supports line ranges for large files.',
|
|
37
|
+
input_schema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
file_path: { type: 'string', description: 'Path to the file (relative to project root)' },
|
|
41
|
+
offset: { type: 'number', description: 'Start line number (1-based, optional)' },
|
|
42
|
+
limit: { type: 'number', description: 'Number of lines to read (optional)' },
|
|
43
|
+
},
|
|
44
|
+
required: ['file_path'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'write_file',
|
|
49
|
+
description: 'Create or overwrite a file with the given content. Parent directories are created automatically.',
|
|
50
|
+
input_schema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
file_path: { type: 'string', description: 'Path to the file (relative to project root)' },
|
|
54
|
+
content: { type: 'string', description: 'The full file content to write' },
|
|
55
|
+
},
|
|
56
|
+
required: ['file_path', 'content'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'edit_file',
|
|
61
|
+
description: 'Search for a string in a file and replace it. The old_string must match exactly (including whitespace).',
|
|
62
|
+
input_schema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
file_path: { type: 'string', description: 'Path to the file' },
|
|
66
|
+
old_string: { type: 'string', description: 'Exact string to find in the file' },
|
|
67
|
+
new_string: { type: 'string', description: 'Replacement string' },
|
|
68
|
+
},
|
|
69
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'list_files',
|
|
74
|
+
description: 'List files matching a glob pattern, or return a bounded directory tree with format="tree".',
|
|
75
|
+
input_schema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
pattern: { type: 'string', description: 'Glob pattern (e.g., "src/**/*.ts", "*.json")' },
|
|
79
|
+
path: { type: 'string', description: 'Directory to search in (default: project root)' },
|
|
80
|
+
format: { type: 'string', enum: ['files', 'tree'], description: 'Output format. "files" returns file paths; "tree" returns folders and files.' },
|
|
81
|
+
max_depth: { type: 'number', description: 'Maximum tree depth when format="tree" (default 2, max 6)' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'search_code',
|
|
87
|
+
description: 'Search file contents using a regex pattern. Returns matching lines with file paths and line numbers.',
|
|
88
|
+
input_schema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
|
92
|
+
path: { type: 'string', description: 'Directory or file to search in (default: project root)' },
|
|
93
|
+
include: { type: 'string', description: 'File glob filter (e.g., "*.ts")' },
|
|
94
|
+
},
|
|
95
|
+
required: ['pattern'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'search_files',
|
|
100
|
+
description: 'Search for files by name pattern. Returns matching file paths.',
|
|
101
|
+
input_schema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
query: { type: 'string', description: 'Filename pattern to search for' },
|
|
105
|
+
path: { type: 'string', description: 'Directory to search in (default: project root)' },
|
|
106
|
+
},
|
|
107
|
+
required: ['query'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'read_files',
|
|
112
|
+
description: 'Read multiple files at once (batch). More efficient than multiple read_file calls.',
|
|
113
|
+
input_schema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
file_paths: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
items: { type: 'string' },
|
|
119
|
+
description: 'Array of file paths to read',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ['file_paths'],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'delete_file',
|
|
127
|
+
description: 'Delete a file from the project.',
|
|
128
|
+
input_schema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
file_path: { type: 'string', description: 'Path to the file to delete' },
|
|
132
|
+
},
|
|
133
|
+
required: ['file_path'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'get_file_info',
|
|
138
|
+
description: 'Get file metadata: size, modification time, type, permissions.',
|
|
139
|
+
input_schema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
file_path: { type: 'string', description: 'Path to the file' },
|
|
143
|
+
},
|
|
144
|
+
required: ['file_path'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'validate_file',
|
|
149
|
+
description: 'Check file syntax. Runs language-specific checks (node --check for JS, py_compile for Python).',
|
|
150
|
+
input_schema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
file_path: { type: 'string', description: 'Path to the file to validate' },
|
|
154
|
+
},
|
|
155
|
+
required: ['file_path'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'validate_build',
|
|
160
|
+
description: 'Run the project build command. Auto-detects: npm run build, make, cargo build, etc.',
|
|
161
|
+
input_schema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
command: { type: 'string', description: 'Build command override (optional — auto-detected if omitted)' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'lint_check',
|
|
170
|
+
description: 'Run linter on a file. Uses ruff for Python, eslint for JS/TS.',
|
|
171
|
+
input_schema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
file_path: { type: 'string', description: 'Path to the file to lint' },
|
|
175
|
+
},
|
|
176
|
+
required: ['file_path'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'validate_structure',
|
|
181
|
+
description: 'Check that a list of expected files exist in the project.',
|
|
182
|
+
input_schema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
files: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
items: { type: 'string' },
|
|
188
|
+
description: 'Array of file paths that should exist',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ['files'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
export class LocalAgent {
|
|
197
|
+
constructor({
|
|
198
|
+
apiKey,
|
|
199
|
+
model,
|
|
200
|
+
toolExecutor,
|
|
201
|
+
verbose = false,
|
|
202
|
+
openRouterKey = null,
|
|
203
|
+
cwd = null,
|
|
204
|
+
systemPromptOverride = null,
|
|
205
|
+
maxTurns = null,
|
|
206
|
+
stagnationDetection = false,
|
|
207
|
+
stagnationThreshold = 3,
|
|
208
|
+
}) {
|
|
209
|
+
this.apiKey = apiKey;
|
|
210
|
+
this.openRouterKey = openRouterKey;
|
|
211
|
+
this.model = model || 'claude-sonnet-4-20250514';
|
|
212
|
+
this.toolExecutor = toolExecutor;
|
|
213
|
+
this.verbose = verbose;
|
|
214
|
+
this.cwd = cwd || process.cwd();
|
|
215
|
+
this.retriever = new ContextRetriever(this.cwd);
|
|
216
|
+
this.systemPromptOverride = systemPromptOverride;
|
|
217
|
+
this.maxTurns = maxTurns || MAX_ITERATIONS;
|
|
218
|
+
this.stagnationDetection = stagnationDetection;
|
|
219
|
+
this.stagnationThreshold = stagnationThreshold;
|
|
220
|
+
this._cancelled = false;
|
|
221
|
+
this.promptCache = new PromptCache();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async *execute(instruction, context = {}) {
|
|
225
|
+
this._cancelled = false;
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
let toolCount = 0;
|
|
228
|
+
const usageTotals = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0 };
|
|
229
|
+
|
|
230
|
+
yield { type: 'status', data: { message: `Local mode: ${this.model}` } };
|
|
231
|
+
|
|
232
|
+
// Retrieve relevant code context via BM25 index
|
|
233
|
+
let retrievedContext = [];
|
|
234
|
+
try {
|
|
235
|
+
retrievedContext = this.retriever.retrieve(instruction, 8);
|
|
236
|
+
if (retrievedContext.length > 0) {
|
|
237
|
+
yield { type: 'status', data: { message: `Context: ${retrievedContext.length} relevant chunks from index` } };
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Index may not exist yet — that's fine, continue without context
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const tools = this._buildToolDefs();
|
|
244
|
+
const systemPrompt = this._buildSystemPrompt(context, retrievedContext);
|
|
245
|
+
const messages = [{ role: 'user', content: instruction }];
|
|
246
|
+
|
|
247
|
+
const stagnation = createStagnationTracker({
|
|
248
|
+
enabled: this.stagnationDetection,
|
|
249
|
+
threshold: this.stagnationThreshold,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < this.maxTurns; i++) {
|
|
253
|
+
if (this._cancelled) {
|
|
254
|
+
yield { type: 'cancelled', data: { reason: 'User cancelled' } };
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let response;
|
|
259
|
+
try {
|
|
260
|
+
response = await this._callLLM(systemPrompt, messages, tools);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
yield { type: 'error', data: { message: `LLM API error: ${err.message}`, fatal: true } };
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { content, stopReason, usage } = response;
|
|
267
|
+
|
|
268
|
+
if (usage) {
|
|
269
|
+
this.promptCache.updateStats(usage);
|
|
270
|
+
usageTotals.input_tokens += usage.input_tokens || 0;
|
|
271
|
+
usageTotals.output_tokens += usage.output_tokens || 0;
|
|
272
|
+
usageTotals.cache_read_tokens += usage.cache_read_input_tokens || 0;
|
|
273
|
+
usageTotals.cache_creation_tokens += usage.cache_creation_input_tokens || 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Process content blocks
|
|
277
|
+
let hasToolUse = false;
|
|
278
|
+
const assistantContent = [];
|
|
279
|
+
|
|
280
|
+
for (const block of content) {
|
|
281
|
+
if (block.type === 'text') {
|
|
282
|
+
yield { type: 'content', data: { text: block.text } };
|
|
283
|
+
assistantContent.push(block);
|
|
284
|
+
} else if (block.type === 'tool_use') {
|
|
285
|
+
hasToolUse = true;
|
|
286
|
+
toolCount++;
|
|
287
|
+
const { id, name, input } = block;
|
|
288
|
+
|
|
289
|
+
const stagnationResult = stagnation.record(name, input);
|
|
290
|
+
if (stagnationResult.detected) {
|
|
291
|
+
const message = stagnationMessage(name, stagnationResult.count);
|
|
292
|
+
yield { type: 'stagnation', data: { tool: name, count: stagnationResult.count, message } };
|
|
293
|
+
assistantContent.push(block);
|
|
294
|
+
messages.push({ role: 'assistant', content: assistantContent.slice() });
|
|
295
|
+
messages.push({
|
|
296
|
+
role: 'user',
|
|
297
|
+
content: [{ type: 'tool_result', tool_use_id: id, content: message }],
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
yield { type: 'tool_call', data: { call_id: id, tool: name, args: input } };
|
|
303
|
+
|
|
304
|
+
// Execute locally
|
|
305
|
+
let result;
|
|
306
|
+
try {
|
|
307
|
+
result = await this.toolExecutor.execute(name, input || {});
|
|
308
|
+
} catch (err) {
|
|
309
|
+
result = { success: false, output: `Error: ${err.message}` };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
yield { type: 'tool_done', data: { tool: name, duration_ms: 0 } };
|
|
313
|
+
|
|
314
|
+
assistantContent.push(block);
|
|
315
|
+
messages.push({ role: 'assistant', content: assistantContent.slice() });
|
|
316
|
+
messages.push({
|
|
317
|
+
role: 'user',
|
|
318
|
+
content: [{ type: 'tool_result', tool_use_id: id, content: result.output || JSON.stringify(result) }],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!hasToolUse || stopReason === 'end_turn') {
|
|
324
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
325
|
+
yield {
|
|
326
|
+
type: 'complete',
|
|
327
|
+
data: {
|
|
328
|
+
summary: 'Done (local)',
|
|
329
|
+
changes: toolCount,
|
|
330
|
+
duration_s: duration,
|
|
331
|
+
usage: _buildLocalUsageEnvelope(this.model, usageTotals),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
yield { type: 'error', data: { message: `Max turns (${this.maxTurns}) reached.` } };
|
|
339
|
+
yield {
|
|
340
|
+
type: 'complete',
|
|
341
|
+
data: {
|
|
342
|
+
summary: 'Aborted (max turns)',
|
|
343
|
+
changes: toolCount,
|
|
344
|
+
duration_s: (Date.now() - startTime) / 1000,
|
|
345
|
+
usage: _buildLocalUsageEnvelope(this.model, usageTotals),
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async _callLLM(systemPrompt, messages, tools) {
|
|
351
|
+
const isClaude = this.model.startsWith('claude') || this.model.startsWith('anthropic/claude');
|
|
352
|
+
|
|
353
|
+
// Use Anthropic direct API only for Claude models when we have an Anthropic key
|
|
354
|
+
if (isClaude && this.apiKey && this.apiKey.startsWith('sk-ant-')) {
|
|
355
|
+
return this._callClaude(systemPrompt, messages, tools);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Everything else goes through OpenRouter (DeepSeek, GPT, Gemini, or Claude via OR)
|
|
359
|
+
if (this.openRouterKey) {
|
|
360
|
+
return this._callOpenRouter(systemPrompt, messages, tools);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (this.apiKey) {
|
|
364
|
+
return this._callClaude(systemPrompt, messages, tools);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
throw new Error('No API key configured. Set ANTHROPIC_API_KEY or configure OpenRouter key.');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async _callClaude(systemPrompt, messages, tools) {
|
|
371
|
+
// PRD-071 Phase 2 — cache_control breakpoints for Anthropic direct.
|
|
372
|
+
// Extended 1-hour TTL beta on the persistent prefix (system + tools).
|
|
373
|
+
// Message history breakpoint stays at default 5-min TTL.
|
|
374
|
+
const cachedSystem = cacheableSystem(systemPrompt);
|
|
375
|
+
const cachedTools = cacheableTools(tools);
|
|
376
|
+
const cachedMessages = withMessageBreakpoint(messages);
|
|
377
|
+
|
|
378
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: {
|
|
381
|
+
'x-api-key': this.apiKey,
|
|
382
|
+
'anthropic-version': '2023-06-01',
|
|
383
|
+
'anthropic-beta': ANTHROPIC_BETA_HEADER,
|
|
384
|
+
'content-type': 'application/json',
|
|
385
|
+
},
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
model: this.model,
|
|
388
|
+
system: cachedSystem,
|
|
389
|
+
messages: cachedMessages,
|
|
390
|
+
tools: cachedTools.length > 0 ? cachedTools : undefined,
|
|
391
|
+
max_tokens: 8192,
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
if (!resp.ok) {
|
|
395
|
+
const text = await resp.text().catch(() => '');
|
|
396
|
+
throw new Error(`Claude API ${resp.status}: ${text.slice(0, 200)}`);
|
|
397
|
+
}
|
|
398
|
+
const data = await resp.json();
|
|
399
|
+
return { content: data.content || [], stopReason: data.stop_reason, usage: data.usage || null };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async _callOpenRouter(systemPrompt, messages, tools) {
|
|
403
|
+
// OpenRouter requires provider prefix (e.g. anthropic/claude-sonnet-4-20250514)
|
|
404
|
+
let model = this.model;
|
|
405
|
+
if (model.startsWith('claude') && !model.includes('/')) {
|
|
406
|
+
model = `anthropic/${model}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// PRD-071 Phase 2 — for anthropic/* models we need explicit cache_control
|
|
410
|
+
// breakpoints (OpenRouter relays them through to Anthropic). OpenAI +
|
|
411
|
+
// DeepSeek auto-cache, so the string system prompt path is fine there.
|
|
412
|
+
const isAnthropic = needsExplicitCacheControl(model);
|
|
413
|
+
const systemForOR = isAnthropic
|
|
414
|
+
? { role: 'system', content: cacheableSystem(systemPrompt) }
|
|
415
|
+
: { role: 'system', content: systemPrompt };
|
|
416
|
+
const messagesForOR = isAnthropic ? withMessageBreakpoint(messages) : messages;
|
|
417
|
+
const orMessages = [systemForOR, ...messagesForOR];
|
|
418
|
+
|
|
419
|
+
// Same tool-schema shape as before, but tag the last tool for Anthropic
|
|
420
|
+
// (OpenRouter passes cache_control on function tools through).
|
|
421
|
+
const orTools = tools.length > 0
|
|
422
|
+
? tools.map(t => ({
|
|
423
|
+
type: 'function',
|
|
424
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
425
|
+
}))
|
|
426
|
+
: [];
|
|
427
|
+
const finalTools = isAnthropic ? cacheableTools(orTools) : orTools;
|
|
428
|
+
|
|
429
|
+
const headers = {
|
|
430
|
+
'Authorization': `Bearer ${this.openRouterKey}`,
|
|
431
|
+
'Content-Type': 'application/json',
|
|
432
|
+
};
|
|
433
|
+
// OpenRouter forwards `anthropic-beta` to Anthropic upstreams.
|
|
434
|
+
if (isAnthropic) headers['anthropic-beta'] = ANTHROPIC_BETA_HEADER;
|
|
435
|
+
|
|
436
|
+
const resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers,
|
|
439
|
+
body: JSON.stringify({
|
|
440
|
+
model,
|
|
441
|
+
messages: orMessages,
|
|
442
|
+
tools: finalTools.length > 0 ? finalTools : undefined,
|
|
443
|
+
// Ask OpenRouter to include upstream cache accounting in the
|
|
444
|
+
// usage payload so PromptCache.updateStats() sees real numbers.
|
|
445
|
+
usage: { include: true },
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
if (!resp.ok) {
|
|
449
|
+
const text = await resp.text().catch(() => '');
|
|
450
|
+
throw new Error(`OpenRouter API ${resp.status}: ${text.slice(0, 200)}`);
|
|
451
|
+
}
|
|
452
|
+
const data = await resp.json();
|
|
453
|
+
const choice = data.choices?.[0];
|
|
454
|
+
const content = [];
|
|
455
|
+
if (choice?.message?.content) content.push({ type: 'text', text: choice.message.content });
|
|
456
|
+
if (choice?.message?.tool_calls) {
|
|
457
|
+
for (const tc of choice.message.tool_calls) {
|
|
458
|
+
content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments || '{}') });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
content,
|
|
463
|
+
stopReason: choice?.finish_reason === 'stop' ? 'end_turn' : 'tool_use',
|
|
464
|
+
usage: _normalizeOpenRouterUsage(data.usage),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
_buildToolDefs() {
|
|
469
|
+
return TOOL_SCHEMAS;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
_buildSystemPrompt(context, retrievedContext = null) {
|
|
473
|
+
if (this.systemPromptOverride) {
|
|
474
|
+
return this.systemPromptOverride;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const parts = [
|
|
478
|
+
'You are Bahulam Code, Bahulam\'s AI coding agent running in local mode.',
|
|
479
|
+
'You have access to tools for reading, writing, and executing code.',
|
|
480
|
+
'Use tools to accomplish the user\'s request. Be concise and direct.',
|
|
481
|
+
];
|
|
482
|
+
if (context.cwd) parts.push(`Working directory: ${context.cwd}`);
|
|
483
|
+
if (context.gitBranch) parts.push(`Git branch: ${context.gitBranch}`);
|
|
484
|
+
|
|
485
|
+
if (retrievedContext && retrievedContext.length > 0) {
|
|
486
|
+
parts.push('');
|
|
487
|
+
parts.push('== RELEVANT CODE CONTEXT (from project index) ==');
|
|
488
|
+
for (const chunk of retrievedContext) {
|
|
489
|
+
parts.push(`--- ${chunk.id} (score: ${chunk.score.toFixed(2)}) ---`);
|
|
490
|
+
parts.push(chunk.text);
|
|
491
|
+
parts.push('');
|
|
492
|
+
}
|
|
493
|
+
parts.push('== END CONTEXT ==');
|
|
494
|
+
parts.push('');
|
|
495
|
+
parts.push('Use the above context to understand the codebase. Read full files with read_file when you need more detail.');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return parts.join('\n');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
cancel() { this._cancelled = true; }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Shape the accumulated per-turn totals into the same envelope the remote
|
|
505
|
+
// SSE `complete` event uses, so repl.mjs:837 can consume both modes with the
|
|
506
|
+
// same code path. `models[0].role = 'local'` distinguishes single-agent
|
|
507
|
+
// local mode from remote's Coder/Explorer/Planner breakdown.
|
|
508
|
+
function _buildLocalUsageEnvelope(model, totals) {
|
|
509
|
+
return {
|
|
510
|
+
total_input_tokens: totals.input_tokens,
|
|
511
|
+
total_output_tokens: totals.output_tokens,
|
|
512
|
+
models: [{
|
|
513
|
+
model,
|
|
514
|
+
role: 'local',
|
|
515
|
+
input_tokens: totals.input_tokens,
|
|
516
|
+
output_tokens: totals.output_tokens,
|
|
517
|
+
cache_read_tokens: totals.cache_read_tokens,
|
|
518
|
+
cache_creation_tokens: totals.cache_creation_tokens,
|
|
519
|
+
}],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Normalize OpenRouter usage into Anthropic's field names so downstream
|
|
524
|
+
// consumers (PromptCache, pricing.calculateCost) don't branch on shape.
|
|
525
|
+
// OpenRouter returns OpenAI-style: prompt_tokens, completion_tokens,
|
|
526
|
+
// prompt_tokens_details.cached_tokens. When the underlying model is
|
|
527
|
+
// Anthropic, OpenRouter also relays cache_read_input_tokens verbatim.
|
|
528
|
+
function _normalizeOpenRouterUsage(usage) {
|
|
529
|
+
if (!usage) return null;
|
|
530
|
+
const cachedFromOpenAI = usage.prompt_tokens_details?.cached_tokens || 0;
|
|
531
|
+
return {
|
|
532
|
+
input_tokens: usage.prompt_tokens || 0,
|
|
533
|
+
output_tokens: usage.completion_tokens || 0,
|
|
534
|
+
cache_read_input_tokens: usage.cache_read_input_tokens || cachedFromOpenAI || 0,
|
|
535
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
|
|
536
|
+
};
|
|
537
|
+
}
|