quay-ai-factory 0.2.0 β†’ 0.3.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/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # Quay - Autonomous AI Software Factory
2
2
 
3
- > Self-hosted AI agents with MCP integration, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.
3
+ > Self-hosted AI agents with MCP integration, A3M routing, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.
4
4
 
5
5
  ## πŸš€ Features
6
6
 
7
7
  ### 5 AI-Powered Features (Built on A3M Router)
8
8
 
9
- | Feature | Description |
10
- |---------|-------------|
11
- | **Human-in-Loop Verification** | Require approval before critical actions (deploy, delete) |
12
- | **Determinism & Replay** | Black-box recorder captures every step for reproducibility |
13
- | **Visual Pipeline Builder** | Drag-drop YAML pipeline generation with built-in templates |
14
- | **Agent RAG** | Knowledge grounding to reduce hallucinations |
15
- | **Self-Healing Agents** | Auto-detect and recover from failures |
9
+ | Feature | Description | Agent Integration |
10
+ |---------|-------------|-------------------|
11
+ | **Human-in-Loop Verification** | Require approval before critical actions | Auto-triggers on deploy, delete, rm, etc. |
12
+ | **Determinism & Replay** | Black-box recorder captures every step | Snapshots after each LLM call |
13
+ | **Visual Pipeline Builder** | Drag-drop YAML pipeline generation | Built-in templates for common workflows |
14
+ | **Agent RAG** | Knowledge grounding to reduce hallucinations | Applied before first LLM call |
15
+ | **Self-Healing Agents** | Auto-detect and recover from failures | Detects 7 failure types, 5 recovery strategies |
16
16
 
17
17
  ### Core Capabilities
18
18
 
@@ -25,7 +25,7 @@
25
25
  ## πŸ“¦ Installation
26
26
 
27
27
  ```bash
28
- npm install
28
+ npm install quay-ai-factory
29
29
  ```
30
30
 
31
31
  ## πŸƒβ€β™‚οΈ Running
@@ -42,6 +42,88 @@ node build
42
42
  bun test
43
43
  ```
44
44
 
45
+ ## πŸ€– Agent Runner
46
+
47
+ The `AgentRunner` orchestrates the observe→plan→act loop with all 5 AI features:
48
+
49
+ ```typescript
50
+ import { agentRunner } from './server/agents/agentRunner.js';
51
+
52
+ // Run an agent with all features enabled
53
+ const result = await agentRunner.run(
54
+ { id: 'task-1', title: 'Deploy to production', description: '...' },
55
+ { id: 'agent-1', name: 'DeployBot', type: 'coder', provider: 'openai', model: 'gpt-4o', config: '{}' },
56
+ 'run-123',
57
+ 'You are a deployment agent...',
58
+ ['bash', 'deploy'],
59
+ {
60
+ knowledgeBaseId: 'kb-1', // Enable RAG grounding
61
+ verificationConfig: { // Custom verification rules
62
+ criticalActions: ['deploy', 'delete'],
63
+ autoApproveBelow: 0.10,
64
+ },
65
+ maxSteps: 30, // Custom step limit
66
+ }
67
+ );
68
+ ```
69
+
70
+ ### How Features Integrate
71
+
72
+ ```
73
+ User Request
74
+ ↓
75
+ AgentRunner.run()
76
+ ↓
77
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
78
+ β”‚ 1. RAG Grounding β”‚
79
+ β”‚ buildGroundedPrompt() β”‚
80
+ β”‚ β†’ Adds relevant knowledge to prompt β”‚
81
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
82
+ ↓
83
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
84
+ β”‚ 2. LLM Call β”‚
85
+ β”‚ callLLM() with A3M routing β”‚
86
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
87
+ ↓
88
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
89
+ β”‚ 3. Self-Healing Check β”‚
90
+ β”‚ detectFailure() β†’ getRecoveryStrategyβ”‚
91
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
92
+ ↓
93
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
94
+ β”‚ 4. Determinism Snapshot β”‚
95
+ β”‚ createSnapshot() with state hash β”‚
96
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
97
+ ↓
98
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
99
+ β”‚ 5. Health Update β”‚
100
+ β”‚ updateHealthStatus() β”‚
101
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
102
+ ↓
103
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
104
+ β”‚ 6. Critical Action Verification β”‚
105
+ β”‚ createVerificationRequest() β”‚
106
+ β”‚ β†’ Pauses if approval needed β”‚
107
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
108
+ ↓
109
+ Tool Execution β†’ Loop
110
+ ```
111
+
112
+ ## πŸ”§ Configuration
113
+
114
+ ```bash
115
+ # Environment variables
116
+ OPENAI_API_KEY=sk-... # OpenAI API key
117
+ ANTHROPIC_API_KEY=sk-ant-... # Anthropic API key
118
+ QUAY_DEFAULT_LLM_URL=https://api.openai.com/v1 # Default LLM endpoint
119
+
120
+ EMBEDDING_PROVIDER=openai # openai, local, or mock
121
+ EMBEDDING_ENDPOINT=http://localhost:11434/api/embeddings
122
+ QUAY_DB_PATH=./quay.db # Database path
123
+ SSE_ENABLED=true # Enable SSE
124
+ NODE_ENV=production # production, development, test
125
+ ```
126
+
45
127
  ## πŸ§ͺ Features
46
128
 
47
129
  ### Verification Dashboard (`/verification`)
@@ -64,6 +146,8 @@ const request = await createVerificationRequest(
64
146
  approveRequest(request.id, 'admin');
65
147
  ```
