@vetala/vetala 0.5.4 → 0.5.6
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 +12 -42
- package/dist/src/agent.d.ts +23 -1
- package/dist/src/agent.js +314 -117
- 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/deliberation.d.ts +15 -0
- package/dist/src/deliberation.js +219 -0
- package/dist/src/deliberation.js.map +1 -0
- package/dist/src/ipc-backend.js +97 -9
- package/dist/src/ipc-backend.js.map +1 -1
- package/dist/src/ipc-ui.d.ts +6 -2
- package/dist/src/ipc-ui.js +15 -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 +5 -1
- package/dist/src/terminal-ui.js +15 -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,25 @@ Current provider support includes Sarvam AI and OpenRouter.
|
|
|
27
27
|
|
|
28
28
|
## Patch Notes
|
|
29
29
|
|
|
30
|
-
### v0.5.
|
|
30
|
+
### v0.5.6
|
|
31
31
|
Added:
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- Turn deliberation with dynamic reasoning effort, visible planning/thinking summaries, and live execution phases in the TUI footer.
|
|
33
|
+
- Clarification-first behavior for underspecified edit requests through stronger `ask_user` guidance.
|
|
34
34
|
|
|
35
35
|
Patched:
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
36
|
+
- `Ctrl+C` pause flow now interrupts active tool/model work more reliably and shows an immediate stopping modal.
|
|
37
|
+
- Repo-wide search cancellation now propagates through the TypeScript backend and Go TUI worker path.
|
|
38
|
+
- Non-trivial turns now surface reasoning and phase state more clearly during execution.
|
|
39
39
|
|
|
40
|
-
### v0.5.
|
|
40
|
+
### v0.5.5
|
|
41
41
|
Added:
|
|
42
|
-
-
|
|
43
|
-
-
|
|
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).
|
|
42
|
+
- Deterministic skill routing with active-skill visibility in the UI.
|
|
43
|
+
- Git-aware review flow upgrades with dedicated `/diff` and `/review` entry points.
|
|
46
44
|
|
|
47
45
|
Patched:
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
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
|
-
|
|
46
|
+
- Malformed tool calls are quarantined before they can poison session history.
|
|
47
|
+
- `Ctrl+C` can now stop active repo search work instead of waiting for long searches to finish.
|
|
48
|
+
- Explicit file-path prompts now steer toward `read_file`/`read_file_chunk` before repo-wide search.
|
|
79
49
|
|
|
80
50
|
## Compatibility
|
|
81
51
|
|
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,21 @@ 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;
|
|
45
|
+
private turnDeliberationPrompt;
|
|
46
|
+
private turnReasoningEffort;
|
|
47
|
+
private turnReasoningLabel;
|
|
31
48
|
constructor(options: AgentOptions);
|
|
32
49
|
get session(): SessionState;
|
|
33
50
|
requestStop(): void;
|
|
@@ -46,3 +63,8 @@ export declare class AgentInterruptedError extends Error {
|
|
|
46
63
|
constructor();
|
|
47
64
|
}
|
|
48
65
|
export declare function isAgentInterruptedError(error: unknown): error is AgentInterruptedError;
|
|
66
|
+
export declare function partitionToolCalls(toolCalls: ToolCall[]): ToolCallPartition;
|
|
67
|
+
export declare function sanitizeConversationMessages<T extends ChatMessage>(messages: T[]): {
|
|
68
|
+
messages: T[];
|
|
69
|
+
invalidToolCallCount: number;
|
|
70
|
+
};
|
package/dist/src/agent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compactConversation } from "./context-memory.js";
|
|
2
|
+
import { analyzeTurnDeliberation, phaseForTool } from "./deliberation.js";
|
|
2
3
|
import { loadMemoriesPrompt, loadRulesPrompt } from "./context-files.js";
|
|
3
4
|
import { appendHistoryEntry } from "./history-store.js";
|
|
4
5
|
import { createProviderClient, getProviderDefinition, providerLabel, withSystemMessage } from "./providers/index.js";
|
|
@@ -7,7 +8,12 @@ export class Agent {
|
|
|
7
8
|
options;
|
|
8
9
|
client;
|
|
9
10
|
activeRequestController = null;
|
|
11
|
+
activeTurnController = null;
|
|
10
12
|
stopRequested = false;
|
|
13
|
+
turnSkillPrompt = null;
|
|
14
|
+
turnDeliberationPrompt = null;
|
|
15
|
+
turnReasoningEffort = null;
|
|
16
|
+
turnReasoningLabel = "none";
|
|
11
17
|
constructor(options) {
|
|
12
18
|
this.options = options;
|
|
13
19
|
this.client = createProviderClient(options.config.providers[options.session.provider]);
|
|
@@ -17,135 +23,208 @@ export class Agent {
|
|
|
17
23
|
}
|
|
18
24
|
requestStop() {
|
|
19
25
|
this.stopRequested = true;
|
|
26
|
+
this.activeTurnController?.abort();
|
|
20
27
|
this.activeRequestController?.abort();
|
|
21
28
|
}
|
|
22
29
|
async runTurn(userInput, streaming) {
|
|
30
|
+
const turnController = new AbortController();
|
|
31
|
+
this.activeTurnController = turnController;
|
|
23
32
|
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();
|
|
33
|
+
try {
|
|
34
|
+
const userMessage = this.persistedMessage({
|
|
35
|
+
role: "user",
|
|
36
|
+
content: userInput
|
|
37
|
+
});
|
|
38
|
+
await this.options.sessionStore.appendMessage(this.options.session, userMessage);
|
|
39
|
+
void appendHistoryEntry(this.options.config, this.options.session.id, userInput).catch(() => {
|
|
40
|
+
// Best-effort history persistence.
|
|
41
|
+
});
|
|
42
|
+
this.turnSkillPrompt = null;
|
|
43
|
+
this.options.ui.updateActiveSkills([]);
|
|
44
|
+
const localGreeting = maybeLocalGreeting(userInput);
|
|
45
|
+
if (localGreeting) {
|
|
46
|
+
await this.appendAndRenderAssistantMessage(localGreeting, false);
|
|
89
47
|
return;
|
|
90
48
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
49
|
+
const turnSkillContext = await this.options.skills.resolveTurnContext(userInput);
|
|
50
|
+
this.turnSkillPrompt = turnSkillContext.prompt;
|
|
51
|
+
this.options.ui.updateActiveSkills(turnSkillContext.labels);
|
|
52
|
+
const provider = this.options.config.providers[this.options.session.provider];
|
|
53
|
+
const providerDefinition = getProviderDefinition(this.options.session.provider);
|
|
54
|
+
const deliberation = analyzeTurnDeliberation(userInput, {
|
|
55
|
+
configuredEffort: providerDefinition.supportsReasoningEffort ? this.options.config.reasoningEffort : null,
|
|
56
|
+
activeSkills: turnSkillContext.labels
|
|
95
57
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
58
|
+
this.turnDeliberationPrompt = deliberation.guidance;
|
|
59
|
+
this.turnReasoningEffort = providerDefinition.supportsReasoningEffort ? deliberation.reasoningEffort : null;
|
|
60
|
+
this.turnReasoningLabel = deliberation.reasoningLabel;
|
|
61
|
+
this.options.ui.updateTurnState(this.turnReasoningLabel, "planning");
|
|
62
|
+
if (deliberation.shouldShowThinking && deliberation.thinkingSummary) {
|
|
63
|
+
this.options.ui.printThinking(deliberation.thinkingSummary);
|
|
64
|
+
}
|
|
65
|
+
if (!provider.authValue) {
|
|
66
|
+
const missingAuthMessage = provider.authSource === "stored_hash"
|
|
67
|
+
? `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(", ")}.`
|
|
68
|
+
: `${providerDefinition.label} credentials are missing. Set ${providerDefinition.auth.envVars.join(", ")} and try again.`;
|
|
69
|
+
await this.appendAndRenderAssistantMessage(missingAuthMessage, false);
|
|
99
70
|
return;
|
|
100
71
|
}
|
|
101
|
-
|
|
102
|
-
|
|
72
|
+
const seenToolCalls = new Set();
|
|
73
|
+
let repeatWarningInjected = 0;
|
|
74
|
+
let consecutiveApiErrors = 0;
|
|
75
|
+
let malformedToolCallRetries = 0;
|
|
76
|
+
let sanitizedHistoryWarningShown = false;
|
|
77
|
+
while (true) {
|
|
103
78
|
this.throwIfStopped();
|
|
104
|
-
this.options.
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
79
|
+
const conversation = compactConversation(this.options.session.messages, this.options.session.referencedFiles, this.options.config.memory);
|
|
80
|
+
const sanitizedConversation = sanitizeConversationMessages(conversation.recentMessages);
|
|
81
|
+
const recentCount = this.options.config.memory.recentMessageCount;
|
|
82
|
+
this.options.ui.activity(conversation.compactedCount > 0
|
|
83
|
+
? `Using ${recentCount} recent messages and ${conversation.compactedCount} compacted earlier messages.`
|
|
84
|
+
: "Using the live conversation context.");
|
|
85
|
+
if (sanitizedConversation.invalidToolCallCount > 0 && !sanitizedHistoryWarningShown) {
|
|
86
|
+
sanitizedHistoryWarningShown = true;
|
|
87
|
+
this.options.ui.activity(`Ignoring ${sanitizedConversation.invalidToolCallCount} malformed tool call${sanitizedConversation.invalidToolCallCount === 1 ? "" : "s"} from earlier session history.`);
|
|
88
|
+
}
|
|
89
|
+
const systemPrompt = await this.systemPrompt(conversation.memory, conversation.compactedCount);
|
|
90
|
+
const requestMessages = withSystemMessage(systemPrompt, sanitizedConversation.messages);
|
|
91
|
+
let turn;
|
|
92
|
+
try {
|
|
93
|
+
turn = await this.completeTurn(requestMessages, streaming);
|
|
94
|
+
consecutiveApiErrors = 0; // Reset on success
|
|
115
95
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const isMutating = toolSpec && !toolSpec.readOnly;
|
|
121
|
-
result = await this.options.tools.execute(toolCall, this.toolContext());
|
|
122
|
-
if (result.isError) {
|
|
123
|
-
seenToolCalls.delete(signature);
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (isAbortError(error) || error instanceof AgentInterruptedError) {
|
|
98
|
+
this.options.ui.endAssistantTurn();
|
|
99
|
+
throw new AgentInterruptedError();
|
|
124
100
|
}
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
consecutiveApiErrors += 1;
|
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
103
|
+
if (consecutiveApiErrors >= 3) {
|
|
104
|
+
await this.appendAndRenderAssistantMessage(`I've encountered multiple consecutive API errors and must stop. Last error: ${errorMessage}`, true);
|
|
105
|
+
return;
|
|
127
106
|
}
|
|
107
|
+
this.options.ui.warn(`API Error: ${errorMessage}`);
|
|
108
|
+
this.options.ui.activity("Retrying automatically...");
|
|
109
|
+
const syntheticUserMessage = this.persistedMessage({
|
|
110
|
+
role: "user",
|
|
111
|
+
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.`
|
|
112
|
+
});
|
|
113
|
+
await this.options.sessionStore.appendMessage(this.options.session, syntheticUserMessage);
|
|
114
|
+
continue;
|
|
128
115
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
116
|
+
const hasContent = typeof turn.content === "string" && turn.content.trim().length > 0;
|
|
117
|
+
if (!hasContent && turn.toolCalls.length === 0) {
|
|
118
|
+
if (emptyResponseWarningEnabled()) {
|
|
119
|
+
this.options.ui.warn(emptyResponseWarningMessage());
|
|
120
|
+
}
|
|
121
|
+
this.options.ui.endAssistantTurn();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const partitionedToolCalls = partitionToolCalls(turn.toolCalls);
|
|
125
|
+
if (partitionedToolCalls.invalidToolCalls.length > 0) {
|
|
126
|
+
malformedToolCallRetries += 1;
|
|
127
|
+
if (hasContent) {
|
|
128
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
129
|
+
role: "assistant",
|
|
130
|
+
content: turn.content,
|
|
131
|
+
tool_calls: null
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
const summary = summarizeInvalidToolCalls(partitionedToolCalls.invalidToolCalls);
|
|
135
|
+
this.options.ui.warn(`Model emitted malformed tool arguments${summary ? `: ${summary}` : ""}. Retrying with smaller, valid tool calls.`);
|
|
136
|
+
if (malformedToolCallRetries >= 3) {
|
|
137
|
+
await this.appendAndRenderAssistantMessage(`The model kept emitting malformed tool calls and I stopped before making changes. Last issue: ${summary || "invalid tool arguments"}.`, true);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
141
|
+
role: "user",
|
|
142
|
+
content: malformedToolCallRepairPrompt(partitionedToolCalls.invalidToolCalls)
|
|
143
|
+
}));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
malformedToolCallRetries = 0;
|
|
147
|
+
const assistantMessage = this.persistedMessage({
|
|
148
|
+
role: "assistant",
|
|
149
|
+
content: turn.content || null,
|
|
150
|
+
tool_calls: partitionedToolCalls.validToolCalls.length > 0 ? partitionedToolCalls.validToolCalls : null
|
|
142
151
|
});
|
|
143
|
-
await this.options.sessionStore.appendMessage(this.options.session,
|
|
152
|
+
await this.options.sessionStore.appendMessage(this.options.session, assistantMessage);
|
|
153
|
+
if (partitionedToolCalls.validToolCalls.length === 0) {
|
|
154
|
+
this.options.ui.endAssistantTurn();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let suppressedRepeats = 0;
|
|
158
|
+
for (const toolCall of partitionedToolCalls.validToolCalls) {
|
|
159
|
+
this.throwIfStopped();
|
|
160
|
+
this.options.ui.printToolCall(toolCall);
|
|
161
|
+
const signature = toolCallSignature(toolCall);
|
|
162
|
+
let result;
|
|
163
|
+
if (seenToolCalls.has(signature)) {
|
|
164
|
+
suppressedRepeats += 1;
|
|
165
|
+
this.options.ui.activity(`Skipping repeated ${toolCall.function.name} call.`);
|
|
166
|
+
result = {
|
|
167
|
+
summary: "Repeated tool call suppressed",
|
|
168
|
+
content: "This exact tool call already ran earlier in this turn. Reuse the earlier result instead of calling it again.",
|
|
169
|
+
isError: true
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
seenToolCalls.add(signature);
|
|
174
|
+
this.options.ui.updateTurnState(this.turnReasoningLabel, phaseForTool(toolCall.function.name));
|
|
175
|
+
this.options.ui.activity(`Running ${toolCall.function.name}.`);
|
|
176
|
+
const toolSpec = this.options.tools.getTool(toolCall.function.name);
|
|
177
|
+
const isMutating = toolSpec && !toolSpec.readOnly;
|
|
178
|
+
try {
|
|
179
|
+
result = await this.options.tools.execute(toolCall, this.toolContext(turnController));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (isAbortError(error) || error instanceof AgentInterruptedError) {
|
|
183
|
+
this.options.ui.endAssistantTurn();
|
|
184
|
+
throw new AgentInterruptedError();
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
if (result.isError) {
|
|
189
|
+
seenToolCalls.delete(signature);
|
|
190
|
+
}
|
|
191
|
+
else if (isMutating) {
|
|
192
|
+
seenToolCalls.clear();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.throwIfStopped();
|
|
196
|
+
this.options.ui.printToolResult(result.summary, result.isError, result.content);
|
|
197
|
+
await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
|
|
198
|
+
role: "tool",
|
|
199
|
+
content: result.content,
|
|
200
|
+
tool_call_id: toolCall.id
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
if (shouldInjectRepeatWarning(suppressedRepeats, repeatWarningInjected)) {
|
|
204
|
+
repeatWarningInjected += 1;
|
|
205
|
+
const warning = this.persistedMessage({
|
|
206
|
+
role: "user",
|
|
207
|
+
content: toolRepeatWarningMessage()
|
|
208
|
+
});
|
|
209
|
+
await this.options.sessionStore.appendMessage(this.options.session, warning);
|
|
210
|
+
}
|
|
211
|
+
this.options.ui.updateTurnState(this.turnReasoningLabel, "thinking");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (this.activeTurnController === turnController) {
|
|
216
|
+
this.activeTurnController = null;
|
|
144
217
|
}
|
|
218
|
+
this.turnSkillPrompt = null;
|
|
219
|
+
this.turnDeliberationPrompt = null;
|
|
220
|
+
this.turnReasoningEffort = null;
|
|
221
|
+
this.turnReasoningLabel = "none";
|
|
222
|
+
this.options.ui.updateTurnState(null, null);
|
|
145
223
|
}
|
|
146
224
|
}
|
|
147
225
|
async completeTurn(messages, streaming) {
|
|
148
|
-
this.options.ui.
|
|
226
|
+
this.options.ui.updateTurnState(this.turnReasoningLabel, "thinking");
|
|
227
|
+
this.options.ui.activity(`Thinking with ${providerLabel(this.options.session.provider)} / ${this.options.session.model} (${this.turnReasoningLabel} reasoning).`);
|
|
149
228
|
const requestOptions = this.beginRequest();
|
|
150
229
|
if (!streaming) {
|
|
151
230
|
const spinner = this.options.ui.startSpinner("Thinking");
|
|
@@ -184,7 +263,7 @@ export class Agent {
|
|
|
184
263
|
this.options.ui.endAssistantTurn();
|
|
185
264
|
throw new AgentInterruptedError();
|
|
186
265
|
}
|
|
187
|
-
this.options.ui.
|
|
266
|
+
this.options.ui.discardAssistantDraft();
|
|
188
267
|
this.options.ui.activity("Streaming failed. Retrying with buffered completion.");
|
|
189
268
|
this.options.ui.warn(`Streaming failed, falling back to buffered completion: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
269
|
return this.completeTurn(messages, false);
|
|
@@ -199,16 +278,24 @@ export class Agent {
|
|
|
199
278
|
model: this.options.session.model,
|
|
200
279
|
temperature: 0.2,
|
|
201
280
|
reasoning_effort: getProviderDefinition(this.options.session.provider).supportsReasoningEffort
|
|
202
|
-
? this.
|
|
281
|
+
? this.turnReasoningEffort
|
|
203
282
|
: null,
|
|
204
283
|
tools: this.options.tools.toSarvamTools(),
|
|
205
284
|
tool_choice: "auto"
|
|
206
285
|
};
|
|
207
286
|
}
|
|
208
|
-
toolContext() {
|
|
287
|
+
toolContext(turnController) {
|
|
209
288
|
return {
|
|
210
289
|
cwd: process.cwd(),
|
|
211
290
|
workspaceRoot: this.options.session.workspaceRoot,
|
|
291
|
+
lifecycle: {
|
|
292
|
+
signal: turnController.signal,
|
|
293
|
+
throwIfAborted: () => {
|
|
294
|
+
if (turnController.signal.aborted || this.stopRequested) {
|
|
295
|
+
throw new AgentInterruptedError();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
212
299
|
approvals: {
|
|
213
300
|
requestApproval: (request) => this.options.approvals.requestApproval(request),
|
|
214
301
|
hasSessionGrant: (key) => this.options.approvals.hasSessionGrant(key),
|
|
@@ -221,7 +308,10 @@ export class Agent {
|
|
|
221
308
|
},
|
|
222
309
|
performance: {
|
|
223
310
|
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,
|
|
311
|
+
fastSearch: (query, root, opts) => this.options.fastSearch ? this.options.fastSearch(query, root, {
|
|
312
|
+
...opts,
|
|
313
|
+
signal: turnController.signal
|
|
314
|
+
}) : Promise.resolve(null)
|
|
225
315
|
},
|
|
226
316
|
reads: {
|
|
227
317
|
hasRead: (targetPath) => this.options.session.readFiles.includes(targetPath),
|
|
@@ -259,7 +349,6 @@ export class Agent {
|
|
|
259
349
|
}
|
|
260
350
|
async systemPrompt(memory, compactedCount) {
|
|
261
351
|
const skillInventory = await this.options.skills.inventoryPrompt();
|
|
262
|
-
const pinnedSkillContext = await this.options.skills.pinnedPrompt();
|
|
263
352
|
const [rulesPrompt, persistentMemory] = await Promise.all([
|
|
264
353
|
loadRulesPrompt(this.options.config.contextFiles),
|
|
265
354
|
this.options.config.memories.useMemories
|
|
@@ -282,6 +371,16 @@ export class Agent {
|
|
|
282
371
|
`Active provider: ${providerLabel(this.options.session.provider)}`,
|
|
283
372
|
`Active model: ${this.options.session.model}`,
|
|
284
373
|
`Allowed roots right now: ${this.options.pathPolicy.allowedRoots().join(", ")}`,
|
|
374
|
+
"When using tools, emit valid JSON object arguments only.",
|
|
375
|
+
"Prefer smaller tool calls over giant payloads. Break large refactors and large apply_patch edits into multiple steps.",
|
|
376
|
+
"When the user names a concrete file path, read that file directly with read_file or read_file_chunk before considering search_repo.",
|
|
377
|
+
"For non-trivial tasks, form a concise plan before acting and execute it incrementally.",
|
|
378
|
+
"If the target, scope, or acceptance criteria remain unclear after initial inspection, use ask_user before editing.",
|
|
379
|
+
"For Git-aware tasks, prefer the dedicated git tools over ad-hoc shell commands.",
|
|
380
|
+
"For change review, start with git_review targeting the full worktree so you cover staged, unstaged, and untracked files.",
|
|
381
|
+
"For branch review, compare against the merge base with the requested base branch instead of assuming HEAD~1 or the previous commit.",
|
|
382
|
+
"Use git_log and git_blame when history or ownership helps explain why code exists.",
|
|
383
|
+
"Do not commit, push, or create branches unless the user explicitly asks.",
|
|
285
384
|
compactedCount > 0
|
|
286
385
|
? `Only the most recent messages are attached verbatim. ${compactedCount} earlier messages were compacted into working memory.`
|
|
287
386
|
: "The full conversation is attached because the session is still short."
|
|
@@ -290,8 +389,11 @@ export class Agent {
|
|
|
290
389
|
lines.push("", rulesPrompt);
|
|
291
390
|
}
|
|
292
391
|
lines.push("", skillInventory);
|
|
293
|
-
if (
|
|
294
|
-
lines.push("",
|
|
392
|
+
if (this.turnSkillPrompt) {
|
|
393
|
+
lines.push("", this.turnSkillPrompt);
|
|
394
|
+
}
|
|
395
|
+
if (this.turnDeliberationPrompt) {
|
|
396
|
+
lines.push("", this.turnDeliberationPrompt);
|
|
295
397
|
}
|
|
296
398
|
if (persistentMemory) {
|
|
297
399
|
lines.push("", persistentMemory);
|
|
@@ -326,6 +428,58 @@ export class AgentInterruptedError extends Error {
|
|
|
326
428
|
export function isAgentInterruptedError(error) {
|
|
327
429
|
return error instanceof AgentInterruptedError;
|
|
328
430
|
}
|
|
431
|
+
export function partitionToolCalls(toolCalls) {
|
|
432
|
+
const validToolCalls = [];
|
|
433
|
+
const invalidToolCalls = [];
|
|
434
|
+
for (const toolCall of toolCalls) {
|
|
435
|
+
const toolName = toolCall.function.name.trim();
|
|
436
|
+
if (!toolName) {
|
|
437
|
+
invalidToolCalls.push({
|
|
438
|
+
toolName: "(unknown tool)",
|
|
439
|
+
rawArguments: toolCall.function.arguments,
|
|
440
|
+
reason: "Missing tool name."
|
|
441
|
+
});
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const validationError = validateToolCallArguments(toolCall.function.arguments);
|
|
445
|
+
if (validationError) {
|
|
446
|
+
invalidToolCalls.push({
|
|
447
|
+
toolName,
|
|
448
|
+
rawArguments: toolCall.function.arguments,
|
|
449
|
+
reason: validationError
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
validToolCalls.push(toolCall);
|
|
454
|
+
}
|
|
455
|
+
return { validToolCalls, invalidToolCalls };
|
|
456
|
+
}
|
|
457
|
+
export function sanitizeConversationMessages(messages) {
|
|
458
|
+
const sanitized = [];
|
|
459
|
+
let invalidToolCallCount = 0;
|
|
460
|
+
for (const message of messages) {
|
|
461
|
+
if (message.role !== "assistant" || !message.tool_calls || message.tool_calls.length === 0) {
|
|
462
|
+
sanitized.push(message);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const partition = partitionToolCalls(message.tool_calls);
|
|
466
|
+
invalidToolCallCount += partition.invalidToolCalls.length;
|
|
467
|
+
if (partition.invalidToolCalls.length === 0) {
|
|
468
|
+
sanitized.push(message);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if ((message.content ?? "").trim() === "" && partition.validToolCalls.length === 0) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const copy = { ...message };
|
|
475
|
+
delete copy.tool_calls;
|
|
476
|
+
if (partition.validToolCalls.length > 0) {
|
|
477
|
+
copy.tool_calls = partition.validToolCalls;
|
|
478
|
+
}
|
|
479
|
+
sanitized.push(copy);
|
|
480
|
+
}
|
|
481
|
+
return { messages: sanitized, invalidToolCallCount };
|
|
482
|
+
}
|
|
329
483
|
function toolCallSignature(toolCall) {
|
|
330
484
|
try {
|
|
331
485
|
return `${toolCall.function.name}:${JSON.stringify(JSON.parse(toolCall.function.arguments))}`;
|
|
@@ -369,6 +523,49 @@ function envBool(name) {
|
|
|
369
523
|
return undefined;
|
|
370
524
|
}
|
|
371
525
|
}
|
|
526
|
+
function validateToolCallArguments(rawArguments) {
|
|
527
|
+
if (!rawArguments.trim()) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(rawArguments);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return error instanceof Error ? error.message : String(error);
|
|
536
|
+
}
|
|
537
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
538
|
+
return "Tool arguments must be a JSON object.";
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
function malformedToolCallRepairPrompt(invalidToolCalls) {
|
|
543
|
+
const tools = [...new Set(invalidToolCalls.map((issue) => issue.toolName))];
|
|
544
|
+
const lines = [
|
|
545
|
+
"SYSTEM ALERT: The previous assistant turn emitted malformed tool-call JSON, so those tools were not executed.",
|
|
546
|
+
`Affected tools: ${tools.join(", ")}.`,
|
|
547
|
+
"Retry the same intent, but emit valid JSON object arguments only.",
|
|
548
|
+
"Keep tool calls small and precise. Split large refactors or large apply_patch edits into multiple smaller tool calls."
|
|
549
|
+
];
|
|
550
|
+
const applyPatchAffected = tools.some((name) => name === "apply_patch" || name === "replace_in_file");
|
|
551
|
+
if (applyPatchAffected) {
|
|
552
|
+
lines.push("For file edits, prefer smaller exact hunks with short search/replace blocks instead of one giant patch payload.");
|
|
553
|
+
}
|
|
554
|
+
const firstIssue = invalidToolCalls[0];
|
|
555
|
+
if (firstIssue) {
|
|
556
|
+
lines.push(`Last parse error: ${firstIssue.toolName}: ${firstIssue.reason}`);
|
|
557
|
+
}
|
|
558
|
+
lines.push("Continue the original request.");
|
|
559
|
+
return lines.join("\n");
|
|
560
|
+
}
|
|
561
|
+
function summarizeInvalidToolCalls(invalidToolCalls) {
|
|
562
|
+
const first = invalidToolCalls[0];
|
|
563
|
+
if (!first) {
|
|
564
|
+
return "";
|
|
565
|
+
}
|
|
566
|
+
const extra = invalidToolCalls.length - 1;
|
|
567
|
+
return `${first.toolName}: ${first.reason}${extra > 0 ? ` (+${extra} more)` : ""}`;
|
|
568
|
+
}
|
|
372
569
|
function envInt(name) {
|
|
373
570
|
const raw = process.env[name];
|
|
374
571
|
if (!raw) {
|