keystone-cli 0.1.0 → 0.2.0

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 (51) hide show
  1. package/README.md +326 -59
  2. package/package.json +1 -1
  3. package/src/cli.ts +90 -81
  4. package/src/db/workflow-db.ts +0 -7
  5. package/src/expression/evaluator.test.ts +42 -0
  6. package/src/expression/evaluator.ts +28 -0
  7. package/src/parser/agent-parser.test.ts +10 -0
  8. package/src/parser/agent-parser.ts +2 -1
  9. package/src/parser/config-schema.ts +13 -5
  10. package/src/parser/workflow-parser.ts +0 -5
  11. package/src/runner/llm-adapter.test.ts +0 -8
  12. package/src/runner/llm-adapter.ts +33 -10
  13. package/src/runner/llm-executor.test.ts +59 -18
  14. package/src/runner/llm-executor.ts +1 -1
  15. package/src/runner/mcp-client.test.ts +166 -88
  16. package/src/runner/mcp-client.ts +156 -22
  17. package/src/runner/mcp-manager.test.ts +73 -15
  18. package/src/runner/mcp-manager.ts +44 -18
  19. package/src/runner/mcp-server.test.ts +4 -1
  20. package/src/runner/mcp-server.ts +25 -11
  21. package/src/runner/shell-executor.ts +3 -3
  22. package/src/runner/step-executor.ts +10 -9
  23. package/src/runner/tool-integration.test.ts +21 -14
  24. package/src/runner/workflow-runner.ts +25 -5
  25. package/src/templates/agents/explore.md +54 -0
  26. package/src/templates/agents/general.md +8 -0
  27. package/src/templates/agents/keystone-architect.md +54 -0
  28. package/src/templates/agents/my-agent.md +3 -0
  29. package/src/templates/agents/summarizer.md +28 -0
  30. package/src/templates/agents/test-agent.md +10 -0
  31. package/src/templates/approval-process.yaml +36 -0
  32. package/src/templates/basic-inputs.yaml +19 -0
  33. package/src/templates/basic-shell.yaml +20 -0
  34. package/src/templates/batch-processor.yaml +43 -0
  35. package/src/templates/cleanup-finally.yaml +22 -0
  36. package/src/templates/composition-child.yaml +13 -0
  37. package/src/templates/composition-parent.yaml +14 -0
  38. package/src/templates/data-pipeline.yaml +38 -0
  39. package/src/templates/full-feature-demo.yaml +64 -0
  40. package/src/templates/human-interaction.yaml +12 -0
  41. package/src/templates/invalid.yaml +5 -0
  42. package/src/templates/llm-agent.yaml +8 -0
  43. package/src/templates/loop-parallel.yaml +37 -0
  44. package/src/templates/retry-policy.yaml +36 -0
  45. package/src/templates/scaffold-feature.yaml +48 -0
  46. package/src/templates/state.db +0 -0
  47. package/src/templates/state.db-shm +0 -0
  48. package/src/templates/state.db-wal +0 -0
  49. package/src/templates/stop-watch.yaml +17 -0
  50. package/src/templates/workflow.db +0 -0
  51. package/src/utils/config-loader.test.ts +2 -2
@@ -1,16 +1,20 @@
1
- import { MCPClient } from './mcp-client';
2
1
  import { ConfigLoader } from '../utils/config-loader';
2
+ import { MCPClient } from './mcp-client';
3
3
  import type { Logger } from './workflow-runner';
4
4
 
5
5
  export interface MCPServerConfig {
6
6
  name: string;
7
- command: string;
7
+ type?: 'local' | 'remote';
8
+ command?: string;
8
9
  args?: string[];
9
10
  env?: Record<string, string>;
11
+ url?: string;
12
+ headers?: Record<string, string>;
10
13
  }
11
14
 
12
15
  export class MCPManager {
13
16
  private clients: Map<string, MCPClient> = new Map();
17
+ private connectionPromises: Map<string, Promise<MCPClient | undefined>> = new Map();
14
18
  private sharedServers: Map<string, MCPServerConfig> = new Map();
15
19
 
16
20
  constructor() {
@@ -23,10 +27,8 @@ export class MCPManager {
23
27
  for (const [name, server] of Object.entries(config.mcp_servers)) {
24
28
  this.sharedServers.set(name, {
25
29
  name,
26
- command: server.command,
27
- args: server.args,
28
- env: server.env,
29
- });
30
+ ...server,
31
+ } as MCPServerConfig);
30
32
  }
31
33
  }
