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.
- package/README.md +326 -59
- package/package.json +1 -1
- package/src/cli.ts +90 -81
- package/src/db/workflow-db.ts +0 -7
- package/src/expression/evaluator.test.ts +42 -0
- package/src/expression/evaluator.ts +28 -0
- package/src/parser/agent-parser.test.ts +10 -0
- package/src/parser/agent-parser.ts +2 -1
- package/src/parser/config-schema.ts +13 -5
- package/src/parser/workflow-parser.ts +0 -5
- package/src/runner/llm-adapter.test.ts +0 -8
- package/src/runner/llm-adapter.ts +33 -10
- package/src/runner/llm-executor.test.ts +59 -18
- package/src/runner/llm-executor.ts +1 -1
- package/src/runner/mcp-client.test.ts +166 -88
- package/src/runner/mcp-client.ts +156 -22
- package/src/runner/mcp-manager.test.ts +73 -15
- package/src/runner/mcp-manager.ts +44 -18
- package/src/runner/mcp-server.test.ts +4 -1
- package/src/runner/mcp-server.ts +25 -11
- package/src/runner/shell-executor.ts +3 -3
- package/src/runner/step-executor.ts +10 -9
- package/src/runner/tool-integration.test.ts +21 -14
- package/src/runner/workflow-runner.ts +25 -5
- package/src/templates/agents/explore.md +54 -0
- package/src/templates/agents/general.md +8 -0
- package/src/templates/agents/keystone-architect.md +54 -0
- package/src/templates/agents/my-agent.md +3 -0
- package/src/templates/agents/summarizer.md +28 -0
- package/src/templates/agents/test-agent.md +10 -0
- package/src/templates/approval-process.yaml +36 -0
- package/src/templates/basic-inputs.yaml +19 -0
- package/src/templates/basic-shell.yaml +20 -0
- package/src/templates/batch-processor.yaml +43 -0
- package/src/templates/cleanup-finally.yaml +22 -0
- package/src/templates/composition-child.yaml +13 -0
- package/src/templates/composition-parent.yaml +14 -0
- package/src/templates/data-pipeline.yaml +38 -0
- package/src/templates/full-feature-demo.yaml +64 -0
- package/src/templates/human-interaction.yaml +12 -0
- package/src/templates/invalid.yaml +5 -0
- package/src/templates/llm-agent.yaml +8 -0
- package/src/templates/loop-parallel.yaml +37 -0
- package/src/templates/retry-policy.yaml +36 -0
- package/src/templates/scaffold-feature.yaml +48 -0
- package/src/templates/state.db +0 -0
- package/src/templates/state.db-shm +0 -0
- package/src/templates/state.db-wal +0 -0
- package/src/templates/stop-watch.yaml +17 -0
- package/src/templates/workflow.db +0 -0
- 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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/runner/mcp-server.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
30
|
+
return new Promise<void>((resolve) => {
|
|
31
|
+
rl.on('line', async (line) => {
|
|
32
|
+
if (!line.trim()) return;
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
39
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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 ?
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
58
|
-
CopilotAdapter.prototype.chat = mockChat as
|
|
59
|
-
AnthropicAdapter.prototype.chat = mockChat as
|
|
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
|
|
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
|
|
145
|
-
CopilotAdapter.prototype.chat = mockChat as
|
|
146
|
-
AnthropicAdapter.prototype.chat = mockChat as
|
|
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
|
|
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
|
|
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
|
-
|
|
277
|
+
|
|
278
|
+
// Only exit if not embedded
|
|
279
|
+
if (!this.options.preventExit) {
|
|
280
|
+
process.exit(130);
|
|
281
|
+
}
|
|
275
282
|
};
|
|
276
283
|
|
|
277
|
-
|
|
278
|
-
|
|
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,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,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,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 }}"
|