@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.
Files changed (62) hide show
  1. package/README.md +12 -42
  2. package/dist/src/agent.d.ts +23 -1
  3. package/dist/src/agent.js +314 -117
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/app-meta.d.ts +1 -1
  6. package/dist/src/app-meta.js +1 -1
  7. package/dist/src/context-files.js +1 -0
  8. package/dist/src/context-files.js.map +1 -1
  9. package/dist/src/deliberation.d.ts +15 -0
  10. package/dist/src/deliberation.js +219 -0
  11. package/dist/src/deliberation.js.map +1 -0
  12. package/dist/src/ipc-backend.js +97 -9
  13. package/dist/src/ipc-backend.js.map +1 -1
  14. package/dist/src/ipc-ui.d.ts +6 -2
  15. package/dist/src/ipc-ui.js +15 -2
  16. package/dist/src/ipc-ui.js.map +1 -1
  17. package/dist/src/process-utils.d.ts +1 -0
  18. package/dist/src/process-utils.js +72 -7
  19. package/dist/src/process-utils.js.map +1 -1
  20. package/dist/src/skills/runtime.d.ts +2 -1
  21. package/dist/src/skills/runtime.js +298 -11
  22. package/dist/src/skills/runtime.js.map +1 -1
  23. package/dist/src/skills/types.d.ts +19 -0
  24. package/dist/src/terminal-ui.d.ts +5 -1
  25. package/dist/src/terminal-ui.js +15 -1
  26. package/dist/src/terminal-ui.js.map +1 -1
  27. package/dist/src/tools/filesystem.js +56 -2
  28. package/dist/src/tools/filesystem.js.map +1 -1
  29. package/dist/src/tools/git.d.ts +23 -0
  30. package/dist/src/tools/git.js +309 -13
  31. package/dist/src/tools/git.js.map +1 -1
  32. package/dist/src/tools/repo-search.d.ts +2 -0
  33. package/dist/src/tools/repo-search.js +45 -13
  34. package/dist/src/tools/repo-search.js.map +1 -1
  35. package/dist/src/tools/shell.js +1 -1
  36. package/dist/src/tools/shell.js.map +1 -1
  37. package/dist/src/tools/skill.js +5 -1
  38. package/dist/src/tools/skill.js.map +1 -1
  39. package/dist/src/types.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/skill/agents-md-generator/SKILL.md +13 -0
  42. package/skill/biz-opportunity-scout/SKILL.md +17 -0
  43. package/skill/code-review/SKILL.md +15 -0
  44. package/skill/code-security-audit/SKILL.md +21 -0
  45. package/skill/composition-patterns/SKILL.md +15 -0
  46. package/skill/deploy-to-vercel/SKILL.md +14 -0
  47. package/skill/git-workflow/SKILL.md +16 -0
  48. package/skill/jetbrains-vmoptions/SKILL.md +14 -0
  49. package/skill/kysely-converter/SKILL.md +13 -0
  50. package/skill/react-best-practices/SKILL.md +19 -0
  51. package/skill/react-native-skills/SKILL.md +19 -0
  52. package/skill/react-vite-guide/SKILL.md +18 -0
  53. package/skill/skill-maker/SKILL.md +14 -0
  54. package/skill/system-prompt-creator/SKILL.md +13 -0
  55. package/skill/typst-creator/SKILL.md +14 -0
  56. package/skill/web-design-guidelines/SKILL.md +17 -0
  57. package/tui/vetala-darwin-arm64 +0 -0
  58. package/tui/vetala-darwin-x64 +0 -0
  59. package/tui/vetala-linux-arm64 +0 -0
  60. package/tui/vetala-linux-x64 +0 -0
  61. package/tui/vetala-win32-arm64.exe +0 -0
  62. 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.4
30
+ ### v0.5.6
31
31
  Added:
32
- - Copy last reply action (Ctrl+Y) with a compact inline affordance after assistant replies.
33
- - Resume picker preview tuning and option line wrapping for tighter terminals.
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
- - Repeat-tool warning injection with configurable threshold/message.
37
- - Empty-response warning when a model returns no content (env-tunable).
38
- - Default mouse mode enables in-app scroll wheel; can be disabled for terminal selection.
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.3
40
+ ### v0.5.5
41
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).
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
- - 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
-
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
 
@@ -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
- const userMessage = this.persistedMessage({
25
- role: "user",
26
- content: userInput
27
- });
28
- await this.options.sessionStore.appendMessage(this.options.session, userMessage);
29
- void appendHistoryEntry(this.options.config, this.options.session.id, userInput).catch(() => {
30
- // Best-effort history persistence.
31
- });
32
- const localGreeting = maybeLocalGreeting(userInput);
33
- if (localGreeting) {
34
- await this.appendAndRenderAssistantMessage(localGreeting, false);
35
- return;
36
- }
37
- const provider = this.options.config.providers[this.options.session.provider];
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 assistantMessage = this.persistedMessage({
92
- role: "assistant",
93
- content: turn.content || null,
94
- tool_calls: turn.toolCalls.length > 0 ? turn.toolCalls : null
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
- await this.options.sessionStore.appendMessage(this.options.session, assistantMessage);
97
- if (turn.toolCalls.length === 0) {
98
- this.options.ui.endAssistantTurn();
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
- let suppressedRepeats = 0;
102
- for (const toolCall of turn.toolCalls) {
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.ui.printToolCall(toolCall);
105
- const signature = toolCallSignature(toolCall);
106
- let result;
107
- if (seenToolCalls.has(signature)) {
108
- suppressedRepeats += 1;
109
- this.options.ui.activity(`Skipping repeated ${toolCall.function.name} call.`);
110
- result = {
111
- summary: "Repeated tool call suppressed",
112
- content: "This exact tool call already ran earlier in this turn. Reuse the earlier result instead of calling it again.",
113
- isError: true
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
- else {
117
- seenToolCalls.add(signature);
118
- this.options.ui.activity(`Running ${toolCall.function.name}.`);
119
- const toolSpec = this.options.tools.getTool(toolCall.function.name);
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
- else if (isMutating) {
126
- seenToolCalls.clear();
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
- this.throwIfStopped();
130
- this.options.ui.printToolResult(result.summary, result.isError);
131
- await this.options.sessionStore.appendMessage(this.options.session, this.persistedMessage({
132
- role: "tool",
133
- content: result.content,
134
- tool_call_id: toolCall.id
135
- }));
136
- }
137
- if (shouldInjectRepeatWarning(suppressedRepeats, repeatWarningInjected)) {
138
- repeatWarningInjected += 1;
139
- const warning = this.persistedMessage({
140
- role: "user",
141
- content: toolRepeatWarningMessage()
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, warning);
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.activity(`Thinking with ${providerLabel(this.options.session.provider)} / ${this.options.session.model}.`);
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.endAssistantTurn();
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.options.config.reasoningEffort
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, opts) : Promise.resolve(null)
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 (pinnedSkillContext) {
294
- lines.push("", pinnedSkillContext);
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) {