32
34
  }
@@ -49,23 +51,47 @@ export class MCPManager {
49
51
  }
50
52
 
51
53
  const key = this.getServerKey(config);
54
+
55
+ // Check if we already have a client
52
56
  if (this.clients.has(key)) {
53
57
  return this.clients.get(key);
54
58
  }
55
59
 
56
- logger.log(` 🔌 Connecting to MCP server: ${config.name}`);
57
- const client = new MCPClient(config.command, config.args || [], config.env || {});
58
- try {
59
- await client.initialize();
60
- this.clients.set(key, client);
61
- return client;
62
- } catch (error) {
63
- logger.error(
64
- ` ✗ Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
65
- );
66
- client.stop();
67
- return undefined;
60
+ // Check if we are already connecting
61
+ if (this.connectionPromises.has(key)) {
62
+ return this.connectionPromises.get(key);
68
63
  }
64
+
65
+ // Start a new connection and cache the promise
66
+ const connectionPromise = (async () => {
67
+ logger.log(` 🔌 Connecting to MCP server: ${config.name} (${config.type || 'local'})`);
68
+
69
+ let client: MCPClient;
70
+ try {
71
+ if (config.type === 'remote') {
72
+ if (!config.url) throw new Error('Remote MCP server missing URL');
73
+ client = await MCPClient.createRemote(config.url, config.headers || {});
74
+ } else {
75
+ if (!config.command) throw new Error('Local MCP server missing command');
76
+ client = await MCPClient.createLocal(config.command, config.args || [], config.env || {});
77
+ }
78
+
79
+ await client.initialize();
80
+ this.clients.set(key, client);
81
+ return client;
82
+ } catch (error) {
83
+ logger.error(
84
+ ` ✗ Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
85
+ );
86
+ return undefined;
87
+ } finally {
88
+ // Remove promise from cache once settled
89
+ this.connectionPromises.delete(key);
90
+ }
91
+ })();
92
+
93
+ this.connectionPromises.set(key, connectionPromise);
94
+ return connectionPromise;
69
95
  }
70
96
 
