keystone-cli 0.5.0 → 0.5.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keystone-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "A local-first, declarative, agentic workflow orchestrator built on Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
"lint:fix": "biome check --write .",
|
|
14
14
|
"format": "biome format --write ."
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"workflow",
|
|
18
|
+
"orchestrator",
|
|
19
|
+
"agentic",
|
|
20
|
+
"automation",
|
|
21
|
+
"bun"
|
|
22
|
+
],
|
|
17
23
|
"author": "Mark Hingston",
|
|
18
24
|
"license": "MIT",
|
|
19
25
|
"repository": {
|
|
@@ -21,7 +27,12 @@
|
|
|
21
27
|
"url": "https://github.com/mhingston/keystone-cli.git"
|
|
22
28
|
},
|
|
23
29
|
"homepage": "https://github.com/mhingston/keystone-cli#readme",
|
|
24
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"logo.png"
|
|
35
|
+
],
|
|
25
36
|
"dependencies": {
|
|
26
37
|
"@jsep-plugin/arrow": "^1.0.6",
|
|
27
38
|
"@jsep-plugin/object": "^1.2.2",
|
|
@@ -46,4 +57,4 @@
|
|
|
46
57
|
"engines": {
|
|
47
58
|
"bun": ">=1.0.0"
|
|
48
59
|
}
|
|
49
|
-
}
|
|
60
|
+
}
|
|
@@ -132,13 +132,13 @@ describe('llm-executor', () => {
|
|
|
132
132
|
beforeAll(() => {
|
|
133
133
|
// Mock spawn to avoid actual process creation
|
|
134
134
|
const mockProcess = Object.assign(new EventEmitter(), {
|
|
135
|
-
stdout: new Readable({ read() {} }),
|
|
135
|
+
stdout: new Readable({ read() { } }),
|
|
136
136
|
stdin: new Writable({
|
|
137
137
|
write(_chunk, _encoding, cb: (error?: Error | null) => void) {
|
|
138
138
|
cb();
|
|
139
139
|
},
|
|
140
140
|
}),
|
|
141
|
-
kill: mock(() => {}),
|
|
141
|
+
kill: mock(() => { }),
|
|
142
142
|
});
|
|
143
143
|
spawnSpy = spyOn(child_process, 'spawn').mockReturnValue(
|
|
144
144
|
mockProcess as unknown as child_process.ChildProcess
|
|
@@ -266,7 +266,7 @@ You are a test agent.`;
|
|
|
266
266
|
expect(result.output).toEqual({ foo: 'bar' });
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
it('should
|
|
269
|
+
it('should retry if LLM output fails schema validation', async () => {
|
|
270
270
|
const step: LlmStep = {
|
|
271
271
|
id: 'l1',
|
|
272
272
|
type: 'llm',
|
|
@@ -279,7 +279,51 @@ You are a test agent.`;
|
|
|
279
279
|
const context: ExpressionContext = { inputs: {}, steps: {} };
|
|
280
280
|
const executeStepFn = mock(async () => ({ status: 'success' as const, output: 'ok' }));
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
const originalOpenAIChatInner = OpenAIAdapter.prototype.chat;
|
|
283
|
+
const originalCopilotChatInner = CopilotAdapter.prototype.chat;
|
|
284
|
+
const originalAnthropicChatInner = AnthropicAdapter.prototype.chat;
|
|
285
|
+
|
|
286
|
+
let attempt = 0;
|
|
287
|
+
const mockChat = mock(async () => {
|
|
288
|
+
attempt++;
|
|
289
|
+
if (attempt === 1) {
|
|
290
|
+
return { message: { role: 'assistant', content: 'Not JSON' } };
|
|
291
|
+
}
|
|
292
|
+
return { message: { role: 'assistant', content: '{"success": true}' } };
|
|
293
|
+
}) as unknown as typeof originalOpenAIChat;
|
|
294
|
+
|
|
295
|
+
OpenAIAdapter.prototype.chat = mockChat;
|
|
296
|
+
CopilotAdapter.prototype.chat = mockChat;
|
|
297
|
+
AnthropicAdapter.prototype.chat = mockChat;
|
|
298
|
+
|
|
299
|
+
const result = await executeLlmStep(
|
|
300
|
+
step,
|
|
301
|
+
context,
|
|
302
|
+
executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(result.status).toBe('success');
|
|
306
|
+
expect(result.output).toEqual({ success: true });
|
|
307
|
+
expect(attempt).toBe(2);
|
|
308
|
+
|
|
309
|
+
OpenAIAdapter.prototype.chat = originalOpenAIChatInner;
|
|
310
|
+
CopilotAdapter.prototype.chat = originalCopilotChatInner;
|
|
311
|
+
AnthropicAdapter.prototype.chat = originalAnthropicChatInner;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should fail after max iterations if JSON remains invalid', async () => {
|
|
315
|
+
const step: LlmStep = {
|
|
316
|
+
id: 'l1',
|
|
317
|
+
type: 'llm',
|
|
318
|
+
agent: 'test-agent',
|
|
319
|
+
prompt: 'give me invalid json',
|
|
320
|
+
needs: [],
|
|
321
|
+
maxIterations: 3,
|
|
322
|
+
schema: { type: 'object' },
|
|
323
|
+
};
|
|
324
|
+
const context: ExpressionContext = { inputs: {}, steps: {} };
|
|
325
|
+
const executeStepFn = mock(async () => ({ status: 'success' as const, output: 'ok' }));
|
|
326
|
+
|
|
283
327
|
const originalOpenAIChatInner = OpenAIAdapter.prototype.chat;
|
|
284
328
|
const originalCopilotChatInner = CopilotAdapter.prototype.chat;
|
|
285
329
|
const originalAnthropicChatInner = AnthropicAdapter.prototype.chat;
|
|
@@ -298,7 +342,7 @@ You are a test agent.`;
|
|
|
298
342
|
context,
|
|
299
343
|
executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>
|
|
300
344
|
)
|
|
301
|
-
).rejects.toThrow(
|
|
345
|
+
).rejects.toThrow('Max ReAct iterations reached');
|
|
302
346
|
|
|
303
347
|
OpenAIAdapter.prototype.chat = originalOpenAIChatInner;
|
|
304
348
|
CopilotAdapter.prototype.chat = originalCopilotChatInner;
|
|
@@ -378,7 +422,7 @@ You are a test agent.`;
|
|
|
378
422
|
spyOn(client, 'stop').mockReturnValue(undefined);
|
|
379
423
|
return client;
|
|
380
424
|
});
|
|
381
|
-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
425
|
+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { });
|
|
382
426
|
|
|
383
427
|
await executeLlmStep(
|
|
384
428
|
step,
|
|
@@ -565,7 +609,7 @@ You are a test agent.`;
|
|
|
565
609
|
};
|
|
566
610
|
const context: ExpressionContext = { inputs: {}, steps: {} };
|
|
567
611
|
const executeStepFn = mock(async () => ({ status: 'success' as const, output: 'ok' }));
|
|
568
|
-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
612
|
+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { });
|
|
569
613
|
|
|
570
614
|
await executeLlmStep(
|
|
571
615
|
step,
|
|
@@ -249,9 +249,14 @@ export async function executeLlmStep(
|
|
|
249
249
|
try {
|
|
250
250
|
output = extractJson(output) as typeof output;
|
|
251
251
|
} catch (e) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
const errorMessage = `Failed to parse LLM output as JSON matching schema: ${e instanceof Error ? e.message : String(e)}`;
|
|
253
|
+
logger.error(` ⚠️ ${errorMessage}. Retrying...`);
|
|
254
|
+
|
|
255
|
+
messages.push({
|
|
256
|
+
role: 'user',
|
|
257
|
+
content: `Error: ${errorMessage}\n\nPlease correct your output to be valid JSON matching the schema.`,
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
|