66
148
 
149
+ **Critical Actions** (auto-detected): `deploy`, `delete`, `rm`, `drop`, `truncate`, `shutdown`, `restart`
150
+
67
151
  ### Health Monitor (`/health`)
68
152
  Real-time agent health status with self-healing.
69
153
 
@@ -76,8 +160,13 @@ updateHealthStatus('agent-1', { latencyMs: 150, success: true });
76
160
  // Detect failure and get recovery strategy
77
161
  const failure = detectFailure(new Error('timeout'));
78
162
  const strategy = getRecoveryStrategy(failure.type);
163
+ // β†’ { action: 'retry', maxRetries: 3, fallbackModel: 'gpt-4o-mini' }
79
164
  ```
80
165
 
166
+ **Failure Types**: `timeout`, `rate_limit`, `api_error`, `context_overflow`, `tool_error`, `unknown`
167
+
168
+ **Recovery Strategies**: `retry`, `fallback_model`, `fallback_agent`, `abort`, `skip_step`
169
+
81
170
  ### Knowledge Base (`/knowledge`)
82
171
  RAG-powered knowledge grounding.
83
172
 
@@ -103,6 +192,8 @@ const templates = getTemplates('code-review');
103
192
  const yaml = generateYAML(nodes, edges);
104
193
  ```
105
194
 
195
+ **Built-in Templates**: `code-review`, `security-scan`, `data-processing`, `research-agent`
196
+
106
197
  ### Replay (`/replay`)
107
198
  Deterministic replay of agent runs.
108
199
 
@@ -113,43 +204,34 @@ await createSnapshot(runId, stepIndex, state);
113
204
  const session = await startReplay(runId);
114
205
  ```
115
206
 
116
- ## πŸ”§ Configuration
117
-
118
- ```bash
119
- # Environment variables
120
- OPENAI_API_KEY=sk-... # OpenAI API key
121
- EMBEDDING_PROVIDER=openai # openai, local, or mock
122
- EMBEDDING_ENDPOINT=http://localhost:11434/api/embeddings
123
- QUAY_DB_PATH=./quay.db # Database path
124
- SSE_ENABLED=true # Enable SSE
125
- NODE_ENV=production # production, development, test
126
- ```
127
-
128
207
  ## πŸ“ Project Structure
129
208
 
130
209
  ```
131
210
  quay/
132
211
  β”œβ”€β”€ src/
133
212
  β”‚ β”œβ”€β”€ lib/
