@denizokcu/haze 0.0.1 → 0.0.2
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/CHANGELOG.md +14 -0
- package/README.md +114 -69
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +203 -11
- package/dist/cli/commands/commands.js +130 -6
- package/dist/cli/commands/formatters.d.ts +1 -0
- package/dist/cli/commands/formatters.js +18 -1
- package/dist/cli/commands/skills.d.ts +1 -1
- package/dist/cli/commands/skills.js +8 -5
- package/dist/cli/commands/streaming.d.ts +2 -0
- package/dist/cli/commands/streaming.js +424 -39
- package/dist/cli/index.js +1 -11
- package/dist/config/paths.d.ts +0 -1
- package/dist/config/paths.js +0 -1
- package/dist/llm/client.js +1 -1
- package/dist/llm/hazeTools.d.ts +32 -0
- package/dist/llm/hazeTools.js +136 -26
- package/dist/llm/initPrompt.js +2 -2
- package/dist/llm/systemPrompt.js +23 -9
- package/dist/skills/SkillLoader.d.ts +12 -2
- package/dist/skills/SkillLoader.js +64 -18
- package/dist/skills/SkillRegistry.d.ts +1 -5
- package/dist/skills/SkillRegistry.js +10 -21
- package/dist/skills/builder/SkillBuilder.d.ts +25 -1
- package/dist/skills/builder/SkillBuilder.js +169 -20
- package/dist/skills/skillTools.d.ts +20 -0
- package/dist/skills/skillTools.js +25 -0
- package/dist/skills/types.d.ts +12 -51
- package/dist/ui/components/Header.d.ts +2 -1
- package/dist/ui/components/Header.js +12 -2
- package/dist/ui/components/TextInput.d.ts +8 -1
- package/dist/ui/components/TextInput.js +29 -14
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +1 -0
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +10 -6
- package/examples/skills/files/SKILL.md +16 -0
- package/examples/skills/files/examples/file-editing.md +3 -0
- package/package.json +2 -2
- package/dist/skills/installer/SkillInstaller.d.ts +0 -1
- package/dist/skills/installer/SkillInstaller.js +0 -48
- package/dist/skills/manifestSchema.d.ts +0 -31
- package/dist/skills/manifestSchema.js +0 -23
- package/dist/tools/ToolExecutor.d.ts +0 -3
- package/dist/tools/ToolExecutor.js +0 -15
- package/dist/tools/types.d.ts +0 -9
- package/dist/tools/types.js +0 -1
- package/examples/skills/files/prompts/file_tasks.md +0 -1
- package/examples/skills/files/skill.yaml +0 -28
- package/examples/skills/files/tools/list_files.ts +0 -21
- package/examples/skills/files/tools/read_file.ts +0 -12
|
@@ -2,41 +2,354 @@ import { stepCountIs, streamText } from 'ai';
|
|
|
2
2
|
import { model } from '../../llm/client.js';
|
|
3
3
|
import { hazeTools } from '../../llm/hazeTools.js';
|
|
4
4
|
import { buildSystemPrompt } from '../../llm/systemPrompt.js';
|
|
5
|
+
import { loadSkillRegistry } from '../../skills/SkillRegistry.js';
|
|
6
|
+
import { buildSkillTools } from '../../skills/skillTools.js';
|
|
5
7
|
import { compact, toolCallSummary, toolResultSummary, formatSeconds } from './formatters.js';
|
|
8
|
+
function stableToolKey(toolCall) {
|
|
9
|
+
return `${toolCall.toolName}:${JSON.stringify(toolCall.input)}`;
|
|
10
|
+
}
|
|
11
|
+
function uniqueRepeatedToolNames(toolCalls) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const repeated = new Set();
|
|
14
|
+
for (const toolCall of toolCalls) {
|
|
15
|
+
const key = stableToolKey(toolCall);
|
|
16
|
+
if (seen.has(key))
|
|
17
|
+
repeated.add(toolCall.toolName);
|
|
18
|
+
seen.add(key);
|
|
19
|
+
}
|
|
20
|
+
return [...repeated];
|
|
21
|
+
}
|
|
22
|
+
function toolOnlyStepCount(steps) {
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const step of [...steps].reverse()) {
|
|
25
|
+
if (step.toolCalls.length === 0 || step.text.trim().length > 0)
|
|
26
|
+
break;
|
|
27
|
+
count += 1;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
function isPlanOnlyRequest(value) {
|
|
32
|
+
return /\b(create|make|write|draft|outline)\s+(?:a\s+)?plan\b|\bplan\s+(?:for|to)\b/i.test(value) && !/\bimplement|execute|do\b/i.test(value);
|
|
33
|
+
}
|
|
34
|
+
function isLikelyActionRequest(value) {
|
|
35
|
+
if (isPlanOnlyRequest(value))
|
|
36
|
+
return false;
|
|
37
|
+
return /\b(add|create|write|implement|update|fix|change|support|wire|test|tests|document|docs|documentation|run|verify)\b/i.test(value);
|
|
38
|
+
}
|
|
39
|
+
function isValidationRequest(value) {
|
|
40
|
+
if (isPlanOnlyRequest(value))
|
|
41
|
+
return false;
|
|
42
|
+
return /\b(run|verify|test|tests|check|validate)\b/i.test(value);
|
|
43
|
+
}
|
|
44
|
+
function isPlanImplementationRequest(value) {
|
|
45
|
+
return /\b(implement|execute|do)\b.*\bplan\b|\bplan\.md\b|\btest_plan\.md\b/i.test(value);
|
|
46
|
+
}
|
|
47
|
+
function looksIncomplete(text) {
|
|
48
|
+
return /\b(incomplete|what remains|remains:|next:|not implemented|not created|no tests exist|created no docs|has not been|have not been|not yet|never executed|not executed|not run|cannot retry|cannot write|cannot validate|tool budget reached)\b/i.test(text);
|
|
49
|
+
}
|
|
50
|
+
function sanitizeAssistantText(text) {
|
|
51
|
+
return [...text].filter(char => {
|
|
52
|
+
const code = char.charCodeAt(0);
|
|
53
|
+
return !(code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || code === 127 || code === 155);
|
|
54
|
+
}).join('');
|
|
55
|
+
}
|
|
56
|
+
function toolInputPath(input) {
|
|
57
|
+
return typeof input === 'object' && input != null && 'path' in input && typeof input.path === 'string'
|
|
58
|
+
? input.path
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
function isDuplicateSkippedOutput(output) {
|
|
62
|
+
return typeof output === 'object' && output != null && 'duplicateSkipped' in output && output.duplicateSkipped === true;
|
|
63
|
+
}
|
|
6
64
|
export async function runAgentTurn(value, displayValue, contextFiles, callbacks) {
|
|
7
65
|
const displayVal = displayValue ?? value;
|
|
8
66
|
const userMessage = { role: 'user', text: displayVal };
|
|
9
67
|
callbacks.setBusy(true);
|
|
10
68
|
callbacks.addMessage(userMessage);
|
|
69
|
+
const abortController = new AbortController();
|
|
70
|
+
callbacks.setAbortController?.(abortController);
|
|
71
|
+
let idleTimer;
|
|
72
|
+
const resetIdleTimer = () => {
|
|
73
|
+
if (idleTimer)
|
|
74
|
+
clearTimeout(idleTimer);
|
|
75
|
+
idleTimer = setTimeout(() => abortController.abort('Haze turn timed out after no model/tool activity.'), 90_000);
|
|
76
|
+
};
|
|
11
77
|
try {
|
|
12
78
|
const m = await model();
|
|
13
79
|
if (!m) {
|
|
14
|
-
callbacks.addMessage({ role: 'assistant', text: 'No
|
|
80
|
+
callbacks.addMessage({ role: 'assistant', text: 'No API key configured. Run /login, then /model x-ai/grok-build-0.1. Haze cannot hallucinate without credentials. Progress.' });
|
|
15
81
|
return;
|
|
16
82
|
}
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
83
|
+
const activeModel = m;
|
|
84
|
+
const skillRegistry = await loadSkillRegistry();
|
|
85
|
+
const availableTools = { ...hazeTools, ...buildSkillTools(skillRegistry) };
|
|
86
|
+
const likelyPlanOnlyRequest = isPlanOnlyRequest(value);
|
|
87
|
+
const likelyPlanImplementationRequest = isPlanImplementationRequest(value);
|
|
88
|
+
const likelyActionRequest = isLikelyActionRequest(value);
|
|
89
|
+
const likelyValidationRequest = isValidationRequest(value);
|
|
90
|
+
const planImplementationGuidance = 'When implementing a plan file, first identify the concrete required checklist items and compare them with the current files. Do not edit source or tests when the required behavior is already present. Implement the smallest clearly required phase or required items, skip optional/design-question items unless explicitly requested, add tests rather than exploratory one-off scripts where possible, use file tools (not bash) for any file changes, run validation once after code/test edits, then update plan status with file tools if requested. Do not call unresolved optional scope a blocker.';
|
|
91
|
+
const requestMessages = likelyPlanImplementationRequest
|
|
92
|
+
? [...callbacks.getConversation(), { role: 'user', content: value }, { role: 'user', content: planImplementationGuidance }]
|
|
93
|
+
: [...callbacks.getConversation(), { role: 'user', content: value }];
|
|
23
94
|
callbacks.setConversation(requestMessages);
|
|
24
|
-
|
|
95
|
+
resetIdleTimer();
|
|
96
|
+
let currentAssistantId = `assistant-${Date.now()}`;
|
|
25
97
|
let assistantStarted = false;
|
|
98
|
+
let currentAssistantText = '';
|
|
26
99
|
let assistantText = '';
|
|
100
|
+
let toolEpoch = 0;
|
|
101
|
+
let currentAssistantToolEpoch = 0;
|
|
27
102
|
let editFileFailed = false;
|
|
28
103
|
let mutatingToolSucceeded = false;
|
|
104
|
+
let validationToolSucceeded = false;
|
|
105
|
+
let sawReadOnlyTool = false;
|
|
106
|
+
let sawToolCall = false;
|
|
107
|
+
let textAfterTool = false;
|
|
108
|
+
let forcedContinuationUsed = false;
|
|
109
|
+
let secondContinuationUsed = false;
|
|
110
|
+
let editRecoveryPath;
|
|
111
|
+
let editRecoveryReadSatisfied = false;
|
|
29
112
|
const toolSummaries = [];
|
|
30
|
-
|
|
113
|
+
const toolExecutionContext = { inFlightToolCalls: new Map() };
|
|
114
|
+
const toolGroupId = `tools-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
115
|
+
const toolDisplayItems = [];
|
|
116
|
+
let toolGroupStarted = false;
|
|
117
|
+
function renderToolGroup(streaming) {
|
|
118
|
+
const visibleItems = toolDisplayItems.filter(item => !item.hidden);
|
|
119
|
+
const grouped = new Map();
|
|
120
|
+
for (const item of visibleItems) {
|
|
121
|
+
const key = `${item.status}:${item.summary}:${item.result ?? ''}`;
|
|
122
|
+
const current = grouped.get(key);
|
|
123
|
+
if (current)
|
|
124
|
+
current.count += 1;
|
|
125
|
+
else
|
|
126
|
+
grouped.set(key, { item, count: 1 });
|
|
127
|
+
}
|
|
128
|
+
const rows = [...grouped.values()];
|
|
129
|
+
const running = visibleItems.some(item => item.status === 'running');
|
|
130
|
+
const header = running || streaming ? 'Running tools' : `Tools: ${visibleItems.length} call${visibleItems.length === 1 ? '' : 's'}`;
|
|
131
|
+
const lines = rows.map(({ item, count }) => {
|
|
132
|
+
const icon = item.status === 'running' ? '…' : item.status === 'success' ? '✓' : '✗';
|
|
133
|
+
const countText = count > 1 ? ` ×${count}` : '';
|
|
134
|
+
const result = item.status === 'running' ? '' : ` — ${item.result ?? item.status}${item.durationMs == null ? '' : ` in ${formatSeconds(item.durationMs)}`}`;
|
|
135
|
+
return ` ${icon} ${item.summary}${countText}${result}`;
|
|
136
|
+
});
|
|
137
|
+
return [header, ...lines].join('\n');
|
|
138
|
+
}
|
|
139
|
+
function updateToolGroup(streaming = true) {
|
|
140
|
+
const text = renderToolGroup(streaming);
|
|
141
|
+
if (!toolGroupStarted) {
|
|
142
|
+
toolGroupStarted = true;
|
|
143
|
+
callbacks.addMessage({ id: toolGroupId, role: 'tool', text, streaming });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
callbacks.updateMessage(toolGroupId, { text, streaming });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function recordToolStart(toolCall) {
|
|
150
|
+
toolDisplayItems.push({ id: toolCall.toolCallId, summary: toolCallSummary(toolCall.toolName, toolCall.input), status: 'running' });
|
|
151
|
+
updateToolGroup(true);
|
|
152
|
+
}
|
|
153
|
+
function recordToolDisplayFinish(event) {
|
|
154
|
+
const item = toolDisplayItems.find(candidate => candidate.id === event.toolCall.toolCallId);
|
|
155
|
+
if (!item)
|
|
156
|
+
return;
|
|
157
|
+
item.status = event.success ? 'success' : 'error';
|
|
158
|
+
item.result = toolResultSummary(event);
|
|
159
|
+
item.durationMs = event.durationMs;
|
|
160
|
+
item.hidden = isDuplicateSkippedOutput(event.output);
|
|
161
|
+
updateToolGroup(toolDisplayItems.some(candidate => candidate.status === 'running'));
|
|
162
|
+
}
|
|
163
|
+
callbacks.debugLog(`request started with ${requestMessages.length} conversation messages; action=${likelyActionRequest}`);
|
|
164
|
+
function recordToolFinish(event) {
|
|
165
|
+
const path = toolInputPath(event.toolCall.input);
|
|
166
|
+
const duplicateSkipped = isDuplicateSkippedOutput(event.output);
|
|
167
|
+
if (!event.success && ['editFile', 'replaceLines', 'writeFile'].includes(event.toolCall.toolName)) {
|
|
168
|
+
editFileFailed = true;
|
|
169
|
+
editRecoveryPath = path;
|
|
170
|
+
editRecoveryReadSatisfied = false;
|
|
171
|
+
}
|
|
172
|
+
if (event.success && ['listFiles', 'readFile'].includes(event.toolCall.toolName))
|
|
173
|
+
sawReadOnlyTool = true;
|
|
174
|
+
if (event.success && event.toolCall.toolName === 'readFile' && path && path === editRecoveryPath && !duplicateSkipped) {
|
|
175
|
+
editRecoveryReadSatisfied = true;
|
|
176
|
+
}
|
|
177
|
+
if (event.success && ['editFile', 'replaceLines', 'writeFile'].includes(event.toolCall.toolName)) {
|
|
178
|
+
mutatingToolSucceeded = true;
|
|
179
|
+
if (!path || path === editRecoveryPath) {
|
|
180
|
+
editRecoveryPath = undefined;
|
|
181
|
+
editRecoveryReadSatisfied = false;
|
|
182
|
+
editFileFailed = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (event.success && event.toolCall.toolName === 'bash') {
|
|
186
|
+
const ok = typeof event.output === 'object' && event.output != null && 'ok' in event.output ? Boolean(event.output.ok) : true;
|
|
187
|
+
if (ok)
|
|
188
|
+
validationToolSucceeded = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function streamAssistantResponse(messages, reason, prompt, allowTools = false) {
|
|
192
|
+
callbacks.debugLog(`requesting assistant ${allowTools ? 'continuation' : 'text'}: ${reason}`);
|
|
193
|
+
const responseId = `assistant-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
194
|
+
let responseStarted = false;
|
|
195
|
+
let responseText = '';
|
|
196
|
+
let continuationToolCalls = 0;
|
|
197
|
+
const continuationMessages = [
|
|
198
|
+
...messages,
|
|
199
|
+
{ role: 'user', content: prompt },
|
|
200
|
+
];
|
|
201
|
+
const followUp = streamText({
|
|
202
|
+
model: activeModel,
|
|
203
|
+
temperature: 0,
|
|
204
|
+
system: buildSystemPrompt(contextFiles),
|
|
205
|
+
messages: continuationMessages,
|
|
206
|
+
tools: availableTools,
|
|
207
|
+
toolChoice: allowTools ? 'auto' : 'none',
|
|
208
|
+
stopWhen: stepCountIs(10),
|
|
209
|
+
abortSignal: abortController.signal,
|
|
210
|
+
experimental_context: toolExecutionContext,
|
|
211
|
+
prepareStep({ steps, messages }) {
|
|
212
|
+
continuationToolCalls = steps.flatMap(step => step.toolCalls).length;
|
|
213
|
+
if (continuationToolCalls >= 10 || toolOnlyStepCount(steps) >= 5) {
|
|
214
|
+
return {
|
|
215
|
+
toolChoice: 'none',
|
|
216
|
+
messages: [
|
|
217
|
+
...messages,
|
|
218
|
+
{ role: 'user', content: 'Tool budget reached. If the current request is complete, summarize only current-turn changes and validation. If incomplete, state the concrete blocker briefly; do not claim tools are unavailable and do not recap unrelated earlier tasks.' },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (likelyPlanOnlyRequest && mutatingToolSucceeded) {
|
|
223
|
+
return {
|
|
224
|
+
toolChoice: 'none',
|
|
225
|
+
messages: [
|
|
226
|
+
...messages,
|
|
227
|
+
{ role: 'user', content: 'This was a planning request and the plan artifact has been created or updated. Stop using tools and summarize the plan file only; do not implement or validate it.' },
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (editRecoveryPath && !editRecoveryReadSatisfied) {
|
|
232
|
+
return {
|
|
233
|
+
activeTools: ['readFile'],
|
|
234
|
+
messages: [
|
|
235
|
+
...messages,
|
|
236
|
+
{ role: 'user', content: `A previous edit failed for ${editRecoveryPath}. Before any further edit or bash inspection, call readFile on exactly ${editRecoveryPath}. Bash/cat does not satisfy this recovery step.` },
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (editFileFailed)
|
|
241
|
+
return { activeTools: ['listFiles', 'readFile', 'replaceLines', 'writeFile', 'bash'] };
|
|
242
|
+
return undefined;
|
|
243
|
+
},
|
|
244
|
+
onError({ error }) {
|
|
245
|
+
callbacks.debugLog(`stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
246
|
+
},
|
|
247
|
+
onFinish(event) {
|
|
248
|
+
callbacks.setConversation([...continuationMessages, ...event.response.messages]);
|
|
249
|
+
callbacks.debugLog(`conversation updated to ${continuationMessages.length + event.response.messages.length} messages after follow-up`);
|
|
250
|
+
},
|
|
251
|
+
experimental_onToolCallStart({ toolCall }) {
|
|
252
|
+
sawToolCall = true;
|
|
253
|
+
recordToolStart(toolCall);
|
|
254
|
+
resetIdleTimer();
|
|
255
|
+
callbacks.debugLog(`follow-up tool start: ${toolCall.toolName} ${compact(toolCall.input)}`);
|
|
256
|
+
},
|
|
257
|
+
experimental_onToolCallFinish(event) {
|
|
258
|
+
resetIdleTimer();
|
|
259
|
+
recordToolFinish(event);
|
|
260
|
+
const summary = toolResultSummary(event);
|
|
261
|
+
toolSummaries.push(`${event.toolCall.toolName}: ${summary}`);
|
|
262
|
+
recordToolDisplayFinish(event);
|
|
263
|
+
if (!isDuplicateSkippedOutput(event.output))
|
|
264
|
+
toolEpoch += 1;
|
|
265
|
+
callbacks.debugLog(event.success
|
|
266
|
+
? `follow-up tool done: ${event.toolCall.toolName} after ${event.durationMs}ms ${compact(event.output)}`
|
|
267
|
+
: `follow-up tool error: ${event.toolCall.toolName} after ${event.durationMs}ms ${compact(event.error)}`);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
for await (const rawDelta of followUp.textStream) {
|
|
271
|
+
resetIdleTimer();
|
|
272
|
+
const delta = sanitizeAssistantText(rawDelta);
|
|
273
|
+
responseText += delta;
|
|
274
|
+
if (!responseStarted) {
|
|
275
|
+
responseStarted = true;
|
|
276
|
+
callbacks.addMessage({ id: responseId, role: 'assistant', text: delta, streaming: true });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
callbacks.updateMessage(responseId, { text: responseText });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (responseStarted) {
|
|
283
|
+
callbacks.setLastAssistantText(responseText.trim());
|
|
284
|
+
callbacks.updateMessage(responseId, { streaming: false });
|
|
285
|
+
}
|
|
286
|
+
return responseText.trim();
|
|
287
|
+
}
|
|
31
288
|
const result = streamText({
|
|
32
|
-
model:
|
|
289
|
+
model: activeModel,
|
|
290
|
+
temperature: 0,
|
|
33
291
|
system: buildSystemPrompt(contextFiles),
|
|
34
292
|
messages: requestMessages,
|
|
35
|
-
tools:
|
|
36
|
-
stopWhen: stepCountIs(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
293
|
+
tools: availableTools,
|
|
294
|
+
stopWhen: stepCountIs(12),
|
|
295
|
+
abortSignal: abortController.signal,
|
|
296
|
+
experimental_context: toolExecutionContext,
|
|
297
|
+
onError({ error }) {
|
|
298
|
+
callbacks.debugLog(`stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
299
|
+
},
|
|
300
|
+
prepareStep({ steps, messages }) {
|
|
301
|
+
const toolCalls = steps.flatMap(step => step.toolCalls);
|
|
302
|
+
const repeatedToolNames = uniqueRepeatedToolNames(toolCalls);
|
|
303
|
+
const repeatedToolCall = repeatedToolNames.length > 0;
|
|
304
|
+
const consecutiveToolOnlySteps = toolOnlyStepCount(steps);
|
|
305
|
+
if (likelyPlanOnlyRequest && mutatingToolSucceeded) {
|
|
306
|
+
return {
|
|
307
|
+
toolChoice: 'none',
|
|
308
|
+
messages: [
|
|
309
|
+
...messages,
|
|
310
|
+
{ role: 'user', content: 'This was a planning request and the plan artifact has been created or updated. Stop using tools and summarize the plan file only; do not implement or validate it.' },
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (editRecoveryPath && !editRecoveryReadSatisfied) {
|
|
315
|
+
return {
|
|
316
|
+
activeTools: ['readFile'],
|
|
317
|
+
messages: [
|
|
318
|
+
...messages,
|
|
319
|
+
{ role: 'user', content: `A previous edit failed for ${editRecoveryPath}. Before any further edit or bash inspection, call readFile on exactly ${editRecoveryPath}. Bash/cat does not satisfy this recovery step.` },
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (repeatedToolCall) {
|
|
324
|
+
const activeTools = Object.keys(availableTools).filter(name => !repeatedToolNames.includes(name));
|
|
325
|
+
callbacks.debugLog(`disabling repeated tools for next step: ${repeatedToolNames.join(', ')}`);
|
|
326
|
+
return {
|
|
327
|
+
activeTools,
|
|
328
|
+
messages: [
|
|
329
|
+
...messages,
|
|
330
|
+
{ role: 'user', content: `You already called ${repeatedToolNames.join(', ')} with the same input. Do not repeat that tool call. Use a different relevant tool. If this is an action request and no file change has been made yet, continue with edit/write tools rather than summarizing.` },
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (likelyActionRequest && !mutatingToolSucceeded && consecutiveToolOnlySteps >= 3 && toolCalls.length < 10) {
|
|
335
|
+
callbacks.debugLog('nudging action request toward mutation after read-only steps');
|
|
336
|
+
return {
|
|
337
|
+
messages: [
|
|
338
|
+
...messages,
|
|
339
|
+
{ role: 'user', content: 'You have inspected enough for now. This is an action request; make the requested change with editFile, replaceLines, or writeFile instead of saying tools are unavailable or summarizing.' },
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (toolCalls.length >= 12 || consecutiveToolOnlySteps >= 5) {
|
|
344
|
+
callbacks.debugLog('forcing text response to avoid tool loop');
|
|
345
|
+
return {
|
|
346
|
+
toolChoice: 'none',
|
|
347
|
+
messages: [
|
|
348
|
+
...messages,
|
|
349
|
+
{ role: 'user', content: 'Tool budget reached. If the current request is complete, summarize only current-turn changes and validation. If the requested change is incomplete, state the concrete blocker briefly. Do not claim tools are unavailable, recap unrelated earlier tasks, or provide a generic remains list.' },
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
40
353
|
if (editFileFailed)
|
|
41
354
|
return { activeTools: ['listFiles', 'readFile', 'replaceLines', 'writeFile', 'bash'] };
|
|
42
355
|
return undefined;
|
|
@@ -44,58 +357,130 @@ export async function runAgentTurn(value, displayValue, contextFiles, callbacks)
|
|
|
44
357
|
onStepFinish({ stepNumber, text, toolCalls, toolResults, finishReason }) {
|
|
45
358
|
callbacks.debugLog(`step ${stepNumber} finished: ${finishReason}; text=${text.length}; toolCalls=${toolCalls.length}; toolResults=${toolResults.length}`);
|
|
46
359
|
},
|
|
47
|
-
onFinish(
|
|
48
|
-
const nextConversation = [...requestMessages, ...response.messages];
|
|
360
|
+
onFinish(event) {
|
|
361
|
+
const nextConversation = [...requestMessages, ...event.response.messages];
|
|
49
362
|
callbacks.setConversation(nextConversation);
|
|
50
363
|
callbacks.debugLog(`conversation updated to ${nextConversation.length} messages`);
|
|
51
364
|
},
|
|
52
365
|
experimental_onToolCallStart({ toolCall }) {
|
|
53
|
-
|
|
54
|
-
|
|
366
|
+
sawToolCall = true;
|
|
367
|
+
recordToolStart(toolCall);
|
|
368
|
+
resetIdleTimer();
|
|
55
369
|
callbacks.debugLog(`tool start: ${toolCall.toolName} ${compact(toolCall.input)}`);
|
|
56
370
|
},
|
|
57
371
|
experimental_onToolCallFinish(event) {
|
|
372
|
+
resetIdleTimer();
|
|
58
373
|
const summary = toolResultSummary(event);
|
|
59
|
-
|
|
60
|
-
if (!event.success && event.toolCall.toolName === 'editFile')
|
|
61
|
-
editFileFailed = true;
|
|
62
|
-
if (event.success && ['editFile', 'replaceLines', 'writeFile'].includes(event.toolCall.toolName))
|
|
63
|
-
mutatingToolSucceeded = true;
|
|
374
|
+
recordToolFinish(event);
|
|
64
375
|
toolSummaries.push(`${event.toolCall.toolName}: ${summary}`);
|
|
65
|
-
|
|
376
|
+
recordToolDisplayFinish(event);
|
|
377
|
+
if (!isDuplicateSkippedOutput(event.output))
|
|
378
|
+
toolEpoch += 1;
|
|
66
379
|
callbacks.debugLog(event.success
|
|
67
380
|
? `tool done: ${event.toolCall.toolName} after ${event.durationMs}ms ${compact(event.output)}`
|
|
68
381
|
: `tool error: ${event.toolCall.toolName} after ${event.durationMs}ms ${compact(event.error)}`);
|
|
69
382
|
}
|
|
70
383
|
});
|
|
71
|
-
for await (const
|
|
384
|
+
for await (const rawDelta of result.textStream) {
|
|
385
|
+
resetIdleTimer();
|
|
386
|
+
const delta = sanitizeAssistantText(rawDelta);
|
|
387
|
+
if (sawToolCall)
|
|
388
|
+
textAfterTool = true;
|
|
389
|
+
if (currentAssistantText.length > 0 && toolEpoch > currentAssistantToolEpoch) {
|
|
390
|
+
callbacks.updateMessage(currentAssistantId, { streaming: false });
|
|
391
|
+
currentAssistantId = `assistant-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
392
|
+
currentAssistantText = '';
|
|
393
|
+
currentAssistantToolEpoch = toolEpoch;
|
|
394
|
+
}
|
|
72
395
|
assistantText += delta;
|
|
73
|
-
|
|
396
|
+
currentAssistantText += delta;
|
|
397
|
+
if (currentAssistantText === delta) {
|
|
74
398
|
assistantStarted = true;
|
|
75
|
-
callbacks.addMessage({ id:
|
|
399
|
+
callbacks.addMessage({ id: currentAssistantId, role: 'assistant', text: currentAssistantText, streaming: true });
|
|
76
400
|
}
|
|
77
401
|
else {
|
|
78
|
-
callbacks.updateMessage(
|
|
402
|
+
callbacks.updateMessage(currentAssistantId, { text: currentAssistantText });
|
|
79
403
|
}
|
|
80
404
|
}
|
|
81
|
-
|
|
405
|
+
let completedConversation = callbacks.getConversation();
|
|
406
|
+
try {
|
|
407
|
+
const response = await result.response;
|
|
408
|
+
completedConversation = [...requestMessages, ...response.messages];
|
|
409
|
+
callbacks.setConversation(completedConversation);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Keep the conversation from onFinish if the response promise is unavailable.
|
|
413
|
+
}
|
|
414
|
+
callbacks.debugLog(`response stream finished; session has ${completedConversation.length} model messages`);
|
|
415
|
+
const finalAssistantText = assistantText.trim();
|
|
416
|
+
const assistantAdmitsIncomplete = looksIncomplete(finalAssistantText);
|
|
417
|
+
const requestCompletedByTools = mutatingToolSucceeded && validationToolSucceeded && !editRecoveryPath;
|
|
418
|
+
const needsActionContinuation = likelyActionRequest
|
|
419
|
+
&& !requestCompletedByTools
|
|
420
|
+
&& ((sawReadOnlyTool && !mutatingToolSucceeded) || editFileFailed || assistantAdmitsIncomplete);
|
|
421
|
+
const needsValidationContinuation = likelyValidationRequest && !requestCompletedByTools && !validationToolSucceeded && (sawReadOnlyTool || mutatingToolSucceeded || assistantAdmitsIncomplete);
|
|
82
422
|
if (assistantStarted) {
|
|
83
|
-
callbacks.setLastAssistantText(
|
|
84
|
-
callbacks.updateMessage(
|
|
423
|
+
callbacks.setLastAssistantText(finalAssistantText);
|
|
424
|
+
callbacks.updateMessage(currentAssistantId, { streaming: false });
|
|
425
|
+
if ((needsActionContinuation || needsValidationContinuation) && !forcedContinuationUsed) {
|
|
426
|
+
forcedContinuationUsed = true;
|
|
427
|
+
callbacks.updateMessage(currentAssistantId, { text: 'Continuing to complete the requested change...', streaming: false });
|
|
428
|
+
const prompt = editFileFailed
|
|
429
|
+
? 'Your editFile attempt failed. Use the latest readFile line-numbered output and replaceLines to complete the requested change. Continue with any remaining tests or validation if relevant. Do not stop with a summary.'
|
|
430
|
+
: needsValidationContinuation
|
|
431
|
+
? 'You have not run the requested validation yet. Continue now by running the appropriate test/check command. Summarize only after the command finishes.'
|
|
432
|
+
: mutatingToolSucceeded
|
|
433
|
+
? 'Your previous response says the current request is incomplete. Continue now with the remaining edits and validation for this same request. Do not summarize a plan unless blocked.'
|
|
434
|
+
: 'You inspected files but have not made the requested change yet. Continue now by editing or writing the necessary files. Do not summarize a plan unless blocked.';
|
|
435
|
+
const continuationText = await streamAssistantResponse(completedConversation, 'current-turn completion gate', prompt, true);
|
|
436
|
+
if (!secondContinuationUsed && looksIncomplete(continuationText) && (likelyActionRequest || likelyValidationRequest)) {
|
|
437
|
+
secondContinuationUsed = true;
|
|
438
|
+
await streamAssistantResponse(callbacks.getConversation(), 'post-continuation completion gate', 'Your previous response still described unfinished work, missing validation, or a tool-budget issue. If any tools are still available, complete the remaining edit or run the final validation now. Only call something a blocker if a concrete tool failure prevents progress.', true);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (sawToolCall && !textAfterTool) {
|
|
442
|
+
const followUpText = await streamAssistantResponse(completedConversation, 'tool use completed without follow-up text', 'Continue from the tool result and answer my original request. Do not call tools. Summarize only current-turn changes and validation; do not recap unrelated earlier tasks.', false);
|
|
443
|
+
if (!followUpText) {
|
|
444
|
+
callbacks.addMessage({ role: 'assistant', text: 'Stopped after tool use without a follow-up response. You can ask me to continue if the task is not complete.' });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (sawToolCall) {
|
|
449
|
+
const allowTools = (likelyActionRequest && (!mutatingToolSucceeded || editFileFailed)) || (likelyValidationRequest && !validationToolSucceeded);
|
|
450
|
+
const prompt = allowTools
|
|
451
|
+
? 'Continue the original request now. If it asks for a change, edit or write the necessary files. If it asks to run or verify tests, run the command. Do not provide only a retrospective summary unless blocked.'
|
|
452
|
+
: 'Continue from the tool result and answer my original request. Do not call tools. Summarize only current-turn changes and validation; do not recap unrelated earlier tasks.';
|
|
453
|
+
const followUpText = await streamAssistantResponse(completedConversation, 'tool-only turn completed without text', prompt, allowTools);
|
|
454
|
+
if (!secondContinuationUsed && allowTools && looksIncomplete(followUpText)) {
|
|
455
|
+
secondContinuationUsed = true;
|
|
456
|
+
await streamAssistantResponse(callbacks.getConversation(), 'post-follow-up completion gate', 'Your previous response still described unfinished work, missing validation, or a tool-budget issue. If any tools are still available, complete the remaining edit or run the final validation now. Only call something a blocker if a concrete tool failure prevents progress.', true);
|
|
457
|
+
}
|
|
458
|
+
if (!followUpText) {
|
|
459
|
+
const fallback = toolSummaries.length > 0
|
|
460
|
+
? `Finished tool work but the model did not produce a final response. Last tool result: ${toolSummaries.at(-1)}.`
|
|
461
|
+
: 'Finished without a text response.';
|
|
462
|
+
callbacks.addMessage({ id: currentAssistantId, role: 'assistant', text: fallback, streaming: false });
|
|
463
|
+
}
|
|
85
464
|
}
|
|
86
465
|
else {
|
|
87
|
-
|
|
88
|
-
? `Finished tool work but the model did not produce a final response. Last tool result: ${toolSummaries.at(-1)}.`
|
|
89
|
-
: 'Finished without a text response.';
|
|
90
|
-
callbacks.addMessage({ id: assistantId, role: 'assistant', text: fallback, streaming: false });
|
|
466
|
+
callbacks.addMessage({ id: currentAssistantId, role: 'assistant', text: 'Finished without a text response.', streaming: false });
|
|
91
467
|
}
|
|
92
468
|
}
|
|
93
469
|
catch (error) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
470
|
+
if (abortController.signal.aborted) {
|
|
471
|
+
callbacks.debugLog('request aborted');
|
|
472
|
+
callbacks.addMessage({ role: 'system', text: 'Thinking aborted. You can type again.' });
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
476
|
+
callbacks.debugLog(`error: ${text}`);
|
|
477
|
+
callbacks.addMessage({ role: 'assistant', text: `Model call failed: ${text}` });
|
|
478
|
+
}
|
|
97
479
|
}
|
|
98
480
|
finally {
|
|
481
|
+
if (idleTimer)
|
|
482
|
+
clearTimeout(idleTimer);
|
|
483
|
+
callbacks.setAbortController?.(null);
|
|
99
484
|
callbacks.setBusy(false);
|
|
100
485
|
}
|
|
101
486
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -3,9 +3,6 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
-
import { listSkills, infoSkill, removeSkill, validateSkill } from './commands/skills.js';
|
|
7
|
-
import { buildSkill } from '../skills/builder/SkillBuilder.js';
|
|
8
|
-
import { installSkill } from '../skills/installer/SkillInstaller.js';
|
|
9
6
|
import { chatCommand } from './commands/chat.js';
|
|
10
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
8
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
|
|
@@ -16,15 +13,8 @@ program
|
|
|
16
13
|
.version(pkg.version)
|
|
17
14
|
.option('--debug', 'show simple model/tool debug logs in the chat UI');
|
|
18
15
|
program.action(async () => {
|
|
19
|
-
await chatCommand({ debug: Boolean(program.opts().debug) });
|
|
16
|
+
await chatCommand({ debug: Boolean(program.opts().debug), version: pkg.version });
|
|
20
17
|
});
|
|
21
|
-
program.command('build-skill <description...>').description('Deliberately design and create a new file-based skill').action(async (d) => buildSkill(d.join(' ')));
|
|
22
|
-
program.command('install-skill <githubRepo>').description('Install a skill from GitHub with mandatory approval').action(installSkill);
|
|
23
|
-
const skills = program.command('skills').description('Manage skills');
|
|
24
|
-
skills.command('list').description('List installed skills').action(listSkills);
|
|
25
|
-
skills.command('info <name>').description('Show skill details').action(infoSkill);
|
|
26
|
-
skills.command('remove <name>').description('Remove an installed skill').action(removeSkill);
|
|
27
|
-
skills.command('validate <dir>').description('Validate a skill directory').action(validateSkill);
|
|
28
18
|
program.parseAsync().catch(error => {
|
|
29
19
|
console.error(error instanceof Error ? error.message : error);
|
|
30
20
|
process.exit(1);
|
package/dist/config/paths.d.ts
CHANGED
package/dist/config/paths.js
CHANGED
package/dist/llm/client.js
CHANGED
|
@@ -4,7 +4,7 @@ export async function model() {
|
|
|
4
4
|
const settings = await readSettings();
|
|
5
5
|
const baseURL = process.env.OPENAI_BASE_URL ?? settings.baseURL;
|
|
6
6
|
const apiKey = process.env.OPENAI_API_KEY ?? settings.apiKey;
|
|
7
|
-
const name = process.env.HAZE_MODEL ?? settings.model ?? '
|
|
7
|
+
const name = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
|
|
8
8
|
if (!apiKey)
|
|
9
9
|
return null;
|
|
10
10
|
return createOpenAI({ apiKey, baseURL })(name);
|
package/dist/llm/hazeTools.d.ts
CHANGED
|
@@ -4,10 +4,18 @@ export declare const hazeTools: {
|
|
|
4
4
|
recursive: boolean;
|
|
5
5
|
maxEntries: number;
|
|
6
6
|
includeIgnored: boolean;
|
|
7
|
+
cursor?: string | undefined;
|
|
7
8
|
}, {
|
|
9
|
+
ok: true;
|
|
10
|
+
duplicateSkipped: true;
|
|
11
|
+
toolName: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
} | {
|
|
8
14
|
path: string;
|
|
9
15
|
recursive: boolean;
|
|
10
16
|
includeIgnored: boolean;
|
|
17
|
+
cursor: string | undefined;
|
|
18
|
+
nextCursor: string | undefined;
|
|
11
19
|
ignoredSkipped: number;
|
|
12
20
|
entries: {
|
|
13
21
|
path: string;
|
|
@@ -22,6 +30,11 @@ export declare const hazeTools: {
|
|
|
22
30
|
offset?: number | undefined;
|
|
23
31
|
limit?: number | undefined;
|
|
24
32
|
}, {
|
|
33
|
+
ok: true;
|
|
34
|
+
duplicateSkipped: true;
|
|
35
|
+
toolName: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
} | {
|
|
25
38
|
text: string;
|
|
26
39
|
truncated: boolean;
|
|
27
40
|
omittedChars?: undefined;
|
|
@@ -47,20 +60,33 @@ export declare const hazeTools: {
|
|
|
47
60
|
content: string;
|
|
48
61
|
allowIgnored: boolean;
|
|
49
62
|
}, {
|
|
63
|
+
ok: true;
|
|
64
|
+
duplicateSkipped: true;
|
|
65
|
+
toolName: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
} | {
|
|
50
68
|
ok: boolean;
|
|
51
69
|
path: string;
|
|
52
70
|
startLine: number;
|
|
53
71
|
endLine: number;
|
|
54
72
|
replacementLines: number;
|
|
73
|
+
appended: boolean;
|
|
55
74
|
}>;
|
|
56
75
|
writeFile: import("ai").Tool<{
|
|
57
76
|
path: string;
|
|
58
77
|
content: string;
|
|
78
|
+
overwriteExisting: boolean;
|
|
59
79
|
allowIgnored: boolean;
|
|
60
80
|
}, {
|
|
81
|
+
ok: true;
|
|
82
|
+
duplicateSkipped: true;
|
|
83
|
+
toolName: string;
|
|
84
|
+
reason: string;
|
|
85
|
+
} | {
|
|
61
86
|
ok: boolean;
|
|
62
87
|
path: string;
|
|
63
88
|
bytes: number;
|
|
89
|
+
overwritten: boolean;
|
|
64
90
|
}>;
|
|
65
91
|
editFile: import("ai").Tool<{
|
|
66
92
|
path: string;
|
|
@@ -70,12 +96,18 @@ export declare const hazeTools: {
|
|
|
70
96
|
}[];
|
|
71
97
|
allowIgnored: boolean;
|
|
72
98
|
}, {
|
|
99
|
+
ok: true;
|
|
100
|
+
duplicateSkipped: true;
|
|
101
|
+
toolName: string;
|
|
102
|
+
reason: string;
|
|
103
|
+
} | {
|
|
73
104
|
ok: boolean;
|
|
74
105
|
path: string;
|
|
75
106
|
edits: number;
|
|
76
107
|
}>;
|
|
77
108
|
bash: import("ai").Tool<{
|
|
78
109
|
command: string;
|
|
110
|
+
allowMutation: boolean;
|
|
79
111
|
timeoutSeconds?: number | undefined;
|
|
80
112
|
}, unknown>;
|
|
81
113
|
};
|