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 +1 -1
- package/src/server/agents/agentRunner.ts +199 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quay-ai-factory",
|
|
3
|
-
"version": "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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
sseBroadcaster.broadcast('run:progress', {
|
|
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', {
|
|
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
|
-
|
|
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:
|
|
244
|
-
}, { id:
|
|
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
|
|