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.0",
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": ["workflow", "orchestrator", "agentic", "automation", "bun"],
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": ["src", "README.md", "LICENSE", "logo.png"],
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 throw error if JSON parsing fails for schema', async () => {
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
- // Mock response with invalid JSON
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(/Failed to parse LLM output as JSON/);
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
- throw new Error(
253
- `Failed to parse LLM output as JSON matching schema: ${e instanceof Error ? e.message : String(e)}\nOutput: ${output}`
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