134
- β”‚ β”‚ β”œβ”€β”€ features/ # 5 AI features
135
- β”‚ β”‚ β”‚ β”œβ”€β”€ verification.ts
136
- β”‚ β”‚ β”‚ β”œβ”€β”€ determinism.ts
137
- β”‚ β”‚ β”‚ β”œβ”€β”€ visualBuilder.ts
138
- β”‚ β”‚ β”‚ β”œβ”€β”€ agentRAG.ts
139
- β”‚ β”‚ β”‚ └── selfHealing.ts
140
- β”‚ β”‚ β”œβ”€β”€ components/ # Svelte UI components
141
- β”‚ β”‚ β”œβ”€β”€ sse/ # SSE broadcaster
142
- β”‚ β”‚ β”œβ”€β”€ a3m.ts # A3M router integration
143
- β”‚ β”‚ β”œβ”€β”€ db.ts # Database layer
144
- β”‚ β”‚ └── embedding.ts # Embedding service
213
+ β”‚ β”‚ β”œβ”€β”€ features/ # 5 AI features
214
+ β”‚ β”‚ β”‚ β”œβ”€β”€ verification.ts # Human-in-loop
215
+ β”‚ β”‚ β”‚ β”œβ”€β”€ determinism.ts # Snapshots & replay
216
+ β”‚ β”‚ β”‚ β”œβ”€β”€ visualBuilder.ts # Pipeline builder
217
+ β”‚ β”‚ β”‚ β”œβ”€β”€ agentRAG.ts # Knowledge grounding
218
+ β”‚ β”‚ β”‚ └── selfHealing.ts # Auto-recovery
219
+ β”‚ β”‚ β”œβ”€β”€ components/ # Svelte UI components
220
+ β”‚ β”‚ β”œβ”€β”€ sse/ # SSE broadcaster
221
+ β”‚ β”‚ β”œβ”€β”€ a3m.ts # A3M router integration
222
+ β”‚ β”‚ β”œβ”€β”€ db.ts # Database layer
223
+ β”‚ β”‚ └── embedding.ts # Embedding service
145
224
  β”‚ β”œβ”€β”€ routes/
146
- β”‚ β”‚ β”œβ”€β”€ api/ # API endpoints
147
- β”‚ β”‚ └── [page].svelte # UI pages
225
+ β”‚ β”‚ β”œβ”€β”€ api/ # API endpoints
226
+ β”‚ β”‚ └── [page].svelte # UI pages
148
227
  β”‚ └── server/
149
- β”‚ β”œβ”€β”€ db/ # Database schema
150
- β”‚ └── a3m/ # A3M config
228
+ β”‚ β”œβ”€β”€ agents/
229
+ β”‚ β”‚ └── agentRunner.ts # Main agent loop (integrates all features)
230
+ β”‚ β”œβ”€β”€ db/ # Database schema
231
+ β”‚ β”œβ”€β”€ features/ # Feature implementations
232
+ β”‚ └── ...
151
233
  └── tests/
