@vetala/vetala 0.5.4 → 0.5.5
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 +6 -46
- package/dist/src/agent.d.ts +20 -1
- package/dist/src/agent.js +277 -116
- package/dist/src/agent.js.map +1 -1
- package/dist/src/app-meta.d.ts +1 -1
- package/dist/src/app-meta.js +1 -1
- package/dist/src/context-files.js +1 -0
- package/dist/src/context-files.js.map +1 -1
- package/dist/src/ipc-backend.js +97 -9
- package/dist/src/ipc-backend.js.map +1 -1
- package/dist/src/ipc-ui.d.ts +3 -1
- package/dist/src/ipc-ui.js +9 -2
- package/dist/src/ipc-ui.js.map +1 -1
- package/dist/src/process-utils.d.ts +1 -0
- package/dist/src/process-utils.js +72 -7
- package/dist/src/process-utils.js.map +1 -1
- package/dist/src/skills/runtime.d.ts +2 -1
- package/dist/src/skills/runtime.js +298 -11
- package/dist/src/skills/runtime.js.map +1 -1
- package/dist/src/skills/types.d.ts +19 -0
- package/dist/src/terminal-ui.d.ts +3 -1
- package/dist/src/terminal-ui.js +7 -1
- package/dist/src/terminal-ui.js.map +1 -1
- package/dist/src/tools/filesystem.js +56 -2
- package/dist/src/tools/filesystem.js.map +1 -1
- package/dist/src/tools/git.d.ts +23 -0
- package/dist/src/tools/git.js +309 -13
- package/dist/src/tools/git.js.map +1 -1
- package/dist/src/tools/repo-search.d.ts +2 -0
- package/dist/src/tools/repo-search.js +45 -13
- package/dist/src/tools/repo-search.js.map +1 -1
- package/dist/src/tools/shell.js +1 -1
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/skill.js +5 -1
- package/dist/src/tools/skill.js.map +1 -1
- package/dist/src/types.d.ts +7 -0
- package/package.json +1 -1
- package/skill/agents-md-generator/SKILL.md +13 -0
- package/skill/biz-opportunity-scout/SKILL.md +17 -0
- package/skill/code-review/SKILL.md +15 -0
- package/skill/code-security-audit/SKILL.md +21 -0
- package/skill/composition-patterns/SKILL.md +15 -0
- package/skill/deploy-to-vercel/SKILL.md +14 -0
- package/skill/git-workflow/SKILL.md +16 -0
- package/skill/jetbrains-vmoptions/SKILL.md +14 -0
- package/skill/kysely-converter/SKILL.md +13 -0
- package/skill/react-best-practices/SKILL.md +19 -0
- package/skill/react-native-skills/SKILL.md +19 -0
- package/skill/react-vite-guide/SKILL.md +18 -0
- package/skill/skill-maker/SKILL.md +14 -0
- package/skill/system-prompt-creator/SKILL.md +13 -0
- package/skill/typst-creator/SKILL.md +14 -0
- package/skill/web-design-guidelines/SKILL.md +17 -0
- package/tui/vetala-darwin-arm64 +0 -0
- package/tui/vetala-darwin-x64 +0 -0
- package/tui/vetala-linux-arm64 +0 -0
- package/tui/vetala-linux-x64 +0 -0
- package/tui/vetala-win32-arm64.exe +0 -0
- package/tui/vetala-win32-x64.exe +0 -0
package/README.md
CHANGED
|
@@ -27,55 +27,15 @@ Current provider support includes Sarvam AI and OpenRouter.
|
|
|
27
27
|
|
|
28
28
|
## Patch Notes
|
|
29
29
|
|
|
30
|
-
### v0.5.
|
|
30
|
+
### v0.5.5
|
|
31
31
|
Added:
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- Deterministic skill routing with active-skill visibility in the UI.
|
|
33
|
+
- Git-aware review flow upgrades with dedicated `/diff` and `/review` entry points.
|
|
34
34
|
|
|
35
35
|
Patched:
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
### v0.5.3
|
|
41
|
-
Added:
|
|
42
|
-
- UI refresh: left-aligned layout with minimal framing.
|
|
43
|
-
- Viewport-backed transcript with in-app scrollback (PgUp/PgDn + mouse wheel in-app mode).
|
|
44
|
-
- Dynamic input bar with background styling and auto-resize.
|
|
45
|
-
- UI hints + tool details toggle (Ctrl+T) plus env-tunable UI controls (mouse mode, alt screen, limits).
|
|
46
|
-
|
|
47
|
-
Patched:
|
|
48
|
-
- Buffered output while modals are open to prevent UI jumps.
|
|
49
|
-
- Live preview rendering is capped and separated from the transcript during streaming.
|
|
50
|
-
- Cached Glamour renderer per theme/width to cut render cost.
|
|
51
|
-
- Fast search skips binaries/oversized files and handles long lines safely.
|
|
52
|
-
|
|
53
|
-
### v0.5.2
|
|
54
|
-
Added:
|
|
55
|
-
- `/resume` now skips empty sessions and previews the last few messages when resuming.
|
|
56
|
-
- Workspace auto-resume uses the most recent non-empty session.
|
|
57
|
-
|
|
58
|
-
Patched:
|
|
59
|
-
- Resume selection list starts at the top and scrolls cleanly for longer lists.
|
|
60
|
-
- Modal transitions no longer leave duplicate empty boxes.
|
|
61
|
-
|
|
62
|
-
### v0.5.1
|
|
63
|
-
Added:
|
|
64
|
-
- New data layout: `memories/`, `rules/`, `snapshots/`, `logs/`, `tasks/`, and `history.jsonl`.
|
|
65
|
-
- Background memory pipeline that writes `raw_memories.md`, rollout summaries, and a consolidated `MEMORY.md`.
|
|
66
|
-
- Configurable memory/context/history limits (visible in `/config`).
|
|
67
|
-
- Update notifier inside the TUI flow with “update now” or “skip for 24 hours”.
|
|
68
|
-
|
|
69
|
-
Patched:
|
|
70
|
-
- Update checks now run before the TUI trust prompt (and won’t duplicate when launched from the CLI).
|
|
71
|
-
- History persistence trims safely to the configured size cap.
|
|
72
|
-
|
|
73
|
-
### v0.5.0
|
|
74
|
-
- **bugfix**: major bug fixes and performance improvements
|
|
75
|
-
- **Universal Syntax Diagnostics**: Replaced the Python-only `compileall` fallback in `get_diagnostics` with `web-tree-sitter`. Vetala can now perform syntax checking across multiple languages (TypeScript, Python, Go, Rust, C, C++, Java, Ruby) directly in memory, even if you don't have the native compiler installed on your machine!
|
|
76
|
-
- **Data-Driven Language Registry**: Centralized language management and file extension mapping. Adding new language support is now a single object definition.
|
|
77
|
-
- **Smart Two-Tier Checks**: Diagnostics now try native tools first (`npx tsc`, `go build`) for the highest quality errors, and transparently fall back to WASM tree-sitter parsing if the toolchain is missing.
|
|
78
|
-
|
|
36
|
+
- Malformed tool calls are quarantined before they can poison session history.
|
|
37
|
+
- `Ctrl+C` can now stop active repo search work instead of waiting for long searches to finish.
|
|
38
|
+
- Explicit file-path prompts now steer toward `read_file`/`read_file_chunk` before repo-wide search.
|
|
79
39
|
|
|
80
40
|
## Compatibility
|
|
81
41
|
|
package/dist/src/agent.d.ts
CHANGED
|
@@ -4,7 +4,16 @@ import { SessionStore } from "./session-store.js";
|
|
|
4
4
|
import { TerminalUI } from "./terminal-ui.js";
|
|
5
5
|
import { ToolRegistry } from "./tools/registry.js";
|
|
6
6
|
import type { SkillRuntime } from "./skills/runtime.js";
|
|
7
|
-
import type { EffectiveConfig, RuntimeHostProfile, SessionState } from "./types.js";
|
|
7
|
+
import type { ChatMessage, EffectiveConfig, RuntimeHostProfile, SessionState, ToolCall } from "./types.js";
|
|
8
|
+
export interface InvalidToolCall {
|
|
9
|
+
toolName: string;
|
|
10
|
+
rawArguments: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ToolCallPartition {
|
|
14
|
+
validToolCalls: ToolCall[];
|
|
15
|
+
invalidToolCalls: InvalidToolCall[];
|
|
16
|
+
}
|
|
8
17
|
export interface AgentOptions {
|
|
9
18
|
config: EffectiveConfig;
|
|
10
19
|
session: SessionState;
|
|
@@ -21,13 +30,18 @@ export interface AgentOptions {
|
|
|
21
30
|
fastSearch?: (query: string, root: string, options?: {
|
|
22
31
|
limit?: number;
|
|
23
32
|
regex?: boolean;
|
|
33
|
+
globs?: string[];
|
|
34
|
+
includeHidden?: boolean;
|
|
35
|
+
signal?: AbortSignal;
|
|
24
36
|
}) => Promise<any[] | null>;
|
|
25
37
|
}
|
|
26
38
|
export declare class Agent {
|
|
27
39
|
private readonly options;
|
|
28
40
|
private readonly client;
|
|
29
41
|
private activeRequestController;
|
|
42
|
+
private activeTurnController;
|
|
30
43
|
private stopRequested;
|
|
44
|
+
private turnSkillPrompt;
|
|
31
45
|
constructor(options: AgentOptions);
|
|
32
46
|
get session(): SessionState;
|
|
33
47
|
requestStop(): void;
|
|
@@ -46,3 +60,8 @@ export declare class AgentInterruptedError extends Error {
|
|
|
46
60
|
constructor();
|
|
47
61
|
}
|
|
48
62
|
export declare function isAgentInterruptedError(error: unknown): error is AgentInterruptedError;
|
|
63
|
+
export declare function partitionToolCalls(toolCalls: ToolCall[]): ToolCallPartition;
|
|
64
|
+
export declare function sanitizeConversationMessages<T extends ChatMessage>(messages: T[]): {
|
|
65
|
+
messages: T[];
|
|
66
|
+
invalidToolCallCount: number;
|
|
67
|
+
};
|
package/dist/src/agent.js
CHANGED
|
@@ -7,7 +7,9 @@ export class Agent {
|
|
|
7
7
|
options;
|
|
8
8
|
client;
|
|
9
9
|
activeRequestController = null;
|
|
10
|
+
activeTurnController = null;
|
|
10
11
|
stopRequested = false;
|
|
12
|
+
turnSkillPrompt = null;
|
|
11
13
|
constructor(options) {
|
|
12
14
|
this.options = options;
|
|
13
15
|
this.client = createProviderClient(options.config.providers[options.session.provider]);
|
|
@@ -17,132 +19,178 @@ export class Agent {
|
|
|
17
19
|
}
|
|
18
20
|
requestStop() {
|
|
19
21
|
this.stopRequested = true;
|
|
22
|
+
this.activeTurnController?.abort();
|
|
20
23
|
this.activeRequestController?.abort();
|
|
21
24
|
}
|
|
22
25
|
async runTurn(userInput, streaming) {
|
|
26
|
+
const turnController = new AbortController();
|
|
27
|
+
this.activeTurnController = turnController;
|
|
23
28
|
this.stopRequested = false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const providerDefinition = getProviderDefinition(this.options.session.provider);
|
|
39
|
-
if (!provider.authValue) {
|
|
40
|
-
const missingAuthMessage = provider.authSource === "stored_hash"
|
|
41
|
-
? `A stored SHA-256 fingerprint exists for ${providerDefinition.label}, but the raw credential is not available in this process. Use /model to enter the key again or set ${providerDefinition.auth.envVars.join(", ")}.`
|
|
42
|
-
: `${providerDefinition.label} credentials are missing. Set ${providerDefinition.auth.envVars.join(", ")} and try again.`;
|
|
43
|
-
await this.appendAndRenderAssistantMessage(missingAuthMessage, false);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const seenToolCalls = new Set();
|
|
47
|
-
let repeatWarningInjected = 0;
|
|
48
|
-
let consecutiveApiErrors = 0;
|
|
49
|
-
while (true) {
|
|
50
|
-
this.throwIfStopped();
|
|
51
|
-
const conversation = compactConversation(this.options.session.messages, this.options.session.referencedFiles, this.options.config.memory);
|
|
52
|
-
const recentCount = this.options.config.memory.recentMessageCount;
|
|
53
|
-
this.options.ui.activity(conversation.compactedCount > 0
|
|
54
|
-
? `Using ${recentCount} recent messages and ${conversation.compactedCount} compacted earlier messages.`
|
|
55
|
-
: "Using the live conversation context.");
|
|
56
|
-
const systemPrompt = await this.systemPrompt(conversation.memory, conversation.compactedCount);
|
|
57
|
-
const requestMessages = withSystemMessage(systemPrompt, conversation.recentMessages);
|
|
58
|
-
let turn;
|
|
59
|
-
try {
|
|
60
|
-
turn = await this.completeTurn(requestMessages, streaming);
|
|
61
|
-
consecutiveApiErrors = 0; // Reset on success
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
if (isAbortError(error) || error instanceof AgentInterruptedError) {
|
|
65
|
-
this.options.ui.endAssistantTurn();
|
|
66
|
-
throw new AgentInterruptedError();
|
|
67
|
-
}
|
|
68
|
-
consecutiveApiErrors += 1;
|
|
69
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
-
if (consecutiveApiErrors >= 3) {
|
|
71
|
-
await this.appendAndRenderAssistantMessage(`I've encountered multiple consecutive API errors and must stop. Last error: ${errorMessage}`, true);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
this.options.ui.warn(`API Error: ${errorMessage}`);
|
|
75
|
-
this.options.ui.activity("Retrying automatically...");
|
|
76
|
-
const syntheticUserMessage = this.persistedMessage({
|
|
77
|
-
role: "user",
|
|
78
|
-
content: `SYSTEM ALERT: The previous generation failed with an API error: ${errorMessage}\nIf you were generating a large file, you likely hit the maximum output token limit. Please try breaking your response down, or use the append_to_file tool to write large files in chunks.`
|
|
79
|
-
});
|
|
80
|
-
await this.options.sessionStore.appendMessage(this.options.session, syntheticUserMessage);
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const hasContent = typeof turn.content === "string" && turn.content.trim().length > 0;
|
|
84
|
-
if (!hasContent && turn.toolCalls.length === 0) {
|
|
85
|
-
if (emptyResponseWarningEnabled()) {
|
|
86
|
-
this.options.ui.warn(emptyResponseWarningMessage());
|
|
87
|
-
}
|
|
88
|
-
this.options.ui.endAssistantTurn();
|
|
29
|
+
try {
|
|
30
|
+
const userMessage = this.persistedMessage({
|
|
31
|
+
role: "user",
|
|
32
|
+
content: userInput
|
|
33
|
+
});
|
|
34
|
+
await this.options.sessionStore.appendMessage(this.options.session, userMessage);
|
|
35
|
+
void appendHistoryEntry(this.options.config, this.options.session.id, userInput).catch(() => {
|
|
36
|
+
// Best-effort history persistence.
|
|
37
|
+
});
|
|
38
|
+
this.turnSkillPrompt = null;
|
|
39
|
+
this.options.ui.updateActiveSkills([]);
|
|
40
|
+
const localGreeting = maybeLocalGreeting(userInput);
|
|
41
|
+
if (localGreeting) {
|
|
42
|
+
await this.appendAndRenderAssistantMessage(localGreeting, false);
|
|
89
43
|
return;
|
|
90
44
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
45
|
+
const turnSkillContext = await this.options.skills.resolveTurnContext(userInput);
|
|
46
|
+
this.turnSkillPrompt = turnSkillContext.prompt;
|
|
47
|
+
this.options.ui.updateActiveSkills(turnSkillContext.labels);
|
|
48
|
+
const provider = this.options.config.providers[this.options.session.provider];
|
|
49
|
+
const providerDefinition = getProviderDefinition(this.options.session.provider);
|
|
50
|
+
if (!provider.authValue) {
|
|
51
|
+
const missingAuthMessage = provider.authSource === "stored_hash"
|
|
52
|
+
? `A stored SHA-256 fingerprint exists for ${providerDefinition.label}, but the raw credential is not available in this process. Use /model to enter the key again or set ${providerDefinition.auth.envVars.join(", ")}.`
|
|
53
|
+
: `${providerDefinition.label} credentials are missing. Set ${providerDefinition.auth.envVars.join(", ")} and try again.`;
|
|
54
|
+
await this.appendAndRenderAssistantMessage(missingAuthMessage, false);
|
|
99
55
|
return;
|
|
100
56
|
}
|
|
101
|
-
|
|
102
|
-
|
|
57
|
+
const seenToolCalls = new Set();
|
|
58
|
+
let repeatWarningInjected = 0;
|
|
59
|
+
let consecutiveApiErrors = 0;
|
|
60
|
+
let malformedToolCallRetries = 0;
|
|
61
|
+
let sanitizedHistoryWarningShown = false;
|
|
62
|
+
while (true) {
|
|
103
63
|
this.throwIfStopped();
|
|
104
|
-
this.options.
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
isError: true
|
|
114
|
-
};
|
|
64
|
+
const conversation = compactConversation(this.options.session.messages, this.options.session.referencedFiles, this.options.config.memory);
|
|
65
|
+
const sanitizedConversation = sanitizeConversationMessages(conversation.recentMessages);
|
|
66
|
+
const recentCount = this.options.config.memory.recentMessageCount;
|
|
67
|
+
this.options.ui.activity(conversation.compactedCount > 0
|
|
68
|
+
? `Using ${recentCount} recent messages and ${conversation.compactedCount} compacted earlier messages.`
|
|
69
|
+
: "Using the live conversation context.");
|
|
70
|
+
if (sanitizedConversation.invalidToolCallCount > 0 && !sanitizedHistoryWarningShown) {
|
|
71
|
+
sanitizedHistoryWarningShown = true;
|
|
72
|
+
this.options.ui.activity(`Ignoring ${sanitizedConversation.invalidToolCallCount} malformed tool call${sanitizedConversation.invalidToolCallCount === 1 ? "" : "s"} from earlier session history.`);
|
|
115
73
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
74
|
+
const systemPrompt = await this.systemPrompt(conversation.memory, conversation.compactedCount);
|
|
75
|
+
const requestMessages = withSystemMessage(systemPrompt, sanitizedConversation.messages);
|
|
76
|
+
let turn;
|
|
77
|
+
try {
|
|
78
|
+
turn = await this.completeTurn(requestMessages, streaming);
|
|
79
|
+
consecutiveApiErrors = 0; // Reset on success
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (isAbortError(error) || error instanceof AgentInterruptedError) {
|
|
83
|
+
this.options.ui.endAssistantTurn();
|
|
84
|
+
throw new AgentInterruptedError();
|
|
124
85
|
}
|
|
125
|
-
|
|
126
|
-
|
|
86
|
+
consecutiveApiErrors += 1;
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
88
|
+
if (consecutiveApiErrors >= 3) {
|
|
89
|
+
await this.appendAndRenderAssistantMessage(`I've encountered multiple consecutive API errors and must stop. Last error: ${errorMessage}`, true);
|
|
90
|
+
return;
|
|
127
91
|
}
|
|
92
|
+
this.options.ui.warn(`API Error: ${errorMessage}`);
|
|
93
|
+
this.options.ui.activity("Retrying automatically...");
|
|
94
|
+
const syntheticUserMessage = this.persistedMessage({
|
|
95
|
+
role: "user",
|
|
96
|
+
content: `SYSTEM ALERT: The previous generation failed with an API error: ${errorMessage}\nIf you were generating a large file, you likely hit the maximum output token limit. Please try breaking your response down, or use the append_to_file tool to write large files in chunks.`
|
|
97
|
+
});
|
|
98
|
+
await this.options.sessionStore.appendMessage(this.options.session, syntheticUserMessage);
|
|
99
|
+
continue;
|
|
128
100
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
101
|
+
const hasContent = typeof turn.content === "string" && turn.content.trim().length > 0;
|
|
102
|
+
if (!hasContent && turn.toolCalls.length === 0) {
|
|
103
|
+
if (emptyResponseWarningEnabled()) {
|
|
104
|
+
this.options.ui.warn(emptyResponseWarningMessage());
|
|
105
|
+
}
|
|
106
|
+
this.options.ui.endAssistantTurn();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const partitionedToolCalls = partitionToolCalls(turn.toolCalls);
|
|
110
|
+
if (partitionedToolCalls.invalidToolCalls.length > 0) {
|
|
111
|
+
malformedToolCallRetries += 1;
|
|
112
|
+
if (hasContent) {
|
|
113
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
114
|
+
role: "assistant",
|
|
115
|
+
content: turn.content,
|
|
116
|
+
tool_calls: null
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
const summary = summarizeInvalidToolCalls(partitionedToolCalls.invalidToolCalls);
|
|
120
|
+
this.options.ui.warn(`Model emitted malformed tool arguments${summary ? `: ${summary}` : ""}. Retrying with smaller, valid tool calls.`);
|
|
121
|
+
if (malformedToolCallRetries >= 3) {
|
|
122
|
+
await this.appendAndRenderAssistantMessage(`The model kept emitting malformed tool calls and I stopped before making changes. Last issue: ${summary || "invalid tool arguments"}.`, true);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
126
|
+
role: "user",
|
|
127
|
+
content: malformedToolCallRepairPrompt(partitionedToolCalls.invalidToolCalls)
|
|
128
|
+
}));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
malformedToolCallRetries = 0;
|
|
132
|
+
const assistantMessage = this.persistedMessage({
|
|
133
|
+
role: "assistant",
|
|
134
|
+
content: turn.content || null,
|
|
135
|
+
tool_calls: partitionedToolCalls.validToolCalls.length > 0 ? partitionedToolCalls.validToolCalls : null
|
|
142
136
|
});
|
|
143
|
-
await this.options.sessionStore.appendMessage(this.options.session,
|
|
137
|
+
await this.options.sessionStore.appendMessage(this.options.session, assistantMessage);
|
|
138
|
+
if (partitionedToolCalls.validToolCalls.length === 0) {
|
|
139
|
+
this.options.ui.endAssistantTurn();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let suppressedRepeats = 0;
|
|
143
|
+
for (const toolCall of partitionedToolCalls.validToolCalls) {
|
|
144
|
+
this.throwIfStopped();
|
|
145
|
+
this.options.ui.printToolCall(toolCall);
|
|
146
|
+
const signature = toolCallSignature(toolCall);
|
|
147
|
+
let result;
|
|
148
|
+
if (seenToolCalls.has(signature)) {
|
|
149
|
+
suppressedRepeats += 1;
|
|
150
|
+
this.options.ui.activity(`Skipping repeated ${toolCall.function.name} call.`);
|
|
151
|
+
result = {
|
|
152
|
+
summary: "Repeated tool call suppressed",
|
|
153
|
+
content: "This exact tool call already ran earlier in this turn. Reuse the earlier result instead of calling it again.",
|
|
154
|
+
isError: true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
seenToolCalls.add(signature);
|
|
159
|
+
this.options.ui.activity(`Running ${toolCall.function.name}.`);
|
|
160
|
+
const toolSpec = this.options.tools.getTool(toolCall.function.name);
|
|
161
|
+
const isMutating = toolSpec && !toolSpec.readOnly;
|
|
162
|
+
result = await this.options.tools.execute(toolCall, this.toolContext(turnController));
|
|
163
|
+
if (result.isError) {
|
|
164
|
+
seenToolCalls.delete(signature);
|
|
165
|
+
}
|
|
166
|
+
else if (isMutating) {
|
|
167
|
+
seenToolCalls.clear();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.throwIfStopped();
|
|
171
|
+
this.options.ui.printToolResult(result.summary, result.isError, result.content);
|
|
172
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
173
|
+
role: "tool",
|
|
174
|
+
content: result.content,
|
|
175
|
+
tool_call_id: toolCall.id
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
if (shouldInjectRepeatWarning(suppressedRepeats, repeatWarningInjected)) {
|
|
179
|
+
repeatWarningInjected += 1;
|
|
180
|
+
const warning = this.persistedMessage({
|
|
181
|
+
role: "user",
|
|
182
|
+
content: toolRepeatWarningMessage()
|
|
183
|
+
});
|
|
184
|
+
await this.options.sessionStore.appendMessage(this.options.session, warning);
|
|
185
|
+
}
|
|
144
186
|
}
|
|
145
187
|
}
|
|
188
|
+
finally {
|
|
189
|
+
if (this.activeTurnController === turnController) {
|
|
190
|
+
this.activeTurnController = null;
|
|
191
|
+
}
|
|
192
|
+
this.turnSkillPrompt = null;
|
|
193
|
+
}
|
|
146
194
|
}
|
|
147
195
|
async completeTurn(messages, streaming) {
|
|
148
196
|
this.options.ui.activity(`Thinking with ${providerLabel(this.options.session.provider)} / ${this.options.session.model}.`);
|
|
@@ -184,7 +232,7 @@ export class Agent {
|
|
|
184
232
|
this.options.ui.endAssistantTurn();
|
|
185
233
|
throw new AgentInterruptedError();
|
|
186
234
|
}
|
|
187
|
-
this.options.ui.
|
|
235
|
+
this.options.ui.discardAssistantDraft();
|
|
188
236
|
this.options.ui.activity("Streaming failed. Retrying with buffered completion.");
|
|
189
237
|
this.options.ui.warn(`Streaming failed, falling back to buffered completion: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
238
|
return this.completeTurn(messages, false);
|
|
@@ -205,10 +253,18 @@ export class Agent {
|
|
|
205
253
|
tool_choice: "auto"
|
|
206
254
|
};
|
|
207
255
|
}
|
|
208
|
-
toolContext() {
|
|
256
|
+
toolContext(turnController) {
|
|
209
257
|
return {
|
|
210
258
|
cwd: process.cwd(),
|
|
211
259
|
workspaceRoot: this.options.session.workspaceRoot,
|
|
260
|
+
lifecycle: {
|
|
261
|
+
signal: turnController.signal,
|
|
262
|
+
throwIfAborted: () => {
|
|
263
|
+
if (turnController.signal.aborted || this.stopRequested) {
|
|
264
|
+
throw new AgentInterruptedError();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
212
268
|
approvals: {
|
|
213
269
|
requestApproval: (request) => this.options.approvals.requestApproval(request),
|
|
214
270
|
hasSessionGrant: (key) => this.options.approvals.hasSessionGrant(key),
|
|
@@ -221,7 +277,10 @@ export class Agent {
|
|
|
221
277
|
},
|
|
222
278
|
performance: {
|
|
223
279
|
computeDiff: (before, after) => this.options.computeDiff ? this.options.computeDiff(before, after) : Promise.resolve(null),
|
|
224
|
-
fastSearch: (query, root, opts) => this.options.fastSearch ? this.options.fastSearch(query, root,
|
|
280
|
+
fastSearch: (query, root, opts) => this.options.fastSearch ? this.options.fastSearch(query, root, {
|
|
281
|
+
...opts,
|
|
282
|
+
signal: turnController.signal
|
|
283
|
+
}) : Promise.resolve(null)
|
|
225
284
|
},
|
|
226
285
|
reads: {
|
|
227
286
|
hasRead: (targetPath) => this.options.session.readFiles.includes(targetPath),
|
|
@@ -259,7 +318,6 @@ export class Agent {
|
|
|
259
318
|
}
|
|
260
319
|
async systemPrompt(memory, compactedCount) {
|
|
261
320
|
const skillInventory = await this.options.skills.inventoryPrompt();
|
|
262
|
-
const pinnedSkillContext = await this.options.skills.pinnedPrompt();
|
|
263
321
|
const [rulesPrompt, persistentMemory] = await Promise.all([
|
|
264
322
|
loadRulesPrompt(this.options.config.contextFiles),
|
|
265
323
|
this.options.config.memories.useMemories
|
|
@@ -282,6 +340,14 @@ export class Agent {
|
|
|
282
340
|
`Active provider: ${providerLabel(this.options.session.provider)}`,
|
|
283
341
|
`Active model: ${this.options.session.model}`,
|
|
284
342
|
`Allowed roots right now: ${this.options.pathPolicy.allowedRoots().join(", ")}`,
|
|
343
|
+
"When using tools, emit valid JSON object arguments only.",
|
|
344
|
+
"Prefer smaller tool calls over giant payloads. Break large refactors and large apply_patch edits into multiple steps.",
|
|
345
|
+
"When the user names a concrete file path, read that file directly with read_file or read_file_chunk before considering search_repo.",
|
|
346
|
+
"For Git-aware tasks, prefer the dedicated git tools over ad-hoc shell commands.",
|
|
347
|
+
"For change review, start with git_review targeting the full worktree so you cover staged, unstaged, and untracked files.",
|
|
348
|
+
"For branch review, compare against the merge base with the requested base branch instead of assuming HEAD~1 or the previous commit.",
|
|
349
|
+
"Use git_log and git_blame when history or ownership helps explain why code exists.",
|
|
350
|
+
"Do not commit, push, or create branches unless the user explicitly asks.",
|
|
285
351
|
compactedCount > 0
|
|
286
352
|
? `Only the most recent messages are attached verbatim. ${compactedCount} earlier messages were compacted into working memory.`
|
|
287
353
|
: "The full conversation is attached because the session is still short."
|
|
@@ -290,8 +356,8 @@ export class Agent {
|
|
|
290
356
|
lines.push("", rulesPrompt);
|
|
291
357
|
}
|
|
292
358
|
lines.push("", skillInventory);
|
|
293
|
-
if (
|
|
294
|
-
lines.push("",
|
|
359
|
+
if (this.turnSkillPrompt) {
|
|
360
|
+
lines.push("", this.turnSkillPrompt);
|
|
295
361
|
}
|
|
296
362
|
if (persistentMemory) {
|
|
297
363
|
lines.push("", persistentMemory);
|
|
@@ -326,6 +392,58 @@ export class AgentInterruptedError extends Error {
|
|
|
326
392
|
export function isAgentInterruptedError(error) {
|
|
327
393
|
return error instanceof AgentInterruptedError;
|
|
328
394
|
}
|
|
395
|
+
export function partitionToolCalls(toolCalls) {
|
|
396
|
+
const validToolCalls = [];
|
|
397
|
+
const invalidToolCalls = [];
|
|
398
|
+
for (const toolCall of toolCalls) {
|
|
399
|
+
const toolName = toolCall.function.name.trim();
|
|
400
|
+
if (!toolName) {
|
|
401
|
+
invalidToolCalls.push({
|
|
402
|
+
toolName: "(unknown tool)",
|
|
403
|
+
rawArguments: toolCall.function.arguments,
|
|
404
|
+
reason: "Missing tool name."
|
|
405
|
+
});
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const validationError = validateToolCallArguments(toolCall.function.arguments);
|
|
409
|
+
if (validationError) {
|
|
410
|
+
invalidToolCalls.push({
|
|
411
|
+
toolName,
|
|
412
|
+
rawArguments: toolCall.function.arguments,
|
|
413
|
+
reason: validationError
|
|
414
|
+
});
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
validToolCalls.push(toolCall);
|
|
418
|
+
}
|
|
419
|
+
return { validToolCalls, invalidToolCalls };
|
|
420
|
+
}
|
|
421
|
+
export function sanitizeConversationMessages(messages) {
|
|
422
|
+
const sanitized = [];
|
|
423
|
+
let invalidToolCallCount = 0;
|
|
424
|
+
for (const message of messages) {
|
|
425
|
+
if (message.role !== "assistant" || !message.tool_calls || message.tool_calls.length === 0) {
|
|
426
|
+
sanitized.push(message);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const partition = partitionToolCalls(message.tool_calls);
|
|
430
|
+
invalidToolCallCount += partition.invalidToolCalls.length;
|
|
431
|
+
if (partition.invalidToolCalls.length === 0) {
|
|
432
|
+
sanitized.push(message);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if ((message.content ?? "").trim() === "" && partition.validToolCalls.length === 0) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const copy = { ...message };
|
|
439
|
+
delete copy.tool_calls;
|
|
440
|
+
if (partition.validToolCalls.length > 0) {
|
|
441
|
+
copy.tool_calls = partition.validToolCalls;
|
|
442
|
+
}
|
|
443
|
+
sanitized.push(copy);
|
|
444
|
+
}
|
|
445
|
+
return { messages: sanitized, invalidToolCallCount };
|
|
446
|
+
}
|
|
329
447
|
function toolCallSignature(toolCall) {
|
|
330
448
|
try {
|
|
331
449
|
return `${toolCall.function.name}:${JSON.stringify(JSON.parse(toolCall.function.arguments))}`;
|
|
@@ -369,6 +487,49 @@ function envBool(name) {
|
|
|
369
487
|
return undefined;
|
|
370
488
|
}
|
|
371
489
|
}
|
|
490
|
+
function validateToolCallArguments(rawArguments) {
|
|
491
|
+
if (!rawArguments.trim()) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
let parsed;
|
|
495
|
+
try {
|
|
496
|
+
parsed = JSON.parse(rawArguments);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
return error instanceof Error ? error.message : String(error);
|
|
500
|
+
}
|
|
501
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
502
|
+
return "Tool arguments must be a JSON object.";
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function malformedToolCallRepairPrompt(invalidToolCalls) {
|
|
507
|
+
const tools = [...new Set(invalidToolCalls.map((issue) => issue.toolName))];
|
|
508
|
+
const lines = [
|
|
509
|
+
"SYSTEM ALERT: The previous assistant turn emitted malformed tool-call JSON, so those tools were not executed.",
|
|
510
|
+
`Affected tools: ${tools.join(", ")}.`,
|
|
511
|
+
"Retry the same intent, but emit valid JSON object arguments only.",
|
|
512
|
+
"Keep tool calls small and precise. Split large refactors or large apply_patch edits into multiple smaller tool calls."
|
|
513
|
+
];
|
|
514
|
+
const applyPatchAffected = tools.some((name) => name === "apply_patch" || name === "replace_in_file");
|
|
515
|
+
if (applyPatchAffected) {
|
|
516
|
+
lines.push("For file edits, prefer smaller exact hunks with short search/replace blocks instead of one giant patch payload.");
|
|
517
|
+
}
|
|
518
|
+
const firstIssue = invalidToolCalls[0];
|
|
519
|
+
if (firstIssue) {
|
|
520
|
+
lines.push(`Last parse error: ${firstIssue.toolName}: ${firstIssue.reason}`);
|
|
521
|
+
}
|
|
522
|
+
lines.push("Continue the original request.");
|
|
523
|
+
return lines.join("\n");
|
|
524
|
+
}
|
|
525
|
+
function summarizeInvalidToolCalls(invalidToolCalls) {
|
|
526
|
+
const first = invalidToolCalls[0];
|
|
527
|
+
if (!first) {
|
|
528
|
+
return "";
|
|
529
|
+
}
|
|
530
|
+
const extra = invalidToolCalls.length - 1;
|
|
531
|
+
return `${first.toolName}: ${first.reason}${extra > 0 ? ` (+${extra} more)` : ""}`;
|
|
532
|
+
}
|
|
372
533
|
function envInt(name) {
|
|
373
534
|
const raw = process.env[name];
|
|
374
535
|
if (!raw) {
|