quay-ai-factory 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quay-ai-factory",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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