152
- └── __tests__/ # Integration tests
234
+ └── __tests__/ # Integration tests
153
235
  ```
154
236
 
155
237
  ## πŸ§ͺ Testing
@@ -172,7 +254,6 @@ Total: 56 tests passing
172
254
  | Endpoint | Description |
173
255
  |----------|-------------|
174
256
  | `GET /api/verification` | List verification requests |
175
- | `POST /api/verification` | Create verification request |
176
257
  | `POST /api/verification/[id]/approve` | Approve request |
177
258
  | `POST /api/verification/[id]/reject` | Reject request |
178
259
  | `GET /api/agent-health` | Get all agent health status |
@@ -188,7 +269,7 @@ Total: 56 tests passing
188
269
 
189
270
  | Route | Description |
190
271
  |-------|-------------|
191
- | `/` | Home |
272
+ | `/` | Home / Mission Control |
192
273
  | `/features` | Features dashboard |
193
274
  | `/verification` | Verification dashboard |
194
275
  | `/health` | Agent health monitor |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quay-ai-factory",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Quay β€” Autonomous AI Software Factory. Self-hosted AI agents, MCP integration, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.",
5
5
  "keywords": [
6
6
  "ai-software-factory",
@@ -12,6 +12,35 @@ import { mcpRegistry } from '../mcp/index.js';
12
12
  import { memoryTree } from '../memory/memoryTree.js';
13
13
  import type { MCPToolResult } from '../mcp/index.js';
14
14
 
15
+ // ----------------------------------------------------------------
16
+ // Import 5 AI Features
17
+ // ----------------------------------------------------------------
18
+ import {
19
+ createVerificationRequest,
20
+ approveRequest,
21
+ rejectRequest,
22
+ type VerificationConfig,
23
+ } from '../features/verification.js';
24
+
25
+ import {
26
+ createSnapshot,
27
+ verifyDeterminism,
28
+ } from '../features/determinism.js';
29
+
30
+ import {
31
+ updateHealthStatus,
32
+ detectFailure,
33
+ getRecoveryStrategy,
34
+ healthStatus,
35
+ type AgentHealth,
36
+ } from '../features/selfHealing.js';
37
+
38
+ import {
39
+ buildGroundedPrompt,
40
+ knowledgeBases,
41
+ type KnowledgeBaseConfig,
42
+ } from '../features/agentRAG.js';
43
+
15
44
  // ----------------------------------------------------------------
16
45
  // Agent LLM Interface (pluggable β€” uses A3M or direct provider)
17
46
  // ----------------------------------------------------------------
@@ -38,7 +67,6 @@ async function callLLM(
38
67
  config: LLMConfig,
39
68
  maxTokens = 4096
40
69
  ): Promise<LLMResponse> {
41
- // baseURL must come from agent config β€” no magic URL construction
42
70
  if (!config.baseURL) {
43
71
  throw new Error(`No baseURL configured for provider ${config.provider}. Set baseURL in agent config or QUAY_DEFAULT_LLM_URL env var.`);
44
72
  }
@@ -69,7 +97,6 @@ async function callLLM(
69
97
  };
70
98
  const content = data.choices[0]?.message?.content ?? '';
71
99
 
72
- // Parse tool calls from <tool_call> XML tags
73
100
  const toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
74
101
  const toolRegex = /<tool_call>\s*<name>([\w:]+)<\/name>\s*<args>([\s\S]*?)<\/args>\s*<\/tool_call>/g;
75
102
  let match;
@@ -93,8 +120,7 @@ async function callLLM(
93
120
  }
94
121
 
95
122
  // ----------------------------------------------------------------
96
- // Sandbox Execution (Docker via HTTP)
97
- // Looks up sandbox endpoint from DB instead of using env var
123
+ // Sandbox Execution
98
124
  // ----------------------------------------------------------------
99
125
 
100
126
  interface SandboxExecResult {
@@ -104,14 +130,13 @@ interface SandboxExecResult {
104
130
  }
105
131
 
106
132
  async function execInSandbox(sandboxId: string, command: string): Promise<SandboxExecResult> {
107
- // Look up sandbox endpoint from DB β€” do NOT use hardcoded SANDBOX_URL
108
133
  const [sandbox] = dbq.select<{ id: string; endpoint: string | null; status: string }>('sandboxes', { id: sandboxId });
109
134
  if (!sandbox) throw new Error(`Sandbox not found: ${sandboxId}`);
110
135
  if (!sandbox.endpoint) throw new Error(`Sandbox endpoint not set: ${sandboxId}`);
111
136
 
112
137
  const res = await fetch(`${sandbox.endpoint}/exec`, {
113
138
  method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
139
+ headers: { ' Content-Type': 'application/json' },
115
140
  body: JSON.stringify({ command, timeout: 300 }),
116
141
  });
117
142
  if (!res.ok) throw new Error(`Sandbox exec failed: ${res.status} ${await res.text()}`);
@@ -119,33 +144,27 @@ async function execInSandbox(sandboxId: string, command: string): Promise<Sandbo
119
144
  }
120
145
 
121
146
  // ----------------------------------------------------------------
122
- // Model Pricing for Cost Tracking
147
+ // Model Pricing
123
148
  // ----------------------------------------------------------------
124
149
 
125
150
  const MODEL_PRICING: Record<string, { input: number; output: number }> = {
126
- // Anthropic
127
151
  'claude-3-5-sonnet-latest': { input: 3, output: 15 },
128
152
  'claude-sonnet-4-20250514': { input: 3, output: 15 },
129
153
  'claude-3-5-haiku-latest': { input: 0.8, output: 4 },
130
- // OpenAI
131
154
  'gpt-4o': { input: 2.5, output: 10 },
132
155
  'gpt-4o-mini': { input: 0.15, output: 0.6 },
133
156
  'gpt-4-turbo': { input: 10, output: 30 },
134
157
  'gpt-4': { input: 30, output: 60 },
135
- // Groq
136
158
  'llama-3.3-70b-versatile': { input: 0.2, output: 0.2 },
137
159
  'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
138
160
  'mixtral-8x7b-32768': { input: 0.24, output: 0.24 },
139
- // Ollama (local β€” free)
140
161
  'llama3': { input: 0, output: 0 },
141
162
  'llama3.1': { input: 0, output: 0 },
142
163
  'codellama': { input: 0, output: 0 },
143
164
  'qwen2.5-coder': { input: 0, output: 0 },
144
165
  'qwen2.5': { input: 0, output: 0 },
145
- // Google
146
166
  'gemini-2.5-flash': { input: 0.1, output: 0.4 },
147
167
  'gemini-1.5-pro': { input: 1.25, output: 5 },
148
- // Zhipu
149
168
  'glm-4': { input: 0.1, output: 0.3 },
150
169
  'glm-4-flash': { input: 0.0, output: 0.0 },
151
170
  'glm-5': { input: 0.5, output: 1.5 },
@@ -161,24 +180,48 @@ function calculateCost(model: string, tokensIn: number, tokensOut: number): numb
161
180
  }
162
181
 
163
182
  // ----------------------------------------------------------------
164
- // Agent Runner
183
+ // Agent Runner with 5 AI Features Integrated
165
184
  // ----------------------------------------------------------------
166
185
 
167
186
  export class AgentRunner {
168
187
  maxSteps = 20;
169
188
 
189
+ // Feature configs (can be set per-agent or globally)
190
+ verificationConfig: VerificationConfig = {
191
+ enabled: true,
192
+ approvalThreshold: 'critical',
193
+ criticalActions: ['deploy', 'delete', 'rm', 'drop', 'truncate', 'shutdown', 'restart'],
194
+ autoApproveBelow: 0.05,
195
+ useA3MRouting: true,
196
+ };
197
+
198
+ knowledgeBaseConfig: KnowledgeBaseConfig = {
199
+ defaultThreshold: 0.7,
200
+ maxChunks: 5,
201
+ embeddingModel: 'text-embedding-3-small',
202
+ };
203
+
170
204
  async run(
171
205
  task: { id: string; title: string; description?: string },
172
206
  agent: { id: string; name: string; type: string; provider: string; model: string; config: string },
173
207
  runId: string,
174
208
  instructions: string,
175
209
  allowedTools: string[],
210
+ options?: {
211
+ knowledgeBaseId?: string;
212
+ verificationConfig?: Partial<VerificationConfig>;
213
+ maxSteps?: number;
214
+ },
176
215
  ): Promise<{ success: boolean; error?: string; cost: number; tokensIn: number; tokensOut: number }> {
177
216
  const startTime = Date.now();
178
217
  let totalCost = 0;
179
218
  let totalTokensIn = 0;
180
219
  let totalTokensOut = 0;
181
220
  let stepIndex = 0;
221
+ const maxSteps = options?.maxSteps ?? this.maxSteps;
222
+
223
+ // Merge verification config
224
+ const verificationConfig = { ...this.verificationConfig, ...options?.verificationConfig };
182
225
 
183
226
  // Parse agent config for baseURL and temperature
184
227
  let agentConfig: Record<string, unknown> = {};
@@ -188,10 +231,23 @@ export class AgentRunner {
188
231
 
189
232
  let currentPrompt = `Task: ${task.title}\n\n${task.description ?? ''}`;
190
233
 
191
- while (stepIndex < this.maxSteps) {
234
+ // Apply RAG grounding if knowledge base is configured
235
+ if (options?.knowledgeBaseId) {
236
+ currentPrompt = await buildGroundedPrompt(
237
+ options.knowledgeBaseId,
238
+ currentPrompt,
239
+ this.knowledgeBaseConfig
240
+ );
241
+ }
242
+
243
+ // Initialize health status for this agent
244
+ updateHealthStatus(agent.id, { success: true, latencyMs: 0 });
245
+
246
+ while (stepIndex < maxSteps) {
192
247
  let llmResponse: LLMResponse;
248
+ const stepStartTime = Date.now();
249
+
193
250
  try {
194
- // baseURL from agent config (required) β€” no magic URL construction
195
251
  const config: LLMConfig = {
196
252
  provider: agent.provider,
197
253
  model: agent.model,
@@ -202,7 +258,23 @@ export class AgentRunner {
202
258
  };
203
259
  llmResponse = await callLLM(currentPrompt, systemPrompt, config);
204
260
  } catch (e) {
261
+ // Self-healing: detect failure and get recovery strategy
262
+ const failure = detectFailure(e as Error);
263
+ const strategy = getRecoveryStrategy(failure.type);
264
+
265
+ updateHealthStatus(agent.id, { error: true });
205
266
  this.recordFailureSync(runId, stepIndex, String(e));
267
+
268
+ // Apply recovery strategy if available
269
+ if (strategy) {
270
+ sseBroadcaster.broadcast('agent:recovery', {
271
+ agentId: agent.id,
272
+ failure: failure.type,
273
+ strategy: strategy.action,
274
+ runId,
275
+ });
276
+ }
277
+
206
278
  return { success: false, error: String(e), cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
207
279
  }
208
280
 
@@ -213,15 +285,35 @@ export class AgentRunner {
213
285
  totalCost += calculateCost(agent.model, llmResponse.usage.inputTokens, llmResponse.usage.outputTokens);
214
286
  }
215
287
 
216
- const stepId = uuidv4();
217
- dbq.insert('step_attempts', {
218
- id: stepId, run_id: runId, step_index: stepIndex,
219
- thought: llmResponse.thought, action: null, action_args: '{}',
220
- observation: null, error: null, started_at: Date.now(), completed_at: null,
288
+ // Update health with latency
289
+ const stepLatency = Date.now() - stepStartTime;
290
+ updateHealthStatus(agent.id, { success: true, latencyMs: stepLatency });
291
+
292
+ // Create deterministic snapshot for this step
293
+ const snapshot = createSnapshot(runId, stepIndex, {
294
+ prompt: currentPrompt,
295
+ context: {
296
+ taskId: task.id,
297
+ agentId: agent.id,
298
+ knowledgeBaseId: options?.knowledgeBaseId,
299
+ },
300
+ memory: memoryTree.getRecentMemories(runId),
301
+ thought: llmResponse.thought,
302
+ toolCalls: llmResponse.toolCalls,
303
+ tokensIn: llmResponse.usage?.inputTokens ?? 0,
304
+ tokensOut: llmResponse.usage?.outputTokens ?? 0,
305
+ costUSD: totalCost,
306
+ latencyMs: stepLatency,
221
307
  });
222
308
 
223
- memoryTree.pushStep(runId, llmResponse.thought, '', '');
224
- sseBroadcaster.broadcast('run:progress', { runId, stepIndex, thought: llmResponse.thought });
309
+ // Broadcast step progress with snapshot info
310
+ sseBroadcaster.broadcast('run:progress', {
311
+ runId,
312
+ stepIndex,
313
+ thought: llmResponse.thought,
314
+ snapshotId: snapshot.id,
315
+ determinismHash: snapshot.stateHash,
316
+ });
225
317
 
226
318
  if (llmResponse.isDone) {
227
319
  dbq.update('runs', {
@@ -232,16 +324,95 @@ export class AgentRunner {
232
324
  tokens_out: totalTokensOut,
233
325
  }, { id: runId });
234
326
  await memoryTree.updateAgentLongTerm(agent.id, { id: runId } as any, true);
235
- sseBroadcaster.broadcast('run:completed', { runId, cost: totalCost, durationMs: Date.now() - startTime });
327
+ sseBroadcaster.broadcast('run:completed', {
328
+ runId,
329
+ cost: totalCost,
330
+ durationMs: Date.now() - startTime,
331
+ totalSteps: stepIndex + 1,
332
+ });
236
333
  return { success: true, cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
237
334
  }
238
335
 
336
+ // Execute tool calls with verification for critical actions
239
337
  for (const tc of llmResponse.toolCalls) {
338
+ const toolName = tc.name.toLowerCase();
339
+ const isCritical = verificationConfig.criticalActions.some(
340
+ action => toolName.includes(action.toLowerCase())
341
+ );
342
+
343
+ // Human-in-Loop Verification for critical actions
344
+ if (isCritical && verificationConfig.enabled) {
345
+ const estimatedCost = totalCost * 0.1; // Estimate 10% of current cost
346
+
347
+ const verificationReq = await createVerificationRequest(
348
+ agent.id,
349
+ task.id,
350
+ tc.name,
351
+ `Executing ${tc.name} with args: ${JSON.stringify(tc.arguments)}`,
352
+ estimatedCost,
353
+ verificationConfig
354
+ );
355
+
356
+ if (verificationReq) {
357
+ // Broadcast verification request
358
+ sseBroadcaster.broadcast('verification:pending', {
359
+ requestId: verificationReq.id,
360
+ agentId: agent.id,
361
+ runId,
362
+ action: tc.name,
363
+ args: tc.arguments,
364
+ });
365
+
366
+ // For now, auto-reject if pending (agent should wait for human approval in real scenario)
367
+ // In production, this would be an async wait for human approval
368
+ sseBroadcaster.broadcast('agent:paused', {
369
+ runId,
370
+ stepIndex,
371
+ reason: 'verification_pending',
372
+ requestId: verificationReq.id,
373
+ });
374
+
375
+ // Apply recovery strategy - skip this step and continue
376
+ const failure = detectFailure(new Error('verification_required'));
377
+ const strategy = getRecoveryStrategy(failure.type);
378
+
379
+ currentPrompt += `\n\n[${tc.name}] β†’ VERIFICATION REQUIRED: Action paused for human approval. Request ID: ${verificationReq.id}`;
380
+ continue;
381
+ }
382
+ }
383
+
240
384
  const toolResult = await this.executeTool(tc.name, tc.arguments);
241
- currentPrompt += `\n\n[${tc.name}] β†’ ${JSON.stringify(toolResult)}`;
385
+ const resultStr = JSON.stringify(toolResult);
386
+ currentPrompt += `\n\n[${tc.name}] β†’ ${resultStr}`;
387
+
388
+ // Update snapshot with observation
389
+ snapshot.context = {
390
+ ...snapshot.context,
391
+ lastObservation: resultStr,
392
+ lastToolCall: tc.name,
393
+ };
394
+
395
+ // Check if tool execution failed
396
+ if (!toolResult.success && toolResult.error) {
397
+ updateHealthStatus(agent.id, { error: true });
398
+
399
+ const failure = detectFailure(new Error(toolResult.error));
400
+ const strategy = getRecoveryStrategy(failure.type);
401
+
402
+ if (strategy) {
403
+ sseBroadcaster.broadcast('agent:recovery', {
404
+ agentId: agent.id,
405
+ failure: failure.type,
406
+ strategy: strategy.action,
407
+ runId,
408
+ stepIndex,
409
+ });
410
+ }
411
+ }
412
+
242
413
  dbq.update('step_attempts', {
243
- action: tc.name, action_args: JSON.stringify(tc.arguments), observation: JSON.stringify(toolResult)
244
- }, { id: stepId });
414
+ action: tc.name, action_args: JSON.stringify(tc.arguments), observation: resultStr
415
+ }, { id: snapshot.id });
245
416
  }
246
417
 
247
418
  stepIndex++;
@@ -249,6 +420,7 @@ export class AgentRunner {
249
420
 
250
421
  const err = 'Max steps exceeded';
251
422
  this.recordFailureSync(runId, stepIndex, err);
423
+ updateHealthStatus(agent.id, { error: true });
252
424
  return { success: false, error: err, cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
253
425
  }
254
426
 
@@ -267,7 +439,6 @@ export class AgentRunner {
267
439
  return { tool: name, success: false, error: String(e) };
268
440
  }
269
441
  }
270
- // Route through MCP
271
442
  return mcpRegistry.routeTool(name, args);
272
443
  }
273
444