71
97
  private getServerKey(config: MCPServerConfig): string {
@@ -219,7 +219,7 @@ describe('MCPServer', () => {
219
219
  const writeSpy = spyOn(process.stdout, 'write').mockImplementation(() => true);
220
220
  const consoleSpy = spyOn(console, 'error').mockImplementation(() => {});
221
221
 
222
- await server.start();
222
+ const startPromise = server.start();
223
223
 
224
224
  // Simulate stdin data
225
225
  const message = {
@@ -236,6 +236,9 @@ describe('MCPServer', () => {
236
236
  const output = JSON.parse(writeSpy.mock.calls[0][0] as string);
237
237
  expect(output.id).toBe(9);
238
238
 
239
+ process.stdin.emit('close');
240
+ await startPromise;
241
+
239
242
  writeSpy.mockRestore();
240
243
  consoleSpy.mockRestore();
241
244
  });
@@ -1,4 +1,5 @@
1
1
  import * as readline from 'node:readline';
2
+ import pkg from '../../package.json' with { type: 'json' };
2
3
  import { WorkflowDb } from '../db/workflow-db';
3
4
  import { WorkflowParser } from '../parser/workflow-parser';
4
5
  import { generateMermaidGraph } from '../utils/mermaid';
@@ -26,21 +27,32 @@ export class MCPServer {
26
27
  terminal: false,
27
28
  });
28
29
 
29
- rl.on('line', async (line) => {
30
- if (!line.trim()) return;
30
+ return new Promise<void>((resolve) => {
31
+ rl.on('line', async (line) => {
32
+ if (!line.trim()) return;
31
33
 
32
- try {
33
- const message = JSON.parse(line) as MCPMessage;
34
- const response = await this.handleMessage(message);
35
- if (response) {
36
- process.stdout.write(`${JSON.stringify(response)}\n`);
34
+ try {
35
+ const message = JSON.parse(line) as MCPMessage;
36
+ const response = await this.handleMessage(message);
37
+ if (response) {
38
+ process.stdout.write(`${JSON.stringify(response)}\n`);
39
+ }
40
+ } catch (error) {
41
+ console.error('Error handling MCP message:', error);
37
42
  }
38
- } catch (error) {
39
- console.error('Error handling MCP message:', error);
40
- }
43
+ });
44
+
45
+ rl.on('close', () => {
46
+ this.stop();
47
+ resolve();
48
+ });
41
49
  });
42
50
  }
43
51
 
52
+ stop() {
53
+ this.db.close();
54
+ }
55
+
44
56
  private async handleMessage(message: MCPMessage) {
45
57
  const { method, params, id } = message;
46
58
 
@@ -56,7 +68,7 @@ export class MCPServer {
56
68
  },
57
69
  serverInfo: {
58
70
  name: 'keystone-mcp',
59
- version: '0.1.0',
71
+ version: pkg.version,
60
72
  },
61
73
  },
62
74
  };
@@ -172,6 +184,7 @@ export class MCPServer {
172
184
  const runner = new WorkflowRunner(workflow, {
173
185
  inputs,
174
186
  logger,
187
+ preventExit: true,
175
188
  });
176
189
 
177
190
  // Note: This waits for completion. For long workflows, we might want to
@@ -337,6 +350,7 @@ export class MCPServer {
337
350
  const runner = new WorkflowRunner(workflow, {
338
351
  resumeRunId: run_id,
339
352
  logger,
353
+ preventExit: true,
340
354
  });
341
355
 
342
356
  let outputs: Record<string, unknown> | undefined;
@@ -88,7 +88,7 @@ export async function executeShell(
88
88
  logger: Logger = console
89
89
  ): Promise<ShellResult> {
90
90
  // Evaluate the command string
91
- const command = ExpressionEvaluator.evaluate(step.run, context) as string;
91
+ const command = ExpressionEvaluator.evaluateString(step.run, context);
92
92
 
93
93
  // Check for potential shell injection risks
94
94
  if (detectShellInjectionRisk(command)) {
@@ -101,12 +101,12 @@ export async function executeShell(
101
101
  const env: Record<string, string> = {};
102
102
  if (step.env) {
103
103
  for (const [key, value] of Object.entries(step.env)) {
104
- env[key] = ExpressionEvaluator.evaluate(value, context) as string;
104
+ env[key] = ExpressionEvaluator.evaluateString(value, context);
105
105
  }
106
106
  }
107
107
 
108
108
  // Set working directory if specified
109
- const cwd = step.dir ? (ExpressionEvaluator.evaluate(step.dir, context) as string) : undefined;
109
+ const cwd = step.dir ? ExpressionEvaluator.evaluateString(step.dir, context) : undefined;
110
110
 
111
111
  try {
112
112
  // Execute command using sh -c to allow shell parsing
@@ -84,11 +84,12 @@ export async function executeStep(
84
84
  // Apply transformation if specified and step succeeded
85
85
  if (step.transform && result.status === 'success') {
86
86
  const transformContext = {
87
- // Provide raw output properties (like stdout, data) directly in context
88
- // Fix: Spread output FIRST, then context to prevent shadowing
87
+ // 1. Provide raw output properties (like stdout, data) directly in context for convenience
89
88
  ...(typeof result.output === 'object' && result.output !== null ? result.output : {}),
90
- output: result.output,
89
+ // 2. Add core context (inputs, secrets, etc.). This takes priority over output properties for security.
91
90
  ...context,
91
+ // 3. Explicitly add 'output' so it refers to the current step's result even if context or output properties have a collision.
92
+ output: result.output,
92
93
  };
93
94
 
94
95
  try {
@@ -159,7 +160,7 @@ async function executeFileStep(
159
160
  context: ExpressionContext,
160
161
  _logger: Logger
161
162
  ): Promise<StepResult> {
162
- const path = ExpressionEvaluator.evaluate(step.path, context) as string;
163
+ const path = ExpressionEvaluator.evaluateString(step.path, context);
163
164
 
164
165
  switch (step.op) {
165
166
  case 'read': {
@@ -178,7 +179,7 @@ async function executeFileStep(
178
179
  if (!step.content) {
179
180
  throw new Error('Content is required for write operation');
180
181
  }
181
- const content = ExpressionEvaluator.evaluate(step.content, context) as string;
182
+ const content = ExpressionEvaluator.evaluateString(step.content, context);
182
183
  const bytes = await Bun.write(path, content);
183
184
  return {
184
185
  output: { path, bytes },
@@ -190,7 +191,7 @@ async function executeFileStep(
190
191
  if (!step.content) {
191
192
  throw new Error('Content is required for append operation');
192
193
  }
193
- const content = ExpressionEvaluator.evaluate(step.content, context) as string;
194
+ const content = ExpressionEvaluator.evaluateString(step.content, context);
194
195
 
195
196
  // Use Node.js fs for efficient append operation
196
197
  const fs = await import('node:fs/promises');
@@ -215,13 +216,13 @@ async function executeRequestStep(
215
216
  context: ExpressionContext,
216
217
  _logger: Logger
217
218
  ): Promise<StepResult> {
218
- const url = ExpressionEvaluator.evaluate(step.url, context) as string;
219
+ const url = ExpressionEvaluator.evaluateString(step.url, context);
219
220
 
220
221
  // Evaluate headers
221
222
  const headers: Record<string, string> = {};
222
223
  if (step.headers) {
223
224
  for (const [key, value] of Object.entries(step.headers)) {
224
- headers[key] = ExpressionEvaluator.evaluate(value, context) as string;
225
+ headers[key] = ExpressionEvaluator.evaluateString(value, context);
225
226
  }
226
227
  }
227
228
 
@@ -290,7 +291,7 @@ async function executeHumanStep(
290
291
  context: ExpressionContext,
291
292
  logger: Logger
292
293
  ): Promise<StepResult> {
293
- const message = ExpressionEvaluator.evaluate(step.message, context) as string;
294
+ const message = ExpressionEvaluator.evaluateString(step.message, context);
294
295
 
295
296
  // If not a TTY (e.g. MCP server), suspend execution
296
297
  if (!process.stdin.isTTY) {
@@ -1,12 +1,19 @@
1
1
  import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test';
2
- import { OpenAIAdapter, CopilotAdapter, AnthropicAdapter } from './llm-adapter';
3
- import { MCPClient } from './mcp-client';
4
- import { executeLlmStep } from './llm-executor';
5
- import type { LlmStep, Step } from '../parser/schema';
2
+ import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
6
4
  import type { ExpressionContext } from '../expression/evaluator';
5
+ import type { LlmStep, Step } from '../parser/schema';
6
+ import {
7
+ AnthropicAdapter,
8
+ CopilotAdapter,
9
+ type LLMMessage,
10
+ type LLMResponse,
11
+ type LLMTool,
12
+ OpenAIAdapter,
13
+ } from './llm-adapter';
14
+ import { executeLlmStep } from './llm-executor';
15
+ import { MCPClient, type MCPResponse } from './mcp-client';
7
16
  import type { StepResult } from './step-executor';
8
- import { join } from 'node:path';
9
- import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
10
17
 
11
18
  interface MockToolCall {
12
19
  function: {
@@ -54,16 +61,16 @@ Test system prompt`;
54
61
  };
55
62
  });
56
63
 
57
- OpenAIAdapter.prototype.chat = mockChat as any;
58
- CopilotAdapter.prototype.chat = mockChat as any;
59
- AnthropicAdapter.prototype.chat = mockChat as any;
64
+ OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
65
+ CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
66
+ AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
60
67
 
61
68
  // Use mock.module for MCPClient
62
69
  const originalInitialize = MCPClient.prototype.initialize;
63
70
  const originalListTools = MCPClient.prototype.listTools;
64
71
  const originalStop = MCPClient.prototype.stop;
65
72
 
66
- const mockInitialize = mock(async () => ({}) as any);
73
+ const mockInitialize = mock(async () => ({}) as MCPResponse);
67
74
  const mockListTools = mock(async () => [
68
75
  {
69
76
  name: 'mcp-tool',
@@ -141,16 +148,16 @@ Test system prompt`;
141
148
  };
142
149
  });
143
150
 
144
- OpenAIAdapter.prototype.chat = mockChat as any;
145
- CopilotAdapter.prototype.chat = mockChat as any;
146
- AnthropicAdapter.prototype.chat = mockChat as any;
151
+ OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
152
+ CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
153
+ AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
147
154
 
148
155
  const originalInitialize = MCPClient.prototype.initialize;
149
156
  const originalListTools = MCPClient.prototype.listTools;
150
157
  const originalCallTool = MCPClient.prototype.callTool;
151
158
  const originalStop = MCPClient.prototype.stop;
152
159
 
153
- const mockInitialize = mock(async () => ({}) as any);
160
+ const mockInitialize = mock(async () => ({}) as MCPResponse);
154
161
  const mockListTools = mock(async () => [
155
162
  {
156
163
  name: 'mcp-tool',
@@ -45,6 +45,7 @@ export interface RunOptions {
45
45
  resumeRunId?: string;
46
46
  logger?: Logger;
47
47
  mcpManager?: MCPManager;
48
+ preventExit?: boolean; // Defaults to false
48
49
  }
49
50
 
50
51
  export interface StepContext {
@@ -76,9 +77,12 @@ export class WorkflowRunner {
76
77
  private restored = false;
77
78
  private logger: Logger;
78
79
  private mcpManager: MCPManager;
80
+ private options: RunOptions;
81
+ private signalHandler?: (signal: string) => void;
79
82
 
80
83
  constructor(workflow: Workflow, options: RunOptions = {}) {
81
84
  this.workflow = workflow;
85
+ this.options = options;
82
86
  this.db = new WorkflowDb(options.dbPath);
83
87
  this.secrets = this.loadSecrets();
84
88
  this.redactor = new Redactor(this.secrets);
@@ -257,7 +261,7 @@ export class WorkflowRunner {
257
261
  * Setup signal handlers for graceful shutdown
258
262
  */
259
263
  private setupSignalHandlers(): void {
260
- const handleShutdown = async (signal: string) => {
264
+ const handler = async (signal: string) => {
261
265
  this.logger.log(`\n\n🛑 Received ${signal}. Cleaning up...`);
262
266
  try {
263
267
  await this.db.updateRunStatus(
@@ -267,15 +271,30 @@ export class WorkflowRunner {
267
271
  `Cancelled by user (${signal})`
268
272
  );
269
273
  this.logger.log('✓ Run status updated to failed');
270
- this.db.close();
271
274
  } catch (error) {
272
275
  this.logger.error('Error during cleanup:', error);
273
276
  }
274
- process.exit(130); // Standard exit code for SIGINT
277
+
278
+ // Only exit if not embedded
279
+ if (!this.options.preventExit) {
280
+ process.exit(130);
281
+ }
275
282
  };
276
283
 
277
- process.on('SIGINT', () => handleShutdown('SIGINT'));
278
- process.on('SIGTERM', () => handleShutdown('SIGTERM'));
284
+ this.signalHandler = handler;
285
+
286
+ process.on('SIGINT', handler);
287
+ process.on('SIGTERM', handler);
288
+ }
289
+
290
+ /**
291
+ * Remove signal handlers
292
+ */
293
+ private removeSignalHandlers(): void {
294
+ if (this.signalHandler) {
295
+ process.removeListener('SIGINT', this.signalHandler);
296
+ process.removeListener('SIGTERM', this.signalHandler);
297
+ }
279
298
  }
280
299
 
281
300
  /**
@@ -853,6 +872,7 @@ export class WorkflowRunner {
853
872
  await this.db.updateRunStatus(this.runId, 'failed', undefined, errorMsg);
854
873
  throw error;
855
874
  } finally {
875
+ this.removeSignalHandlers();
856
876
  await this.runFinally();
857
877
  await this.mcpManager.stopAll();
858
878
  this.db.close();
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: explore
3
+ description: Agent for exploring and understanding codebases
4
+ model: claude-sonnet-4.5
5
+ ---
6
+
7
+ # Explore Agent
8
+
9
+ You are an expert at exploring and understanding new codebases. Your role is to map out the structure, identify key components, and understand how the system works.
10
+
11
+ ## Core Competencies
12
+
13
+ ### Codebase Exploration
14
+ - Directory structure analysis
15
+ - Key file identification
16
+ - Entry point discovery
17
+ - Configuration analysis
18
+
19
+ ### Architectural Mapping
20
+ - Component identification
21
+ - Service boundaries
22
+ - Data flow mapping
23
+ - Dependency analysis
24
+
25
+ ### Pattern Recognition
26
+ - Coding conventions
27
+ - Design patterns in use
28
+ - Framework-specific idioms
29
+ - Error handling patterns
30
+
31
+ ## Exploration Process
32
+
33
+ 1. **Scan Structure** - Understand the high-level directory organization
34
+ 2. **Identify Entry Points** - Find where the application or specific features start
35
+ 3. **Trace Flows** - Follow the data or execution path for key use cases
36
+ 4. **Analyze Configuration** - Understand how the system is set up and tuned
37
+ 5. **Map Dependencies** - Identify internal and external connections
38
+
39
+ ## Output Format
40
+
41
+ ### Exploration Summary
42
+ - **Key Files & Directories**: Most important parts of the codebase
43
+ - **Architecture Overview**: How the pieces fit together
44
+ - **Notable Patterns**: Consistent ways the code is written
45
+ - **Dependencies**: Critical internal and external links
46
+ - **Concerns/Complexity**: Areas that might be difficult to work with
47
+
48
+ ## Guidelines
49
+
50
+ - Focus on the big picture first, then dive into details
51
+ - Identify both what is there and what is missing
52
+ - Look for consistency and deviations
53
+ - Provide clear references to files and directories
54
+ - Summarize findings for technical and non-technical audiences
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: general
3
+ description: "A general-purpose assistant for various tasks"
4
+ model: gpt-4o
5
+ ---
6
+
7
+ # Identity
8
+ You are a versatile and helpful assistant capable of handling a wide range of tasks, from information retrieval to analysis and formatting.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: keystone-architect
3
+ description: "Expert at designing Keystone workflows and agents"
4
+ model: gpt-4o
5
+ ---
6
+
7
+ # Role
8
+ You are the Keystone Architect. Your goal is to design and generate high-quality Keystone workflows (.yaml) and agents (.md). You understand the underlying schema and expression syntax perfectly.
9
+
10
+ # Knowledge Base
11
+
12
+ ## Workflow Schema (.yaml)
13
+ - **name**: Unique identifier for the workflow.
14
+ - **inputs**: Map of `{ type: string, default: any, description: string }` under the `inputs` key.
15
+ - **outputs**: Map of expressions (e.g., `${{ steps.id.output }}`) under the `outputs` key.
16
+ - **steps**: Array of step objects. Each step MUST have an `id` and a `type`:
17
+ - **shell**: `{ id, type: 'shell', run, dir, env, transform }`
18
+ - **llm**: `{ id, type: 'llm', agent, prompt, schema }`
19
+ - **workflow**: `{ id, type: 'workflow', path, inputs }`
20
+ - **file**: `{ id, type: 'file', path, op: 'read'|'write'|'append', content }`
21
+ - **request**: `{ id, type: 'request', url, method, body, headers }`
22
+ - **human**: `{ id, type: 'human', message, inputType: 'confirm'|'text' }`
23
+ - **sleep**: `{ id, type: 'sleep', duration }`
24
+ - **Common Step Fields**: `needs` (array of IDs), `if` (expression), `retry`, `foreach`, `concurrency`.
25
+ - **IMPORTANT**: Steps run in **parallel** by default. To ensure sequential execution, a step must explicitly list the previous step's ID in its `needs` array.
26
+
27
+ ## Agent Schema (.md)
28
+ Markdown files with YAML frontmatter:
29
+ - **name**: Agent name.
30
+ - **model**: (Optional) e.g., `gpt-4o`, `claude-sonnet-4.5`.
31
+ - **tools**: Array of `{ name, parameters, execution }` where `execution` is a standard Step object.
32
+ - **Body**: The Markdown body is the `systemPrompt`.
33
+
34
+ ## Expression Syntax
35
+ - `${{ inputs.name }}`
36
+ - `${{ steps.id.output }}`
37
+ - `${{ steps.id.status }}`
38
+ - `${{ args.paramName }}` (used inside agent tools)
39
+ - Standard JS-like expressions: `${{ steps.count > 0 ? 'yes' : 'no' }}`
40
+
41
+ # Output Instructions
42
+ When asked to design a feature:
43
+ 1. Provide the necessary Keystone files (Workflows and Agents).
44
+ 2. **IMPORTANT**: Return ONLY a raw JSON object. Do not include markdown code blocks, preamble, or postamble.
45
+
46
+ The JSON structure must be:
47
+ {
48
+ "files": [
49
+ {
50
+ "path": "workflows/...",
51
+ "content": "..."
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: my-agent
3
+ ---
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: summarizer
3
+ description: "Summarizes text content"
4
+ model: gpt-4o
5
+ tools:
6
+ - name: read_file
7
+ description: "Read the contents of a file"
8
+ parameters:
9
+ type: object
10
+ properties:
11
+ filepath:
12
+ type: string
13
+ description: "The path to the file to read"
14
+ required: ["filepath"]
15
+ execution:
16
+ type: file
17
+ op: read
18
+ path: "${{ args.filepath }}"
19
+ ---
20
+
21
+ # Identity
22
+ You are a concise summarizer. Your goal is to extract the key points from any text and present them in a clear, brief format.
23
+
24
+ ## Guidelines
25
+ - Focus on the main ideas and key takeaways
26
+ - Keep summaries under 3-5 sentences unless more detail is explicitly requested
27
+ - Use clear, simple language
28
+ - Maintain objectivity and accuracy
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: test-agent
3
+ model: gpt-4
4
+ tools:
5
+ - name: test-tool
6
+ execution:
7
+ type: shell
8
+ run: echo "tool executed with ${{ args.val }}"
9
+ ---
10
+ You are a test agent.
@@ -0,0 +1,36 @@
1
+ name: approval-process
2
+ description: "A workflow demonstrating human-in-the-loop and conditional logic"
3
+
4
+ inputs:
5
+ request_details: { type: string, default: "Access to production database" }
6
+
7
+ steps:
8
+ - id: request_approval
9
+ type: human
10
+ message: "Do you approve the following request: ${{ inputs.request_details }}?"
11
+ inputType: confirm
12
+
13
+ - id: log_approval
14
+ type: shell
15
+ if: ${{ steps.request_approval.output == true }}
16
+ run: echo "Request approved. Proceeding with implementation."
17
+ needs: [request_approval]
18
+
19
+ - id: log_rejection
20
+ type: shell
21
+ if: ${{ steps.request_approval.output == false }}
22
+ run: echo "Request rejected. Notifying requester."
23
+ needs: [request_approval]
24
+
25
+ - id: get_rejection_reason
26
+ type: human
27
+ if: ${{ steps.request_approval.output == false }}
28
+ message: "Please provide a reason for rejection:"
29
+ inputType: text
30
+ needs: [request_approval]
31
+
32
+ - id: finalize_rejection
33
+ type: shell
34
+ if: ${{ steps.request_approval.output == false }}
35
+ run: echo "Rejection reason - ${{ steps.get_rejection_reason.output }}"
36
+ needs: [get_rejection_reason]
@@ -0,0 +1,19 @@
1
+ name: basic-inputs
2
+ description: "A simple workflow that greets a user with optional repetition"
3
+ inputs:
4
+ user_name:
5
+ type: string
6
+ description: "The name of the person to greet"
7
+ default: "World"
8
+ count:
9
+ type: number
10
+ description: "Number of times to greet"
11
+ default: 1
12
+
13
+ steps:
14
+ - id: hello
15
+ type: shell
16
+ run: |
17
+ for i in $(seq 1 ${{ inputs.count }}); do
18
+ echo "Hello, ${{ escape(inputs.user_name) }}! (Attempt $i)"
19
+ done
@@ -0,0 +1,20 @@
1
+ name: basic-shell
2
+ description: "A simple example workflow demonstrating basic features"
3
+
4
+ inputs:
5
+ greeting: { type: string, default: "Hello" }
6
+ name: { type: string, default: "World" }
7
+
8
+ outputs:
9
+ message: ${{ steps.create_message.output }}
10
+
11
+ steps:
12
+ - id: create_message
13
+ type: shell
14
+ run: echo "${{ escape(inputs.greeting) }}, ${{ escape(inputs.name) }}!"
15
+ transform: "stdout.trim()"
16
+
17
+ - id: print_message
18
+ type: shell
19
+ needs: [create_message]
20
+ run: echo "Generated message - ${{ steps.create_message.output }}"