ubume 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# `src/core`
|
|
2
|
+
|
|
3
|
+
Non-UI runtime logic for Ubume: launching backends, talking to provider CLIs,
|
|
4
|
+
terminal I/O, workspace resolution, and supporting utilities. UI lives in `src/ui`,
|
|
5
|
+
app state in `src/session`, config in `src/config`. The top level of `src/core` is
|
|
6
|
+
folders-only — every file lives in a domain folder so the directory stays scannable.
|
|
7
|
+
|
|
8
|
+
## Folder map
|
|
9
|
+
|
|
10
|
+
| Folder | Responsibility |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `providers/` | **Low-level Codex subprocess I/O.** Spawns the `codex` binary and parses its output (`codexSubprocess`, `codexJsonStream`, `codexTranscript`), plus the backend registry/types and the `openaiNative` stub. |
|
|
13
|
+
| `providerRuntime/` | **Multi-provider runtimes + discovery.** One runtime per provider (`anthropic`, `gemini`, `local`, `antigravity`) behind a shared interface, plus routing (`registry`), model/metadata helpers (`models`, `capabilityProfile`, `contextMetadata`, `reasoning`) and Claude Code discovery. |
|
|
14
|
+
| `providerLauncher/` | **Workspace provider config + CLI launching.** Which provider is active per workspace (`workspaceConfig`), provider UI state (`registry`), and spawning provider CLIs (`launcher`). |
|
|
15
|
+
| `codex/` | Codex CLI launch/prompt assembly: `codexExecArgs`, `codexLaunch`, `codexPrompt`. |
|
|
16
|
+
| `models/` | Codex CLI capability discovery (`codexCapabilities`, `codexModelCapabilities`) and the legacy model-spec service (`modelSpecs`). |
|
|
17
|
+
| `executables/` | Resolve external CLI binaries with PATH/env handling (`executableResolver` + per-CLI resolvers). |
|
|
18
|
+
| `auth/` | Codex auth status probing. |
|
|
19
|
+
| `process/` | Generic process spawning (`CommandRunner`) and executable-path validation. |
|
|
20
|
+
| `terminal/` | Terminal I/O: ANSI sanitize, raw mode / cursor, title sequences, capability detection, and the `/clear` + resize repaint boundary (`clearFrameBoundary`, `inkRenderReset`). |
|
|
21
|
+
| `workspace/` | Workspace resolution and state: `workspaceRoot`, `workspaceGuard`, `workspaceActivity`, `projectInstructions`, `planStorage`, `launchContext`. |
|
|
22
|
+
| `version/` | Build channel / version branding (`channel`) and update checking (`updateCheck`). |
|
|
23
|
+
| `shared/` | Small cross-cutting utilities: `clipboard`, `cleanupFastFail`, `githubDiagnostics`, `attachments`, `hollowResponseFormat`. |
|
|
24
|
+
| `perf/` | Performance + render instrumentation (`profiler`, `renderDebug`). |
|
|
25
|
+
| `debug/` | Dev-only tracing helpers (`inputDebug`). |
|
|
26
|
+
|
|
27
|
+
## The three provider layers
|
|
28
|
+
|
|
29
|
+
`providers/`, `providerRuntime/`, and `providerLauncher/` have similar names but are
|
|
30
|
+
distinct layers — they are **not** duplicates:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
providerLauncher/ which provider is active for this workspace + how to spawn its CLI
|
|
34
|
+
│
|
|
35
|
+
providerRuntime/ per-provider runtimes (anthropic/gemini/local/antigravity),
|
|
36
|
+
│ routing, model discovery, capability/context metadata
|
|
37
|
+
│
|
|
38
|
+
providers/ low-level Codex subprocess I/O + output parsing
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The default Codex backend flows through `providers/`; the other providers are
|
|
42
|
+
implemented as `providerRuntime/` runtimes.
|
|
43
|
+
|
|
44
|
+
## Debug instrumentation
|
|
45
|
+
|
|
46
|
+
These are intentional, env-gated diagnostics (not dead code) — keep them named clearly:
|
|
47
|
+
|
|
48
|
+
- `debug/inputDebug.ts` — stdin state tracing (`UBUME_DEBUG_INPUT=1`).
|
|
49
|
+
- `debug/localStreamDebug.ts` — privacy-aware Local streaming diagnostics
|
|
50
|
+
(`UBUME_DEBUG_LOCAL_STREAM=1`; add `UBUME_DEBUG_LOCAL_STREAM_CONTENT=1`
|
|
51
|
+
only when response text is safe to record).
|
|
52
|
+
- `perf/renderDebug.ts` — Ink render/flicker tracing (`UBUME_RENDER_DEBUG=1`). Kept in
|
|
53
|
+
`perf/` rather than `debug/` because it is imported widely across the UI.
|
|
54
|
+
- `providerRuntime/claudeCodeDiscoveryDebug.ts` — entry point for the
|
|
55
|
+
`bun run debug:claude-models` script; lives next to the discovery code it exercises.
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BackendRunHandlers } from "../providers/types.js";
|
|
4
|
+
import type { ProviderChatRequest } from "../providerRuntime/types.js";
|
|
5
|
+
import { executeAgentTool, type AgentToolResult } from "./tools.js";
|
|
6
|
+
import {
|
|
7
|
+
parseAgentToolCall,
|
|
8
|
+
serializeToolResult,
|
|
9
|
+
type MalformedOpenAiToolCall,
|
|
10
|
+
type NormalizedAgentToolCall,
|
|
11
|
+
} from "./protocol.js";
|
|
12
|
+
|
|
13
|
+
export interface AgentChatToolCall {
|
|
14
|
+
id: string;
|
|
15
|
+
type: "function";
|
|
16
|
+
function: { name: string; arguments: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AgentChatMessage =
|
|
20
|
+
| { role: "system" | "user"; content: string }
|
|
21
|
+
| { role: "assistant"; content: string | null; reasoning_content?: string; tool_calls?: readonly AgentChatToolCall[] }
|
|
22
|
+
| { role: "tool"; content: string; tool_call_id: string };
|
|
23
|
+
|
|
24
|
+
export interface AgentChatResponse {
|
|
25
|
+
text: string;
|
|
26
|
+
reasoning?: string;
|
|
27
|
+
toolCalls?: readonly NormalizedAgentToolCall[];
|
|
28
|
+
malformedToolCalls?: readonly MalformedOpenAiToolCall[];
|
|
29
|
+
finishReason?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RunAgentLoopOptions {
|
|
33
|
+
request: ProviderChatRequest;
|
|
34
|
+
handlers: BackendRunHandlers;
|
|
35
|
+
sendMessages: (messages: readonly AgentChatMessage[], turnIndex: number) => Promise<AgentChatResponse>;
|
|
36
|
+
includeSystemPrompt: boolean;
|
|
37
|
+
toolProtocol?: "none" | "text" | "openai";
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
maxConsecutiveNoProgressCalls?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_MAX_CONSECUTIVE_NO_PROGRESS_CALLS = 3;
|
|
43
|
+
|
|
44
|
+
function workspaceSummary(workspaceRoot: string): string {
|
|
45
|
+
const lines = [`Workspace root: ${workspaceRoot}`];
|
|
46
|
+
try {
|
|
47
|
+
const entries = readdirSync(workspaceRoot, { withFileTypes: true })
|
|
48
|
+
.filter((entry) => ![".git", "node_modules", "dist", "build", "coverage"].includes(entry.name))
|
|
49
|
+
.map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`)
|
|
50
|
+
.sort()
|
|
51
|
+
.slice(0, 20);
|
|
52
|
+
if (entries.length > 0) lines.push(`Top-level entries: ${entries.join(", ")}`);
|
|
53
|
+
} catch {
|
|
54
|
+
// Tool-based inspection remains available when a shallow summary is unavailable.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const packageJsonPath = path.join(workspaceRoot, "package.json");
|
|
59
|
+
if (existsSync(packageJsonPath)) {
|
|
60
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as Record<string, unknown>;
|
|
61
|
+
const name = typeof packageJson.name === "string" ? packageJson.name : null;
|
|
62
|
+
const description = typeof packageJson.description === "string" ? packageJson.description : null;
|
|
63
|
+
if (name) lines.push(`Package: ${name}${description ? ` - ${description}` : ""}`);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// A malformed package.json should not prevent a local chat request.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function localAgentSystemPrompt(request: ProviderChatRequest, toolProtocol: "none" | "text" | "openai"): string {
|
|
73
|
+
const hasCargoToml = existsSync(path.join(request.workspaceRoot, "Cargo.toml"));
|
|
74
|
+
const planning = request.runIntent === "plan";
|
|
75
|
+
return [
|
|
76
|
+
`You are an autonomous coding assistant running inside this workspace: ${request.workspaceRoot}`,
|
|
77
|
+
"You must inspect files with tools before claiming you cannot see them.",
|
|
78
|
+
"For broad questions about the repository, use the workspace summary below, then inspect with get_workspace_info or list_files before answering when more detail is needed.",
|
|
79
|
+
planning
|
|
80
|
+
? "PLAN MODE: inspect the repository and return a concrete Markdown implementation plan. Do not write files, apply patches, or run shell commands."
|
|
81
|
+
: "Use tools to create, edit, build, and test when the user asks for workspace changes.",
|
|
82
|
+
planning
|
|
83
|
+
? null
|
|
84
|
+
: "When the user explicitly asks you to commit, push, or open a pull request, complete those actions with tools when runtime policy permits. Do not replace unfinished authorized work with commands for the user to run.",
|
|
85
|
+
"Keep inspecting and acting until the requested work is complete or a concrete external blocker prevents progress. Never mention internal tool budgets or claim a file, commit, push, or pull request exists unless a tool confirmed it.",
|
|
86
|
+
"Do not ask vague clarification questions when the user's intent has an obvious safe implementation.",
|
|
87
|
+
hasCargoToml
|
|
88
|
+
? "Rust workspace note: Cargo.toml exists. Prefer src/main.rs for simple binaries, use cargo check for validation, use cargo run for running, and do not use rustc main.rs unless main.rs is truly at the workspace root."
|
|
89
|
+
: null,
|
|
90
|
+
toolProtocol === "text" ? "Use exactly one tool call at a time in this format:" : null,
|
|
91
|
+
toolProtocol === "text" ? '<tool_call>{"name":"read_file","arguments":{"path":"src/index.tsx"}}</tool_call>' : null,
|
|
92
|
+
toolProtocol === "text"
|
|
93
|
+
? planning
|
|
94
|
+
? "Available tools: list_files, read_file, get_workspace_info."
|
|
95
|
+
: "Available tools: list_files, read_file, write_file, apply_patch, run_shell, get_workspace_info."
|
|
96
|
+
: toolProtocol === "openai"
|
|
97
|
+
? "Use the provided API tools when workspace inspection or action is required."
|
|
98
|
+
: "No workspace tools are available for this model.",
|
|
99
|
+
"Summarize changed files and commands run in your final answer.",
|
|
100
|
+
`Workspace summary:\n${workspaceSummary(request.workspaceRoot)}`,
|
|
101
|
+
request.projectInstructions?.content
|
|
102
|
+
? ["Project instructions:", request.projectInstructions.content].join("\n")
|
|
103
|
+
: null,
|
|
104
|
+
].filter(Boolean).join("\n\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildInitialMessages(
|
|
108
|
+
request: ProviderChatRequest,
|
|
109
|
+
includeSystemPrompt: boolean,
|
|
110
|
+
toolProtocol: "none" | "text" | "openai",
|
|
111
|
+
): AgentChatMessage[] {
|
|
112
|
+
const systemPrompt = localAgentSystemPrompt(request, toolProtocol);
|
|
113
|
+
if (includeSystemPrompt) {
|
|
114
|
+
return [
|
|
115
|
+
{ role: "system", content: systemPrompt },
|
|
116
|
+
...(request.conversationHistory ?? []).map((message) => ({ role: message.role, content: message.content })),
|
|
117
|
+
{ role: "user", content: request.prompt },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
...(request.conversationHistory ?? []).map((message) => ({ role: message.role, content: message.content })),
|
|
123
|
+
{ role: "user", content: `${systemPrompt}\n\nUser request:\n${request.prompt}` },
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toolActivityCommand(result: Pick<AgentToolResult, "tool" | "path" | "paths" | "command">): string {
|
|
128
|
+
if (result.command) return `${result.tool}: ${result.command}`;
|
|
129
|
+
if (result.path) return `${result.tool}: ${result.path}`;
|
|
130
|
+
if (result.paths && result.paths.length > 0) return `${result.tool}: ${result.paths.join(", ")}`;
|
|
131
|
+
return result.tool;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface ExecutedCommand {
|
|
135
|
+
command: string;
|
|
136
|
+
success: boolean;
|
|
137
|
+
exitCode?: number | null;
|
|
138
|
+
durationMs?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface AgentLoopSummary {
|
|
142
|
+
changedFiles: Set<string>;
|
|
143
|
+
commands: ExecutedCommand[];
|
|
144
|
+
toolResults: AgentToolResult[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stableJson(value: unknown): string {
|
|
148
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
149
|
+
if (value && typeof value === "object") {
|
|
150
|
+
const record = value as Record<string, unknown>;
|
|
151
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
152
|
+
}
|
|
153
|
+
return JSON.stringify(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toolCallSignature(call: Pick<NormalizedAgentToolCall, "name" | "arguments">): string {
|
|
157
|
+
return `${call.name}:${stableJson(call.arguments)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toolResultFingerprint(result: AgentToolResult): string {
|
|
161
|
+
const { durationMs: _durationMs, ...stableResult } = result;
|
|
162
|
+
return stableJson(stableResult);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function recordToolResult(summary: AgentLoopSummary, result: AgentToolResult): void {
|
|
166
|
+
for (const file of result.paths ?? []) {
|
|
167
|
+
if (file) summary.changedFiles.add(file);
|
|
168
|
+
}
|
|
169
|
+
if (result.path && (result.tool === "write_file" || result.tool === "apply_patch")) {
|
|
170
|
+
summary.changedFiles.add(result.path);
|
|
171
|
+
}
|
|
172
|
+
if (result.command) {
|
|
173
|
+
summary.commands.push({
|
|
174
|
+
command: result.command,
|
|
175
|
+
success: result.success,
|
|
176
|
+
exitCode: result.exitCode,
|
|
177
|
+
durationMs: result.durationMs,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
summary.toolResults.push(result);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function commandStatus(command: ExecutedCommand): string {
|
|
184
|
+
const status = command.success ? "succeeded" : "failed";
|
|
185
|
+
const exitCode = command.exitCode === undefined ? "" : `, exit ${command.exitCode ?? "n/a"}`;
|
|
186
|
+
return `- ${command.command}: ${status}${exitCode}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function synthesizeFinalMessage(_request: ProviderChatRequest, summary: AgentLoopSummary, reason: string): string {
|
|
190
|
+
const files = [...summary.changedFiles].sort();
|
|
191
|
+
const commandLines = summary.commands.map(commandStatus);
|
|
192
|
+
return [
|
|
193
|
+
reason,
|
|
194
|
+
"",
|
|
195
|
+
"Files changed:",
|
|
196
|
+
files.length > 0 ? files.map((file) => `- ${file}`).join("\n") : "- None detected",
|
|
197
|
+
"",
|
|
198
|
+
"Commands run:",
|
|
199
|
+
commandLines.length > 0 ? commandLines.join("\n") : "- None",
|
|
200
|
+
].join("\n").trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function requestFinalAnswer(options: RunAgentLoopOptions, messages: AgentChatMessage[], toolCallCount: number, reason: string): Promise<string | null> {
|
|
204
|
+
messages.push({
|
|
205
|
+
role: "user",
|
|
206
|
+
content: [
|
|
207
|
+
reason,
|
|
208
|
+
"Repeated tool calls are no longer changing the result. Stop calling tools and report the exact blocker using only confirmed tool results.",
|
|
209
|
+
"Include files changed and commands run with their outcomes. Do not expose internal loop controls, claim unfinished work is complete, or delegate runnable actions to the user unless the required capability is genuinely unavailable.",
|
|
210
|
+
].join("\n"),
|
|
211
|
+
});
|
|
212
|
+
const response = await options.sendMessages(messages, toolCallCount);
|
|
213
|
+
if ((response.toolCalls?.length ?? 0) > 0 || (response.malformedToolCalls?.length ?? 0) > 0) return null;
|
|
214
|
+
const parsed = parseAgentToolCall(response.text);
|
|
215
|
+
return parsed.kind === "final" && parsed.text.trim() ? parsed.text.trim() : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function wireToolCall(call: NormalizedAgentToolCall, id: string): AgentChatToolCall {
|
|
219
|
+
return {
|
|
220
|
+
id,
|
|
221
|
+
type: "function",
|
|
222
|
+
function: {
|
|
223
|
+
name: call.name,
|
|
224
|
+
arguments: call.rawArguments || JSON.stringify(call.arguments),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function malformedWireToolCall(call: MalformedOpenAiToolCall, id: string): AgentChatToolCall {
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
type: "function",
|
|
233
|
+
function: {
|
|
234
|
+
name: call.name ?? "unknown",
|
|
235
|
+
arguments: call.rawArguments || "{}",
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function appendToolResultMessage(
|
|
241
|
+
messages: AgentChatMessage[],
|
|
242
|
+
native: boolean,
|
|
243
|
+
toolCallId: string,
|
|
244
|
+
result: unknown,
|
|
245
|
+
): void {
|
|
246
|
+
const content = serializeToolResult(result);
|
|
247
|
+
messages.push(native
|
|
248
|
+
? { role: "tool", tool_call_id: toolCallId, content }
|
|
249
|
+
: { role: "user", content });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function runAgentLoop(options: RunAgentLoopOptions): Promise<string> {
|
|
253
|
+
const maxConsecutiveNoProgressCalls = Math.max(
|
|
254
|
+
1,
|
|
255
|
+
Math.floor(options.maxConsecutiveNoProgressCalls ?? DEFAULT_MAX_CONSECUTIVE_NO_PROGRESS_CALLS),
|
|
256
|
+
);
|
|
257
|
+
const toolProtocol = options.toolProtocol ?? "text";
|
|
258
|
+
const messages = buildInitialMessages(options.request, options.includeSystemPrompt, toolProtocol);
|
|
259
|
+
let toolCallCount = 0;
|
|
260
|
+
const completedToolCallIds = new Set<string>();
|
|
261
|
+
const previousToolResults = new Map<string, string>();
|
|
262
|
+
let consecutiveNoProgressCalls = 0;
|
|
263
|
+
const approvedForRun = new Set<string>();
|
|
264
|
+
const summary: AgentLoopSummary = {
|
|
265
|
+
changedFiles: new Set(),
|
|
266
|
+
commands: [],
|
|
267
|
+
toolResults: [],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
while (true) {
|
|
271
|
+
if (options.signal?.aborted) {
|
|
272
|
+
throw new Error("Local agent run was canceled.");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const response = await options.sendMessages(messages, toolCallCount);
|
|
276
|
+
const structuredCalls = [...(response.toolCalls ?? [])];
|
|
277
|
+
const malformedCalls = [...(response.malformedToolCalls ?? [])];
|
|
278
|
+
const native = structuredCalls.length > 0 || malformedCalls.length > 0;
|
|
279
|
+
let calls = structuredCalls;
|
|
280
|
+
let textMalformed: { error: string; raw: string } | null = null;
|
|
281
|
+
|
|
282
|
+
if (!native) {
|
|
283
|
+
const parsed = parseAgentToolCall(response.text);
|
|
284
|
+
if (parsed.kind === "final") {
|
|
285
|
+
if (response.finishReason === "tool_calls") {
|
|
286
|
+
textMalformed = { error: "Completion ended with finish_reason=tool_calls but contained no tool calls.", raw: response.text };
|
|
287
|
+
} else {
|
|
288
|
+
return parsed.text.trim();
|
|
289
|
+
}
|
|
290
|
+
} else if (parsed.kind === "malformed_tool_call") {
|
|
291
|
+
textMalformed = { error: parsed.error, raw: parsed.raw };
|
|
292
|
+
} else {
|
|
293
|
+
calls = [{
|
|
294
|
+
id: parsed.id,
|
|
295
|
+
name: parsed.name,
|
|
296
|
+
arguments: parsed.arguments,
|
|
297
|
+
rawArguments: parsed.rawArguments,
|
|
298
|
+
}];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (toolProtocol === "none" && calls.length > 0) {
|
|
303
|
+
textMalformed = { error: "Tool calls are disabled by the selected model capability profile.", raw: response.text };
|
|
304
|
+
calls = [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const callIds = calls.map((call, index) => call.id ?? `local-call-${toolCallCount + index + 1}`);
|
|
308
|
+
const malformedIds = malformedCalls.map((call, index) => call.id ?? `local-malformed-${toolCallCount + index + 1}`);
|
|
309
|
+
if (native) {
|
|
310
|
+
messages.push({
|
|
311
|
+
role: "assistant",
|
|
312
|
+
content: response.text || null,
|
|
313
|
+
...(response.reasoning?.trim() ? { reasoning_content: response.reasoning } : {}),
|
|
314
|
+
tool_calls: [
|
|
315
|
+
...calls.map((call, index) => wireToolCall(call, callIds[index]!)),
|
|
316
|
+
...malformedCalls.map((call, index) => malformedWireToolCall(call, malformedIds[index]!)),
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
messages.push({ role: "assistant", content: response.text });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (textMalformed) {
|
|
324
|
+
toolCallCount += 1;
|
|
325
|
+
appendToolResultMessage(messages, false, "", {
|
|
326
|
+
success: false,
|
|
327
|
+
error: `Malformed tool call: ${textMalformed.error}`,
|
|
328
|
+
raw: textMalformed.raw,
|
|
329
|
+
});
|
|
330
|
+
consecutiveNoProgressCalls += 1;
|
|
331
|
+
if (consecutiveNoProgressCalls >= maxConsecutiveNoProgressCalls) {
|
|
332
|
+
const reason = "The Local agent repeated malformed tool calls without making progress.";
|
|
333
|
+
const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
|
|
334
|
+
return final ?? synthesizeFinalMessage(options.request, summary, reason);
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let index = 0; index < malformedCalls.length; index += 1) {
|
|
340
|
+
const malformed = malformedCalls[index]!;
|
|
341
|
+
toolCallCount += 1;
|
|
342
|
+
appendToolResultMessage(messages, true, malformedIds[index]!, {
|
|
343
|
+
success: false,
|
|
344
|
+
tool: malformed.name ?? "unknown",
|
|
345
|
+
error: `Malformed tool call: ${malformed.error}`,
|
|
346
|
+
raw: malformed.rawArguments,
|
|
347
|
+
});
|
|
348
|
+
consecutiveNoProgressCalls += 1;
|
|
349
|
+
}
|
|
350
|
+
if (consecutiveNoProgressCalls >= maxConsecutiveNoProgressCalls) {
|
|
351
|
+
const reason = "The Local agent repeated malformed tool calls without making progress.";
|
|
352
|
+
const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
|
|
353
|
+
return final ?? synthesizeFinalMessage(options.request, summary, reason);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (let index = 0; index < calls.length; index += 1) {
|
|
357
|
+
const call = calls[index]!;
|
|
358
|
+
const callId = callIds[index]!;
|
|
359
|
+
const signature = toolCallSignature(call);
|
|
360
|
+
if (completedToolCallIds.has(callId)) {
|
|
361
|
+
const reason = `Local agent replayed completed tool call ID ${callId}.`;
|
|
362
|
+
appendToolResultMessage(messages, native, callId, {
|
|
363
|
+
success: false,
|
|
364
|
+
tool: call.name,
|
|
365
|
+
error: reason,
|
|
366
|
+
});
|
|
367
|
+
consecutiveNoProgressCalls += 1;
|
|
368
|
+
if (consecutiveNoProgressCalls >= maxConsecutiveNoProgressCalls) {
|
|
369
|
+
const finalReason = "The Local agent replayed completed tool calls without making progress.";
|
|
370
|
+
const final = await requestFinalAnswer(options, messages, toolCallCount, finalReason);
|
|
371
|
+
return final ?? synthesizeFinalMessage(options.request, summary, finalReason);
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
completedToolCallIds.add(callId);
|
|
377
|
+
toolCallCount += 1;
|
|
378
|
+
const activityId = `local-agent-${toolCallCount}-${call.name}`;
|
|
379
|
+
const startedAt = Date.now();
|
|
380
|
+
const runningCommand = toolActivityCommand({
|
|
381
|
+
tool: call.name,
|
|
382
|
+
path: typeof call.arguments.path === "string" ? call.arguments.path : undefined,
|
|
383
|
+
command: typeof call.arguments.command === "string" ? call.arguments.command : undefined,
|
|
384
|
+
});
|
|
385
|
+
const mutating = call.name === "write_file" || call.name === "apply_patch" || call.name === "run_shell";
|
|
386
|
+
let deniedReason: string | null = null;
|
|
387
|
+
if (mutating && options.request.runIntent === "plan") {
|
|
388
|
+
deniedReason = "This tool is unavailable in Plan mode. Continue with read-only inspection and return a plan.";
|
|
389
|
+
} else if (
|
|
390
|
+
mutating
|
|
391
|
+
&& options.request.runtime.policy.approvalPolicy !== "never"
|
|
392
|
+
&& !approvedForRun.has(signature)
|
|
393
|
+
) {
|
|
394
|
+
const rawPath = typeof call.arguments.path === "string" ? call.arguments.path : null;
|
|
395
|
+
const patchPaths = call.name === "apply_patch" && typeof call.arguments.patch === "string"
|
|
396
|
+
? [...call.arguments.patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File:\s*(.+)$/gm)].map((match) => match[1]!.trim())
|
|
397
|
+
: [];
|
|
398
|
+
const decision = await options.handlers.onToolApproval?.({
|
|
399
|
+
tool: call.name,
|
|
400
|
+
signature,
|
|
401
|
+
command: typeof call.arguments.command === "string" ? call.arguments.command : undefined,
|
|
402
|
+
paths: rawPath ? [rawPath] : patchPaths,
|
|
403
|
+
}) ?? "deny";
|
|
404
|
+
if (decision === "deny") deniedReason = "User denied this local-model action.";
|
|
405
|
+
if (decision === "allow-for-run") approvedForRun.add(signature);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (deniedReason) {
|
|
409
|
+
const denied: AgentToolResult = { success: false, tool: call.name, error: deniedReason };
|
|
410
|
+
options.handlers.onToolActivity?.({
|
|
411
|
+
id: activityId,
|
|
412
|
+
command: runningCommand,
|
|
413
|
+
status: "failed",
|
|
414
|
+
startedAt,
|
|
415
|
+
completedAt: Date.now(),
|
|
416
|
+
summary: deniedReason,
|
|
417
|
+
});
|
|
418
|
+
recordToolResult(summary, denied);
|
|
419
|
+
appendToolResultMessage(messages, native, callId, denied);
|
|
420
|
+
const deniedFingerprint = toolResultFingerprint(denied);
|
|
421
|
+
const previousDenied = previousToolResults.get(signature);
|
|
422
|
+
previousToolResults.set(signature, deniedFingerprint);
|
|
423
|
+
consecutiveNoProgressCalls = previousDenied === deniedFingerprint
|
|
424
|
+
? consecutiveNoProgressCalls + 1
|
|
425
|
+
: 0;
|
|
426
|
+
if (consecutiveNoProgressCalls >= maxConsecutiveNoProgressCalls) {
|
|
427
|
+
const reason = "The Local agent repeated denied tool calls without making progress.";
|
|
428
|
+
const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
|
|
429
|
+
return final ?? synthesizeFinalMessage(options.request, summary, reason);
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
options.handlers.onToolActivity?.({ id: activityId, command: runningCommand, status: "running", startedAt });
|
|
435
|
+
const result = await executeAgentTool(call.name, call.arguments, {
|
|
436
|
+
workspaceRoot: options.request.workspaceRoot,
|
|
437
|
+
runtime: options.request.runtime,
|
|
438
|
+
signal: options.signal,
|
|
439
|
+
});
|
|
440
|
+
const completedCommand = toolActivityCommand(result);
|
|
441
|
+
options.handlers.onToolActivity?.({
|
|
442
|
+
id: activityId,
|
|
443
|
+
command: completedCommand,
|
|
444
|
+
status: result.success ? "completed" : "failed",
|
|
445
|
+
startedAt,
|
|
446
|
+
completedAt: Date.now(),
|
|
447
|
+
summary: result.summary ?? result.error ?? null,
|
|
448
|
+
});
|
|
449
|
+
recordToolResult(summary, result);
|
|
450
|
+
appendToolResultMessage(messages, native, callId, result);
|
|
451
|
+
const resultFingerprint = toolResultFingerprint(result);
|
|
452
|
+
const previousResult = previousToolResults.get(signature);
|
|
453
|
+
previousToolResults.set(signature, resultFingerprint);
|
|
454
|
+
consecutiveNoProgressCalls = previousResult === resultFingerprint
|
|
455
|
+
? consecutiveNoProgressCalls + 1
|
|
456
|
+
: 0;
|
|
457
|
+
if (consecutiveNoProgressCalls >= maxConsecutiveNoProgressCalls) {
|
|
458
|
+
const reason = "The Local agent repeated tool calls with unchanged results and could not make further progress.";
|
|
459
|
+
const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
|
|
460
|
+
return final ?? synthesizeFinalMessage(options.request, summary, reason);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|