sentinelayer-cli 0.8.11 → 0.9.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 +10 -5
- package/src/agents/devtestbot/config/definition.js +100 -0
- package/src/agents/devtestbot/config/system-prompt.js +92 -0
- package/src/agents/devtestbot/index.js +9 -0
- package/src/agents/devtestbot/runner.js +769 -0
- package/src/agents/devtestbot/tool.js +707 -0
- package/src/agents/jules/stream.js +2 -12
- package/src/audit/orchestrator.js +471 -114
- package/src/audit/persona-loop.js +1342 -0
- package/src/audit/registry.js +58 -2
- package/src/commands/audit.js +42 -1
- package/src/commands/legacy-args.js +32 -1
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +417 -89
- package/src/commands/swarm.js +11 -2
- package/src/cost/history.js +41 -21
- package/src/events/schema.js +27 -1
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +110 -18
- package/src/prompt/generator.js +4 -16
- package/src/review/ai-review.js +95 -6
- package/src/review/dd-report-email-client.js +148 -0
- package/src/review/investor-dd-devtestbot.js +599 -0
- package/src/review/investor-dd-orchestrator.js +135 -3
- package/src/review/omargate-cache.js +285 -0
- package/src/review/omargate-orchestrator.js +605 -4
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +189 -4
- package/src/session/coordination-guidance.js +48 -0
- package/src/session/daemon.js +3 -2
- package/src/session/listener.js +236 -0
- package/src/session/senti-naming.js +36 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/session/sync.js +23 -0
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createMultiProviderApiClient } from "../ai/client.js";
|
|
5
|
+
import { evaluateBudget } from "../cost/budget.js";
|
|
6
|
+
import { estimateTokens } from "../cost/tokenizer.js";
|
|
7
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
8
|
+
import {
|
|
9
|
+
SHARED_TOOLS,
|
|
10
|
+
SHARED_READ_ONLY_TOOLS,
|
|
11
|
+
createAgentContext,
|
|
12
|
+
createToolDispatcher,
|
|
13
|
+
BudgetExhaustedError,
|
|
14
|
+
} from "../agents/shared-tools/index.js";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_AUDIT_AGENT_TOOLS,
|
|
17
|
+
normalizeAuditAgentTools,
|
|
18
|
+
} from "./registry.js";
|
|
19
|
+
import { createBlackboard } from "../memory/blackboard.js";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MAX_TURNS = 6;
|
|
22
|
+
const HEARTBEAT_INTERVAL_TURNS = 3;
|
|
23
|
+
const DEFAULT_PERSONA_BUDGET = Object.freeze({
|
|
24
|
+
maxCostUsd: 0.75,
|
|
25
|
+
maxOutputTokens: 6000,
|
|
26
|
+
maxRuntimeMs: 300000,
|
|
27
|
+
maxToolCalls: 50,
|
|
28
|
+
warningThresholdPercent: 70,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const AUDIT_SWARM_THRESHOLDS = Object.freeze({
|
|
32
|
+
minFilesForSwarm: 15,
|
|
33
|
+
minRouteGroupsForSwarm: 3,
|
|
34
|
+
minLocForSwarm: 5000,
|
|
35
|
+
maxFilesPerScanner: 12,
|
|
36
|
+
maxConcurrentAgents: 4,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const MUTATING_TOOLS = new Set(["FileEdit"]);
|
|
40
|
+
const DEFAULT_ISOLATION_MODE = "strict";
|
|
41
|
+
|
|
42
|
+
function normalizeString(value) {
|
|
43
|
+
return String(value || "").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeSeverity(value) {
|
|
47
|
+
const severity = normalizeString(value).toUpperCase();
|
|
48
|
+
if (severity === "P0" || severity === "P1" || severity === "P2" || severity === "P3") {
|
|
49
|
+
return severity;
|
|
50
|
+
}
|
|
51
|
+
return "P3";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeNumber(value, fallback = 0) {
|
|
55
|
+
if (value === undefined || value === null || value === "") {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
const parsed = Number(value);
|
|
59
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toPosixPath(value) {
|
|
63
|
+
return normalizeString(value).replace(/\\/g, "/");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function uniqueScopeFiles(files = []) {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const normalized = [];
|
|
69
|
+
for (const item of Array.isArray(files) ? files : []) {
|
|
70
|
+
const rawPath =
|
|
71
|
+
typeof item === "string"
|
|
72
|
+
? item
|
|
73
|
+
: item?.path || item?.file || item?.relativePath || "";
|
|
74
|
+
const filePath = toPosixPath(rawPath);
|
|
75
|
+
if (!filePath || seen.has(filePath)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
seen.add(filePath);
|
|
79
|
+
const loc =
|
|
80
|
+
typeof item === "object" && item
|
|
81
|
+
? Math.max(0, Math.floor(normalizeNumber(item.loc ?? item.lines ?? item.lineCount, 0)))
|
|
82
|
+
: 0;
|
|
83
|
+
normalized.push({ path: filePath, loc });
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function filterFindingsByFiles(findings = [], files = []) {
|
|
89
|
+
const fileSet = new Set(uniqueScopeFiles(files).map((file) => file.path));
|
|
90
|
+
if (fileSet.size === 0) {
|
|
91
|
+
return Array.isArray(findings) ? findings : [];
|
|
92
|
+
}
|
|
93
|
+
return (Array.isArray(findings) ? findings : []).filter((finding) => {
|
|
94
|
+
const filePath = toPosixPath(finding?.file || finding?.path || "");
|
|
95
|
+
return !filePath || fileSet.has(filePath);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function filesFromScope(scope = {}) {
|
|
100
|
+
if (Array.isArray(scope.files)) {
|
|
101
|
+
return uniqueScopeFiles(scope.files);
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(scope.primary)) {
|
|
104
|
+
return uniqueScopeFiles(scope.primary);
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(scope.scannedRelativeFiles)) {
|
|
107
|
+
return uniqueScopeFiles(scope.scannedRelativeFiles);
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function estimateScopeLoc(scope = {}, files = []) {
|
|
113
|
+
const explicit =
|
|
114
|
+
normalizeNumber(scope.totalLoc, 0) ||
|
|
115
|
+
normalizeNumber(scope.estimatedLoc, 0) ||
|
|
116
|
+
normalizeNumber(scope.summary?.totalLoc, 0);
|
|
117
|
+
if (explicit > 0) {
|
|
118
|
+
return Math.floor(explicit);
|
|
119
|
+
}
|
|
120
|
+
return files.reduce((sum, file) => sum + (file.loc > 0 ? file.loc : 80), 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function detectRouteGroups(files = []) {
|
|
124
|
+
const routeGroups = new Set();
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const filePath = toPosixPath(file.path || file);
|
|
127
|
+
const match = filePath.match(/(?:^|\/)(?:app|pages|routes)\/([^/]+)/);
|
|
128
|
+
if (match?.[1]) {
|
|
129
|
+
routeGroups.add(match[1]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...routeGroups].sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function buildAuditPersonaFileScope({ ingest = null, scope = {} } = {}) {
|
|
136
|
+
const explicitFiles = filesFromScope(scope);
|
|
137
|
+
const ingestFiles = uniqueScopeFiles(ingest?.indexedFiles?.files || []);
|
|
138
|
+
const files = explicitFiles.length > 0 ? explicitFiles : ingestFiles;
|
|
139
|
+
const totalLoc =
|
|
140
|
+
normalizeNumber(scope.totalLoc, 0) ||
|
|
141
|
+
normalizeNumber(scope.estimatedLoc, 0) ||
|
|
142
|
+
normalizeNumber(scope.summary?.totalLoc, 0) ||
|
|
143
|
+
normalizeNumber(ingest?.summary?.totalLoc, 0) ||
|
|
144
|
+
estimateScopeLoc(scope, files);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
files,
|
|
148
|
+
scannedFiles: files.length,
|
|
149
|
+
scannedRelativeFiles: files.map((file) => file.path),
|
|
150
|
+
totalLoc: Math.floor(totalLoc),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function decideAuditPersonaSwarm({ scope = {} } = {}) {
|
|
155
|
+
const files = filesFromScope(scope);
|
|
156
|
+
const routeGroups = detectRouteGroups(files);
|
|
157
|
+
const estimatedLoc = estimateScopeLoc(scope, files);
|
|
158
|
+
const spawn =
|
|
159
|
+
files.length > AUDIT_SWARM_THRESHOLDS.minFilesForSwarm ||
|
|
160
|
+
routeGroups.length >= AUDIT_SWARM_THRESHOLDS.minRouteGroupsForSwarm ||
|
|
161
|
+
estimatedLoc > AUDIT_SWARM_THRESHOLDS.minLocForSwarm;
|
|
162
|
+
|
|
163
|
+
let reason = "below all thresholds";
|
|
164
|
+
if (files.length > AUDIT_SWARM_THRESHOLDS.minFilesForSwarm) {
|
|
165
|
+
reason = `${files.length} files exceeds threshold (${AUDIT_SWARM_THRESHOLDS.minFilesForSwarm})`;
|
|
166
|
+
} else if (routeGroups.length >= AUDIT_SWARM_THRESHOLDS.minRouteGroupsForSwarm) {
|
|
167
|
+
reason = `${routeGroups.length} route groups exceeds threshold (${AUDIT_SWARM_THRESHOLDS.minRouteGroupsForSwarm})`;
|
|
168
|
+
} else if (estimatedLoc > AUDIT_SWARM_THRESHOLDS.minLocForSwarm) {
|
|
169
|
+
reason = `${estimatedLoc} LOC exceeds threshold (${AUDIT_SWARM_THRESHOLDS.minLocForSwarm})`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
spawn,
|
|
174
|
+
fileCount: files.length,
|
|
175
|
+
routeGroups: routeGroups.length,
|
|
176
|
+
routeGroupNames: routeGroups,
|
|
177
|
+
estimatedLoc,
|
|
178
|
+
reason,
|
|
179
|
+
thresholds: { ...AUDIT_SWARM_THRESHOLDS },
|
|
180
|
+
maxConcurrent: AUDIT_SWARM_THRESHOLDS.maxConcurrentAgents,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function partitionAuditPersonaFiles(
|
|
185
|
+
files = [],
|
|
186
|
+
maxPerPartition = AUDIT_SWARM_THRESHOLDS.maxFilesPerScanner
|
|
187
|
+
) {
|
|
188
|
+
const normalizedFiles = uniqueScopeFiles(files);
|
|
189
|
+
const partitionSize = Math.max(
|
|
190
|
+
1,
|
|
191
|
+
Math.floor(normalizeNumber(maxPerPartition, AUDIT_SWARM_THRESHOLDS.maxFilesPerScanner))
|
|
192
|
+
);
|
|
193
|
+
const partitions = [];
|
|
194
|
+
for (let index = 0; index < normalizedFiles.length; index += partitionSize) {
|
|
195
|
+
partitions.push(normalizedFiles.slice(index, index + partitionSize));
|
|
196
|
+
}
|
|
197
|
+
return partitions;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function divideAuditSwarmBudget(budget = {}, subagentCount = 1) {
|
|
201
|
+
const count = Math.max(1, Math.floor(normalizeNumber(subagentCount, 1)));
|
|
202
|
+
const parentBudget = { ...DEFAULT_PERSONA_BUDGET, ...(budget || {}) };
|
|
203
|
+
return {
|
|
204
|
+
maxCostUsd: Math.max(0, normalizeNumber(parentBudget.maxCostUsd, DEFAULT_PERSONA_BUDGET.maxCostUsd)) / count,
|
|
205
|
+
maxOutputTokens: Math.max(
|
|
206
|
+
1,
|
|
207
|
+
Math.floor(normalizeNumber(parentBudget.maxOutputTokens, DEFAULT_PERSONA_BUDGET.maxOutputTokens) / count)
|
|
208
|
+
),
|
|
209
|
+
maxRuntimeMs: Math.max(1, Math.floor(normalizeNumber(parentBudget.maxRuntimeMs, DEFAULT_PERSONA_BUDGET.maxRuntimeMs))),
|
|
210
|
+
maxToolCalls: Math.max(
|
|
211
|
+
1,
|
|
212
|
+
Math.floor(normalizeNumber(parentBudget.maxToolCalls, DEFAULT_PERSONA_BUDGET.maxToolCalls) / count)
|
|
213
|
+
),
|
|
214
|
+
warningThresholdPercent: parentBudget.warningThresholdPercent,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runWithConcurrency(items, maxConcurrent, fn) {
|
|
219
|
+
const results = [];
|
|
220
|
+
const executing = new Set();
|
|
221
|
+
|
|
222
|
+
for (const item of items) {
|
|
223
|
+
const p = Promise.resolve().then(() => fn(item)).finally(() => {
|
|
224
|
+
executing.delete(p);
|
|
225
|
+
});
|
|
226
|
+
executing.add(p);
|
|
227
|
+
results.push(p);
|
|
228
|
+
|
|
229
|
+
if (executing.size >= maxConcurrent) {
|
|
230
|
+
await Promise.race(executing);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Promise.all(results);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function severitySummary(findings = []) {
|
|
238
|
+
const summary = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
239
|
+
for (const finding of findings || []) {
|
|
240
|
+
const severity = normalizeSeverity(finding.severity);
|
|
241
|
+
summary[severity] += 1;
|
|
242
|
+
}
|
|
243
|
+
summary.blocking = summary.P0 > 0 || summary.P1 > 0;
|
|
244
|
+
summary.findingCount = findings.length;
|
|
245
|
+
return summary;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeFinding(finding = {}, agent = {}, source = "persona-agentic-loop") {
|
|
249
|
+
const title = normalizeString(finding.title || finding.message || finding.ruleId || "Audit finding");
|
|
250
|
+
const message = normalizeString(finding.message || finding.title || title);
|
|
251
|
+
return {
|
|
252
|
+
...finding,
|
|
253
|
+
severity: normalizeSeverity(finding.severity),
|
|
254
|
+
file: toPosixPath(finding.file || finding.path || ""),
|
|
255
|
+
line: Math.max(0, Math.floor(normalizeNumber(finding.line, 0))),
|
|
256
|
+
title,
|
|
257
|
+
message,
|
|
258
|
+
confidence: Math.max(0, Math.min(1, normalizeNumber(finding.confidence, agent.confidenceFloor || 0.7))),
|
|
259
|
+
persona: normalizeString(finding.persona || agent.id),
|
|
260
|
+
source: normalizeString(finding.source || source),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function snapshotUsage(ctx) {
|
|
265
|
+
return {
|
|
266
|
+
costUsd: ctx.usage.costUsd,
|
|
267
|
+
outputTokens: ctx.usage.outputTokens,
|
|
268
|
+
toolCalls: ctx.usage.toolCalls,
|
|
269
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
270
|
+
filesRead: [...(ctx.usage.filesRead || [])],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildAgentIdentity(agent = {}) {
|
|
275
|
+
return {
|
|
276
|
+
id: normalizeString(agent.id) || "audit-persona",
|
|
277
|
+
persona: normalizeString(agent.persona || agent.id || "Audit Persona"),
|
|
278
|
+
domain: normalizeString(agent.domain),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildSubagentPersona(agent = {}, subagentIndex = 1) {
|
|
283
|
+
const normalizedAgent = normalizeAuditPersona(agent || {});
|
|
284
|
+
return {
|
|
285
|
+
...normalizedAgent,
|
|
286
|
+
id: `${normalizedAgent.id}-subagent-${subagentIndex}`,
|
|
287
|
+
persona: `${normalizedAgent.persona} Subagent ${subagentIndex}`,
|
|
288
|
+
parentId: normalizedAgent.id,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolveGrantedTools(agent = {}) {
|
|
293
|
+
const grants = normalizeAuditAgentTools(agent.tools, { useDefaultWhenEmpty: true });
|
|
294
|
+
return grants.length > 0 ? grants : [...DEFAULT_AUDIT_AGENT_TOOLS];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resolveAvailableTools(agent = {}) {
|
|
298
|
+
const permissionMode = normalizeString(agent.permissionMode || "plan").toLowerCase();
|
|
299
|
+
const granted = resolveGrantedTools(agent);
|
|
300
|
+
return granted.filter((tool) => {
|
|
301
|
+
if (!SHARED_TOOLS[tool]) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
if (permissionMode === "plan" && MUTATING_TOOLS.has(tool)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function createAuditToolDispatcher(availableTools = []) {
|
|
312
|
+
const toolMap = {};
|
|
313
|
+
const readOnlyTools = new Set();
|
|
314
|
+
for (const tool of availableTools) {
|
|
315
|
+
if (!SHARED_TOOLS[tool]) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
toolMap[tool] = SHARED_TOOLS[tool];
|
|
319
|
+
if (SHARED_READ_ONLY_TOOLS.has(tool)) {
|
|
320
|
+
readOnlyTools.add(tool);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return createToolDispatcher(toolMap, readOnlyTools);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizePathWithinRoot(value, rootPath) {
|
|
327
|
+
const raw = normalizeString(value);
|
|
328
|
+
if (!raw || raw === ".") {
|
|
329
|
+
return rootPath;
|
|
330
|
+
}
|
|
331
|
+
return path.isAbsolute(raw) ? raw : path.resolve(rootPath, raw);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function normalizeToolInput(toolName, input = {}, rootPath) {
|
|
335
|
+
const normalized = { ...(input || {}) };
|
|
336
|
+
if (toolName === "FileRead") {
|
|
337
|
+
normalized.file_path = normalizePathWithinRoot(
|
|
338
|
+
normalized.file_path || normalized.filePath || normalized.path,
|
|
339
|
+
rootPath
|
|
340
|
+
);
|
|
341
|
+
normalized.allowed_root = rootPath;
|
|
342
|
+
} else if (toolName === "FileEdit") {
|
|
343
|
+
normalized.file_path = normalizePathWithinRoot(
|
|
344
|
+
normalized.file_path || normalized.filePath || normalized.path,
|
|
345
|
+
rootPath
|
|
346
|
+
);
|
|
347
|
+
normalized.allowed_root = rootPath;
|
|
348
|
+
} else if (toolName === "Grep" || toolName === "Glob") {
|
|
349
|
+
normalized.path = normalizePathWithinRoot(normalized.path || ".", rootPath);
|
|
350
|
+
} else if (toolName === "Shell") {
|
|
351
|
+
normalized.cwd = normalizePathWithinRoot(normalized.cwd || ".", rootPath);
|
|
352
|
+
}
|
|
353
|
+
return normalized;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function normalizeIsolationMode(value) {
|
|
357
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
358
|
+
return normalized === "relaxed" ? "relaxed" : DEFAULT_ISOLATION_MODE;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeAuditPersona(agent = {}) {
|
|
362
|
+
return {
|
|
363
|
+
id: normalizeString(agent?.id) || "audit-persona",
|
|
364
|
+
persona: normalizeString(agent?.persona || agent?.id || "Audit Persona"),
|
|
365
|
+
domain: normalizeString(agent?.domain || "Audit"),
|
|
366
|
+
permissionMode: normalizeString(agent?.permissionMode || "plan") || "plan",
|
|
367
|
+
maxTurns: Math.max(1, Math.floor(normalizeNumber(agent?.maxTurns, DEFAULT_MAX_TURNS))),
|
|
368
|
+
confidenceFloor: Math.max(0, Math.min(1, normalizeNumber(agent?.confidenceFloor, 0.7))),
|
|
369
|
+
tools: resolveGrantedTools(agent || {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function createIsolatedPersonaContext({
|
|
374
|
+
agent,
|
|
375
|
+
runId = "",
|
|
376
|
+
rootPath = ".",
|
|
377
|
+
artifactDir = "",
|
|
378
|
+
provider = null,
|
|
379
|
+
budget = {},
|
|
380
|
+
onEvent = null,
|
|
381
|
+
clientFactory = null,
|
|
382
|
+
env = process.env,
|
|
383
|
+
isolation = DEFAULT_ISOLATION_MODE,
|
|
384
|
+
eventContext = null,
|
|
385
|
+
} = {}) {
|
|
386
|
+
const normalizedAgent = normalizeAuditPersona(agent || {});
|
|
387
|
+
const resolvedRootPath = path.resolve(String(rootPath || "."));
|
|
388
|
+
const resolvedRunId =
|
|
389
|
+
normalizeString(runId) ||
|
|
390
|
+
`audit-persona-${normalizedAgent.id}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
391
|
+
const isolationMode = normalizeIsolationMode(isolation);
|
|
392
|
+
const grantedTools = resolveGrantedTools(normalizedAgent);
|
|
393
|
+
const availableTools = resolveAvailableTools(normalizedAgent);
|
|
394
|
+
const agentIdentity = buildAgentIdentity(normalizedAgent);
|
|
395
|
+
const loopBudget = { ...DEFAULT_PERSONA_BUDGET, ...budget };
|
|
396
|
+
const ctx = createAgentContext({
|
|
397
|
+
agentIdentity,
|
|
398
|
+
budget: loopBudget,
|
|
399
|
+
runId: resolvedRunId,
|
|
400
|
+
artifactDir,
|
|
401
|
+
onEvent,
|
|
402
|
+
});
|
|
403
|
+
const emitter = (event, payload = {}) => {
|
|
404
|
+
const evt = createAgentEvent({
|
|
405
|
+
event,
|
|
406
|
+
agent: agentIdentity,
|
|
407
|
+
payload: { ...(eventContext || {}), ...payload },
|
|
408
|
+
usage: snapshotUsage(ctx),
|
|
409
|
+
sessionId: ctx.sessionId,
|
|
410
|
+
runId: resolvedRunId,
|
|
411
|
+
});
|
|
412
|
+
if (onEvent) {
|
|
413
|
+
onEvent(evt);
|
|
414
|
+
}
|
|
415
|
+
return evt;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
agent: normalizedAgent,
|
|
420
|
+
runId: resolvedRunId,
|
|
421
|
+
rootPath: resolvedRootPath,
|
|
422
|
+
isolation: isolationMode,
|
|
423
|
+
client: resolveClient({ clientFactory, provider, env }),
|
|
424
|
+
blackboard: createBlackboard({
|
|
425
|
+
runId: resolvedRunId,
|
|
426
|
+
scope: `audit-persona:${normalizedAgent.id}`,
|
|
427
|
+
}),
|
|
428
|
+
emitter,
|
|
429
|
+
ctx,
|
|
430
|
+
agentIdentity,
|
|
431
|
+
messageHistory: [],
|
|
432
|
+
tools: {
|
|
433
|
+
grantedTools,
|
|
434
|
+
availableTools,
|
|
435
|
+
dispatcher: createAuditToolDispatcher(availableTools),
|
|
436
|
+
},
|
|
437
|
+
budget: loopBudget,
|
|
438
|
+
createdAt: new Date().toISOString(),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildPersonaSystemPrompt({
|
|
443
|
+
agent,
|
|
444
|
+
rootPath,
|
|
445
|
+
ingest,
|
|
446
|
+
grantedTools,
|
|
447
|
+
availableTools,
|
|
448
|
+
sharedContext,
|
|
449
|
+
hybridContext,
|
|
450
|
+
assignedFiles = [],
|
|
451
|
+
}) {
|
|
452
|
+
const summary = ingest?.summary || {};
|
|
453
|
+
const frameworks = Array.isArray(ingest?.frameworks) ? ingest.frameworks.join(", ") : "";
|
|
454
|
+
const riskSurfaces = Array.isArray(ingest?.riskSurfaces)
|
|
455
|
+
? ingest.riskSurfaces.map((item) => item.surface || item).filter(Boolean).join(", ")
|
|
456
|
+
: "";
|
|
457
|
+
const sharedCount = Array.isArray(sharedContext?.entries) ? sharedContext.entries.length : 0;
|
|
458
|
+
const hybridCount = Array.isArray(hybridContext?.results) ? hybridContext.results.length : 0;
|
|
459
|
+
const assignedFileLines = uniqueScopeFiles(assignedFiles)
|
|
460
|
+
.slice(0, 24)
|
|
461
|
+
.map((file) => `- ${file.path}${file.loc ? ` (${file.loc} LOC)` : ""}`)
|
|
462
|
+
.join("\n");
|
|
463
|
+
|
|
464
|
+
return `SYSTEM PROMPT - SENTINELAYER AUDIT PERSONA
|
|
465
|
+
${agent.persona} | ${agent.domain} | ${agent.id}
|
|
466
|
+
|
|
467
|
+
ROLE
|
|
468
|
+
You are the ${agent.domain} specialist for SentinelLayer's investor due-diligence audit.
|
|
469
|
+
You are isolated from other personas. Build your own evidence before trusting routed baseline findings.
|
|
470
|
+
|
|
471
|
+
CODEBASE CONTEXT
|
|
472
|
+
Root: ${rootPath}
|
|
473
|
+
Files scanned: ${summary.filesScanned || "unknown"}
|
|
474
|
+
Total LOC: ${summary.totalLoc || "unknown"}
|
|
475
|
+
Frameworks: ${frameworks || "unknown"}
|
|
476
|
+
Risk surfaces: ${riskSurfaces || "none"}
|
|
477
|
+
Shared context entries: ${sharedCount}
|
|
478
|
+
Hybrid memory entries: ${hybridCount}
|
|
479
|
+
Assigned file partition:
|
|
480
|
+
${assignedFileLines || "- full persona scope"}
|
|
481
|
+
|
|
482
|
+
AVAILABLE TOOLS
|
|
483
|
+
Granted: ${grantedTools.join(", ") || "none"}
|
|
484
|
+
Usable in this run: ${availableTools.join(", ") || "none"}
|
|
485
|
+
To call a tool, output exactly one fenced block:
|
|
486
|
+
\`\`\`tool_use
|
|
487
|
+
{"tool":"Grep","input":{"pattern":"credential|token|secret","path":"."}}
|
|
488
|
+
\`\`\`
|
|
489
|
+
|
|
490
|
+
EVIDENCE CONTRACT - 11 LENSES
|
|
491
|
+
1. Security, auth, secrets, and trust boundaries
|
|
492
|
+
2. Architecture, module boundaries, dependency direction, and coupling
|
|
493
|
+
3. Data layer correctness, migrations, queries, and persistence guarantees
|
|
494
|
+
4. Release engineering, CI/CD gates, provenance, rollback, and versioning
|
|
495
|
+
5. Infrastructure, environment, IaC, and blast radius
|
|
496
|
+
6. Reliability, timeout, retry, idempotency, and failure-mode behavior
|
|
497
|
+
7. Observability, logs, metrics, traces, and alertability
|
|
498
|
+
8. Testing, coverage, fixture quality, and regression proof
|
|
499
|
+
9. Performance, runtime cost, N+1 paths, and bottlenecks
|
|
500
|
+
10. Compliance, privacy, policy, retention, and audit evidence
|
|
501
|
+
11. Documentation, operator guidance, AI governance, and maintainability
|
|
502
|
+
|
|
503
|
+
RULES
|
|
504
|
+
- Use tools before reporting a finding unless the finding is already in supplied evidence.
|
|
505
|
+
- Every confirmed finding needs file, line, severity, user/system impact, recommended fix, and confidence.
|
|
506
|
+
- Report only high-confidence findings. Below ${agent.confidenceFloor || 0.7} is an evidence gap, not a confirmed issue.
|
|
507
|
+
- In plan mode, do not mutate files. If FileEdit is not usable, propose the fix without editing.
|
|
508
|
+
- Prefer a small number of concrete findings over noisy speculation.
|
|
509
|
+
|
|
510
|
+
OUTPUT CONTRACT
|
|
511
|
+
When you need more evidence, call one tool.
|
|
512
|
+
When complete, return a JSON array in a fenced \`\`\`json block:
|
|
513
|
+
[{
|
|
514
|
+
"severity": "P1",
|
|
515
|
+
"file": "src/example.js",
|
|
516
|
+
"line": 42,
|
|
517
|
+
"title": "Concrete issue title",
|
|
518
|
+
"message": "Concrete issue title",
|
|
519
|
+
"evidence": "File:line proof or command output summary",
|
|
520
|
+
"rootCause": "Why it happens",
|
|
521
|
+
"recommendedFix": "Smallest safe fix",
|
|
522
|
+
"user_impact": "What users or operators experience",
|
|
523
|
+
"confidence": 0.9
|
|
524
|
+
}]`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function buildInitialUserPrompt({
|
|
528
|
+
agent,
|
|
529
|
+
deterministicBaseline,
|
|
530
|
+
seedFindings,
|
|
531
|
+
sharedContext,
|
|
532
|
+
hybridContext,
|
|
533
|
+
assignedFiles = [],
|
|
534
|
+
}) {
|
|
535
|
+
const parts = [];
|
|
536
|
+
parts.push(`Run an isolated ${agent.domain} audit pass now.`);
|
|
537
|
+
parts.push("Start by inspecting the code with tools. Do not simply restate routed baseline findings.");
|
|
538
|
+
const scopeFiles = uniqueScopeFiles(assignedFiles);
|
|
539
|
+
if (scopeFiles.length > 0) {
|
|
540
|
+
parts.push(`\nYou are sub-scoped to ${scopeFiles.length} file(s). Focus this pass on:`);
|
|
541
|
+
for (const file of scopeFiles.slice(0, 24)) {
|
|
542
|
+
parts.push(`- ${file.path}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const baselineFindings = Array.isArray(deterministicBaseline?.findings)
|
|
547
|
+
? deterministicBaseline.findings
|
|
548
|
+
: [];
|
|
549
|
+
if (baselineFindings.length > 0) {
|
|
550
|
+
parts.push(`\nOmar deterministic baseline has ${baselineFindings.length} total findings. You may use it only after forming evidence.`);
|
|
551
|
+
for (const finding of baselineFindings.slice(0, 12)) {
|
|
552
|
+
parts.push(`- [${finding.severity || "P3"}] ${finding.file || ""}:${finding.line || ""} ${finding.message || finding.title || ""}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (seedFindings.length > 0) {
|
|
557
|
+
parts.push(`\nRouted/legacy seed findings for your domain (${seedFindings.length}):`);
|
|
558
|
+
for (const finding of seedFindings.slice(0, 12)) {
|
|
559
|
+
parts.push(`- [${finding.severity || "P3"}] ${finding.file || ""}:${finding.line || ""} ${finding.message || finding.title || ""}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const sharedEntries = Array.isArray(sharedContext?.entries) ? sharedContext.entries : [];
|
|
564
|
+
if (sharedEntries.length > 0) {
|
|
565
|
+
parts.push(`\nShared blackboard context (${sharedEntries.length}):`);
|
|
566
|
+
for (const entry of sharedEntries.slice(0, 8)) {
|
|
567
|
+
parts.push(`- [${entry.severity || "P3"}] ${entry.file || ""}:${entry.line || ""} ${entry.message || ""}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const hybridResults = Array.isArray(hybridContext?.results) ? hybridContext.results : [];
|
|
572
|
+
if (hybridResults.length > 0) {
|
|
573
|
+
parts.push(`\nMemory recall (${hybridResults.length}):`);
|
|
574
|
+
for (const entry of hybridResults.slice(0, 8)) {
|
|
575
|
+
parts.push(`- ${entry.snippet || entry.text || entry.documentId || ""}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
parts.push("\nFirst action: call a relevant read/search tool. If enough evidence already exists, return the final JSON findings.");
|
|
580
|
+
return parts.join("\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parseToolUseBlocks(text) {
|
|
584
|
+
const calls = [];
|
|
585
|
+
const regex = /```tool_use\s*\n([\s\S]*?)```/g;
|
|
586
|
+
let match;
|
|
587
|
+
while ((match = regex.exec(String(text || ""))) !== null) {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(match[1].trim());
|
|
590
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
591
|
+
for (const entry of entries) {
|
|
592
|
+
const tool = normalizeAuditAgentTools([entry?.tool || entry?.name])[0] || "";
|
|
593
|
+
if (tool) {
|
|
594
|
+
calls.push({
|
|
595
|
+
tool,
|
|
596
|
+
input: entry?.input && typeof entry.input === "object" ? entry.input : {},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
// Malformed tool blocks are ignored; the next model turn can recover.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return calls;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function parseJsonFindings(text) {
|
|
608
|
+
const raw = String(text || "");
|
|
609
|
+
const fenced = raw.match(/```json\s*\n([\s\S]*?)```/i);
|
|
610
|
+
const candidate = fenced ? fenced[1].trim() : raw.trim();
|
|
611
|
+
if (!candidate) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const parsed = JSON.parse(candidate);
|
|
616
|
+
if (Array.isArray(parsed)) {
|
|
617
|
+
return parsed;
|
|
618
|
+
}
|
|
619
|
+
if (Array.isArray(parsed?.findings)) {
|
|
620
|
+
return parsed.findings;
|
|
621
|
+
}
|
|
622
|
+
} catch {
|
|
623
|
+
// No valid findings payload.
|
|
624
|
+
}
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function formatPromptForClient(systemPrompt, messages) {
|
|
629
|
+
const parts = [systemPrompt];
|
|
630
|
+
for (const message of messages) {
|
|
631
|
+
const role = message.role === "assistant" ? "ASSISTANT" : "USER";
|
|
632
|
+
parts.push(`\n${role}:\n${message.content}`);
|
|
633
|
+
}
|
|
634
|
+
return parts.join("\n");
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function createDeterministicPersonaClient() {
|
|
638
|
+
let callCount = 0;
|
|
639
|
+
return {
|
|
640
|
+
async invoke() {
|
|
641
|
+
callCount += 1;
|
|
642
|
+
if (callCount === 1) {
|
|
643
|
+
return {
|
|
644
|
+
provider: "local",
|
|
645
|
+
model: "deterministic-persona-test",
|
|
646
|
+
text: [
|
|
647
|
+
"I will inspect the repository file set before concluding.",
|
|
648
|
+
"```tool_use",
|
|
649
|
+
"{\"tool\":\"Glob\",\"input\":{\"pattern\":\"**/*.{js,ts,tsx,jsx,json,yml,yaml,md}\",\"path\":\".\",\"limit\":50}}",
|
|
650
|
+
"```",
|
|
651
|
+
].join("\n"),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
provider: "local",
|
|
656
|
+
model: "deterministic-persona-test",
|
|
657
|
+
text: "No additional high-confidence findings beyond the supplied evidence.\n```json\n[]\n```",
|
|
658
|
+
};
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function resolveClient({ clientFactory, provider, env }) {
|
|
664
|
+
if (typeof clientFactory === "function") {
|
|
665
|
+
return clientFactory();
|
|
666
|
+
}
|
|
667
|
+
if (env?.SENTINELAYER_CLI_TEST_MODE === "1" && env?.SENTINELAYER_CLI_LIVE_AI_TESTS !== "1") {
|
|
668
|
+
return createDeterministicPersonaClient();
|
|
669
|
+
}
|
|
670
|
+
return createMultiProviderApiClient(provider || {});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function estimateResponseUsage(response, responseText) {
|
|
674
|
+
const usage = response?.usage || {};
|
|
675
|
+
const outputTokens = Math.max(
|
|
676
|
+
0,
|
|
677
|
+
Math.floor(normalizeNumber(usage.outputTokens ?? usage.output_tokens, estimateTokens(responseText, {
|
|
678
|
+
provider: response?.provider,
|
|
679
|
+
model: response?.model,
|
|
680
|
+
})))
|
|
681
|
+
);
|
|
682
|
+
const costUsd = Math.max(0, normalizeNumber(usage.costUsd ?? usage.cost_usd, (outputTokens / 1_000_000) * 15));
|
|
683
|
+
return { outputTokens, costUsd };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function computeConfidence(agent, findings = []) {
|
|
687
|
+
if (!findings.length) {
|
|
688
|
+
return Math.min(0.99, Math.max(0, normalizeNumber(agent.confidenceFloor, 0.7)) + 0.05);
|
|
689
|
+
}
|
|
690
|
+
const total = findings.reduce((sum, finding) => sum + normalizeNumber(finding.confidence, agent.confidenceFloor || 0.7), 0);
|
|
691
|
+
return Math.max(0, Math.min(1, total / findings.length));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function runSinglePersonaAgenticLoop({
|
|
695
|
+
agent,
|
|
696
|
+
rootPath,
|
|
697
|
+
ingest = null,
|
|
698
|
+
deterministicBaseline = null,
|
|
699
|
+
seedFindings = [],
|
|
700
|
+
sharedContext = null,
|
|
701
|
+
hybridContext = null,
|
|
702
|
+
artifactDir = "",
|
|
703
|
+
provider = null,
|
|
704
|
+
budget = {},
|
|
705
|
+
maxTurns = null,
|
|
706
|
+
abortController = null,
|
|
707
|
+
onEvent = null,
|
|
708
|
+
clientFactory = null,
|
|
709
|
+
env = process.env,
|
|
710
|
+
dryRun = false,
|
|
711
|
+
isolation = DEFAULT_ISOLATION_MODE,
|
|
712
|
+
runId: requestedRunId = "",
|
|
713
|
+
eventContext = null,
|
|
714
|
+
assignedFiles = [],
|
|
715
|
+
} = {}) {
|
|
716
|
+
const resolvedRootPath = path.resolve(String(rootPath || "."));
|
|
717
|
+
const normalizedAgent = normalizeAuditPersona(agent || {});
|
|
718
|
+
const startedAt = Date.now();
|
|
719
|
+
const isolatedContext = createIsolatedPersonaContext({
|
|
720
|
+
agent: normalizedAgent,
|
|
721
|
+
runId: requestedRunId,
|
|
722
|
+
rootPath: resolvedRootPath,
|
|
723
|
+
artifactDir,
|
|
724
|
+
provider,
|
|
725
|
+
budget,
|
|
726
|
+
onEvent,
|
|
727
|
+
clientFactory,
|
|
728
|
+
env,
|
|
729
|
+
isolation,
|
|
730
|
+
eventContext,
|
|
731
|
+
});
|
|
732
|
+
const {
|
|
733
|
+
runId,
|
|
734
|
+
client,
|
|
735
|
+
ctx,
|
|
736
|
+
emitter: emit,
|
|
737
|
+
messageHistory: messages,
|
|
738
|
+
tools,
|
|
739
|
+
budget: resolvedBudget,
|
|
740
|
+
isolation: isolationMode,
|
|
741
|
+
} = isolatedContext;
|
|
742
|
+
const grantedTools = tools.grantedTools;
|
|
743
|
+
const availableTools = tools.availableTools;
|
|
744
|
+
const toolDispatcher = tools.dispatcher;
|
|
745
|
+
|
|
746
|
+
const allFindings = (Array.isArray(seedFindings) ? seedFindings : []).map((finding) =>
|
|
747
|
+
normalizeFinding(finding, normalizedAgent, "legacy-seed")
|
|
748
|
+
);
|
|
749
|
+
const assignedScopeFiles = uniqueScopeFiles(assignedFiles);
|
|
750
|
+
const loopMaxTurns = Math.max(
|
|
751
|
+
1,
|
|
752
|
+
Math.floor(normalizeNumber(maxTurns, normalizedAgent.maxTurns || DEFAULT_MAX_TURNS))
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
emit("agent_start", {
|
|
756
|
+
runId,
|
|
757
|
+
mode: "audit",
|
|
758
|
+
maxTurns: loopMaxTurns,
|
|
759
|
+
budget: resolvedBudget,
|
|
760
|
+
grantedTools,
|
|
761
|
+
availableTools,
|
|
762
|
+
isolation: isolationMode,
|
|
763
|
+
permissionMode: normalizedAgent.permissionMode,
|
|
764
|
+
dryRun: Boolean(dryRun),
|
|
765
|
+
files: assignedScopeFiles.map((file) => file.path),
|
|
766
|
+
fileCount: assignedScopeFiles.length,
|
|
767
|
+
seedFindingCount: allFindings.length,
|
|
768
|
+
deterministicBaselineFindingCount: Array.isArray(deterministicBaseline?.findings)
|
|
769
|
+
? deterministicBaseline.findings.length
|
|
770
|
+
: 0,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if (dryRun) {
|
|
774
|
+
emit("progress", {
|
|
775
|
+
phase: "dry_run",
|
|
776
|
+
message: `${normalizedAgent.id} planned with ${availableTools.length} usable tools.`,
|
|
777
|
+
});
|
|
778
|
+
const summary = severitySummary(allFindings);
|
|
779
|
+
emit("agent_complete", {
|
|
780
|
+
...summary,
|
|
781
|
+
status: "dry_run",
|
|
782
|
+
turns: 0,
|
|
783
|
+
costUsd: ctx.usage.costUsd,
|
|
784
|
+
durationMs: Date.now() - startedAt,
|
|
785
|
+
});
|
|
786
|
+
return {
|
|
787
|
+
runId,
|
|
788
|
+
agentId: normalizedAgent.id,
|
|
789
|
+
persona: normalizedAgent.persona,
|
|
790
|
+
domain: normalizedAgent.domain,
|
|
791
|
+
status: "dry_run",
|
|
792
|
+
findings: allFindings,
|
|
793
|
+
summary,
|
|
794
|
+
confidence: computeConfidence(normalizedAgent, allFindings),
|
|
795
|
+
usage: snapshotUsage(ctx),
|
|
796
|
+
grantedTools,
|
|
797
|
+
availableTools,
|
|
798
|
+
isolation: isolationMode,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const systemPrompt = buildPersonaSystemPrompt({
|
|
803
|
+
agent: normalizedAgent,
|
|
804
|
+
rootPath: resolvedRootPath,
|
|
805
|
+
ingest,
|
|
806
|
+
grantedTools,
|
|
807
|
+
availableTools,
|
|
808
|
+
sharedContext,
|
|
809
|
+
hybridContext,
|
|
810
|
+
assignedFiles,
|
|
811
|
+
});
|
|
812
|
+
messages.push({
|
|
813
|
+
role: "user",
|
|
814
|
+
content: buildInitialUserPrompt({
|
|
815
|
+
agent: normalizedAgent,
|
|
816
|
+
deterministicBaseline,
|
|
817
|
+
seedFindings: allFindings,
|
|
818
|
+
sharedContext,
|
|
819
|
+
hybridContext,
|
|
820
|
+
assignedFiles,
|
|
821
|
+
}),
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
let turnCount = 0;
|
|
825
|
+
let status = "completed";
|
|
826
|
+
|
|
827
|
+
while (turnCount < loopMaxTurns) {
|
|
828
|
+
if (abortController?.signal?.aborted) {
|
|
829
|
+
status = "aborted";
|
|
830
|
+
emit("agent_abort", { reason: "aborted", turn: turnCount });
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const preCheck = evaluateBudget({
|
|
835
|
+
sessionSummary: {
|
|
836
|
+
costUsd: ctx.usage.costUsd,
|
|
837
|
+
outputTokens: ctx.usage.outputTokens,
|
|
838
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
839
|
+
toolCalls: ctx.usage.toolCalls,
|
|
840
|
+
noProgressStreak: 0,
|
|
841
|
+
},
|
|
842
|
+
...resolvedBudget,
|
|
843
|
+
});
|
|
844
|
+
if (preCheck.blocking) {
|
|
845
|
+
status = "budget_stop";
|
|
846
|
+
emit("budget_stop", { reasons: preCheck.reasons, turn: turnCount });
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
if (preCheck.warnings.length > 0) {
|
|
850
|
+
emit("budget_warning", { warnings: preCheck.warnings, turn: turnCount });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
turnCount += 1;
|
|
854
|
+
if (turnCount % HEARTBEAT_INTERVAL_TURNS === 0) {
|
|
855
|
+
emit("heartbeat", {
|
|
856
|
+
turnsCompleted: turnCount,
|
|
857
|
+
turnsMax: loopMaxTurns,
|
|
858
|
+
findingsSoFar: allFindings.length,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let response;
|
|
863
|
+
try {
|
|
864
|
+
response = await client.invoke({
|
|
865
|
+
prompt: formatPromptForClient(systemPrompt, messages),
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
status = "llm_error_fallback";
|
|
869
|
+
emit("llm_error", {
|
|
870
|
+
turn: turnCount,
|
|
871
|
+
error: error instanceof Error ? error.message : String(error),
|
|
872
|
+
});
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const responseText = normalizeString(response?.text);
|
|
877
|
+
const responseUsage = estimateResponseUsage(response, responseText);
|
|
878
|
+
ctx.usage.outputTokens += responseUsage.outputTokens;
|
|
879
|
+
ctx.usage.costUsd += responseUsage.costUsd;
|
|
880
|
+
ctx.usage.runtimeMs = Date.now() - ctx.startedAt;
|
|
881
|
+
|
|
882
|
+
emit("reasoning", {
|
|
883
|
+
phase: "agentic_analysis",
|
|
884
|
+
turn: turnCount,
|
|
885
|
+
summary: responseText.slice(0, 240),
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const toolCalls = parseToolUseBlocks(responseText);
|
|
889
|
+
if (toolCalls.length === 0) {
|
|
890
|
+
const parsedFindings = parseJsonFindings(responseText).map((finding) =>
|
|
891
|
+
normalizeFinding(finding, normalizedAgent)
|
|
892
|
+
);
|
|
893
|
+
for (const finding of parsedFindings) {
|
|
894
|
+
allFindings.push(finding);
|
|
895
|
+
emit("finding", finding);
|
|
896
|
+
}
|
|
897
|
+
messages.push({ role: "assistant", content: responseText });
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const toolResults = [];
|
|
902
|
+
for (const call of toolCalls) {
|
|
903
|
+
if (!availableTools.includes(call.tool)) {
|
|
904
|
+
const error = `Tool ${call.tool} is not available to ${normalizedAgent.id}.`;
|
|
905
|
+
toolResults.push({ tool: call.tool, error });
|
|
906
|
+
emit("tool_result", { tool: call.tool, success: false, error });
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const input = normalizeToolInput(call.tool, call.input, resolvedRootPath);
|
|
911
|
+
const result = await toolDispatcher.dispatchTool(call.tool, input, ctx);
|
|
912
|
+
toolResults.push({ tool: call.tool, result });
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (error instanceof BudgetExhaustedError) {
|
|
915
|
+
status = "budget_stop";
|
|
916
|
+
emit("budget_stop", {
|
|
917
|
+
turn: turnCount,
|
|
918
|
+
reason: error.message,
|
|
919
|
+
reasons: error.budgetCheck?.reasons || [],
|
|
920
|
+
});
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
toolResults.push({
|
|
924
|
+
tool: call.tool,
|
|
925
|
+
error: error instanceof Error ? error.message : String(error),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
messages.push({ role: "assistant", content: responseText });
|
|
931
|
+
messages.push({
|
|
932
|
+
role: "user",
|
|
933
|
+
content:
|
|
934
|
+
toolResults
|
|
935
|
+
.map((result) =>
|
|
936
|
+
result.error
|
|
937
|
+
? `Tool ${result.tool} failed: ${result.error}`
|
|
938
|
+
: `Tool ${result.tool} result:\n${JSON.stringify(result.result).slice(0, 3500)}`
|
|
939
|
+
)
|
|
940
|
+
.join("\n\n") +
|
|
941
|
+
"\n\nContinue the audit. Call another tool if needed. If done, return final findings in a fenced JSON array.",
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
if (status === "budget_stop") {
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (turnCount >= loopMaxTurns && status === "completed") {
|
|
950
|
+
emit("progress", {
|
|
951
|
+
phase: "turn_limit",
|
|
952
|
+
message: `${normalizedAgent.id} reached maxTurns=${loopMaxTurns}.`,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const summary = severitySummary(allFindings);
|
|
957
|
+
const confidence = computeConfidence(normalizedAgent, allFindings);
|
|
958
|
+
const usage = snapshotUsage(ctx);
|
|
959
|
+
emit("agent_complete", {
|
|
960
|
+
...summary,
|
|
961
|
+
status,
|
|
962
|
+
turns: turnCount,
|
|
963
|
+
confidence,
|
|
964
|
+
costUsd: usage.costUsd,
|
|
965
|
+
durationMs: Date.now() - startedAt,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
runId,
|
|
970
|
+
agentId: normalizedAgent.id,
|
|
971
|
+
persona: normalizedAgent.persona,
|
|
972
|
+
domain: normalizedAgent.domain,
|
|
973
|
+
status,
|
|
974
|
+
findings: allFindings,
|
|
975
|
+
summary,
|
|
976
|
+
confidence,
|
|
977
|
+
usage,
|
|
978
|
+
grantedTools,
|
|
979
|
+
availableTools,
|
|
980
|
+
isolation: isolationMode,
|
|
981
|
+
messageHistoryLength: messages.length,
|
|
982
|
+
turns: turnCount,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function emitSwarmLifecycle(onEvent, event, agent, payload = {}, usage = null, runId = "") {
|
|
987
|
+
if (!onEvent) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
const evt = createAgentEvent({
|
|
991
|
+
event,
|
|
992
|
+
agent,
|
|
993
|
+
payload,
|
|
994
|
+
usage: usage || undefined,
|
|
995
|
+
runId,
|
|
996
|
+
});
|
|
997
|
+
onEvent(evt);
|
|
998
|
+
return evt;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function aggregateUsage(results = []) {
|
|
1002
|
+
const filesRead = new Set();
|
|
1003
|
+
const usage = {
|
|
1004
|
+
costUsd: 0,
|
|
1005
|
+
outputTokens: 0,
|
|
1006
|
+
toolCalls: 0,
|
|
1007
|
+
durationMs: 0,
|
|
1008
|
+
filesRead: [],
|
|
1009
|
+
};
|
|
1010
|
+
for (const result of results || []) {
|
|
1011
|
+
const resultUsage = result?.usage || {};
|
|
1012
|
+
usage.costUsd += normalizeNumber(resultUsage.costUsd, 0);
|
|
1013
|
+
usage.outputTokens += normalizeNumber(resultUsage.outputTokens, 0);
|
|
1014
|
+
usage.toolCalls += normalizeNumber(resultUsage.toolCalls, 0);
|
|
1015
|
+
usage.durationMs += normalizeNumber(resultUsage.durationMs, 0);
|
|
1016
|
+
for (const file of resultUsage.filesRead || []) {
|
|
1017
|
+
filesRead.add(file);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
usage.filesRead = [...filesRead].sort();
|
|
1021
|
+
return usage;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function summarizeSwarmStatus(results = []) {
|
|
1025
|
+
const statuses = new Set(results.map((result) => normalizeString(result.status) || "unknown"));
|
|
1026
|
+
const errorCount = results.filter((result) => result.status === "error").length;
|
|
1027
|
+
const okCount = results.length - errorCount;
|
|
1028
|
+
if (results.length === 0) {
|
|
1029
|
+
return "error";
|
|
1030
|
+
}
|
|
1031
|
+
if (errorCount === 0 && statuses.size === 1 && statuses.has("dry_run")) {
|
|
1032
|
+
return "dry_run";
|
|
1033
|
+
}
|
|
1034
|
+
if (errorCount > 0 && okCount === 0) {
|
|
1035
|
+
return "error";
|
|
1036
|
+
}
|
|
1037
|
+
if (errorCount > 0) {
|
|
1038
|
+
return "completed_with_errors";
|
|
1039
|
+
}
|
|
1040
|
+
if (statuses.has("budget_stop")) {
|
|
1041
|
+
return "budget_stop";
|
|
1042
|
+
}
|
|
1043
|
+
if (statuses.has("aborted")) {
|
|
1044
|
+
return "aborted";
|
|
1045
|
+
}
|
|
1046
|
+
if (statuses.size === 1) {
|
|
1047
|
+
return [...statuses][0];
|
|
1048
|
+
}
|
|
1049
|
+
return "completed";
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export async function runPersonaAgenticLoop(options = {}) {
|
|
1053
|
+
const {
|
|
1054
|
+
agent,
|
|
1055
|
+
rootPath,
|
|
1056
|
+
ingest = null,
|
|
1057
|
+
deterministicBaseline = null,
|
|
1058
|
+
seedFindings = [],
|
|
1059
|
+
budget = {},
|
|
1060
|
+
maxTurns = null,
|
|
1061
|
+
onEvent = null,
|
|
1062
|
+
dryRun = false,
|
|
1063
|
+
isolation = DEFAULT_ISOLATION_MODE,
|
|
1064
|
+
scope = {},
|
|
1065
|
+
} = options;
|
|
1066
|
+
const normalizedAgent = normalizeAuditPersona(agent || {});
|
|
1067
|
+
const personaScope = buildAuditPersonaFileScope({ ingest, scope });
|
|
1068
|
+
const decision = decideAuditPersonaSwarm({ scope: personaScope });
|
|
1069
|
+
|
|
1070
|
+
if (!decision.spawn || personaScope.files.length === 0) {
|
|
1071
|
+
return runSinglePersonaAgenticLoop(options);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const parentRunId =
|
|
1075
|
+
normalizeString(options.runId) ||
|
|
1076
|
+
`audit-persona-${normalizedAgent.id}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1077
|
+
const swarmRunId = `${parentRunId}-swarm`;
|
|
1078
|
+
const startedAt = Date.now();
|
|
1079
|
+
const partitions = partitionAuditPersonaFiles(
|
|
1080
|
+
personaScope.files,
|
|
1081
|
+
AUDIT_SWARM_THRESHOLDS.maxFilesPerScanner
|
|
1082
|
+
);
|
|
1083
|
+
const subagentBudget = divideAuditSwarmBudget(budget, partitions.length);
|
|
1084
|
+
const maxConcurrent = Math.min(AUDIT_SWARM_THRESHOLDS.maxConcurrentAgents, partitions.length);
|
|
1085
|
+
const parentAgent = buildAgentIdentity(normalizedAgent);
|
|
1086
|
+
const parentBudget = { ...DEFAULT_PERSONA_BUDGET, ...(budget || {}) };
|
|
1087
|
+
const grantedTools = resolveGrantedTools(normalizedAgent);
|
|
1088
|
+
const availableTools = resolveAvailableTools(normalizedAgent);
|
|
1089
|
+
const normalizedIsolation = normalizeIsolationMode(isolation);
|
|
1090
|
+
const loopMaxTurns = Math.max(
|
|
1091
|
+
1,
|
|
1092
|
+
Math.floor(normalizeNumber(maxTurns, normalizedAgent.maxTurns || DEFAULT_MAX_TURNS))
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
emitSwarmLifecycle(
|
|
1096
|
+
onEvent,
|
|
1097
|
+
"agent_start",
|
|
1098
|
+
parentAgent,
|
|
1099
|
+
{
|
|
1100
|
+
runId: parentRunId,
|
|
1101
|
+
mode: "audit",
|
|
1102
|
+
swarm: true,
|
|
1103
|
+
maxTurns: loopMaxTurns,
|
|
1104
|
+
budget: parentBudget,
|
|
1105
|
+
grantedTools,
|
|
1106
|
+
availableTools,
|
|
1107
|
+
isolation: normalizedIsolation,
|
|
1108
|
+
permissionMode: normalizedAgent.permissionMode,
|
|
1109
|
+
dryRun: Boolean(dryRun),
|
|
1110
|
+
seedFindingCount: Array.isArray(seedFindings) ? seedFindings.length : 0,
|
|
1111
|
+
deterministicBaselineFindingCount: Array.isArray(deterministicBaseline?.findings)
|
|
1112
|
+
? deterministicBaseline.findings.length
|
|
1113
|
+
: 0,
|
|
1114
|
+
fileCount: personaScope.files.length,
|
|
1115
|
+
partitionCount: partitions.length,
|
|
1116
|
+
},
|
|
1117
|
+
null,
|
|
1118
|
+
parentRunId
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
emitSwarmLifecycle(
|
|
1122
|
+
onEvent,
|
|
1123
|
+
"swarm_start",
|
|
1124
|
+
parentAgent,
|
|
1125
|
+
{
|
|
1126
|
+
runId: parentRunId,
|
|
1127
|
+
swarmRunId,
|
|
1128
|
+
personaId: normalizedAgent.id,
|
|
1129
|
+
mode: "audit",
|
|
1130
|
+
reason: decision.reason,
|
|
1131
|
+
fileCount: decision.fileCount,
|
|
1132
|
+
estimatedLoc: decision.estimatedLoc,
|
|
1133
|
+
routeGroups: decision.routeGroups,
|
|
1134
|
+
routeGroupNames: decision.routeGroupNames,
|
|
1135
|
+
partitionCount: partitions.length,
|
|
1136
|
+
maxFilesPerSubagent: AUDIT_SWARM_THRESHOLDS.maxFilesPerScanner,
|
|
1137
|
+
maxConcurrent,
|
|
1138
|
+
parentMaxCostUsd: parentBudget.maxCostUsd,
|
|
1139
|
+
subagentMaxCostUsd: subagentBudget.maxCostUsd,
|
|
1140
|
+
},
|
|
1141
|
+
null,
|
|
1142
|
+
parentRunId
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
const subagentResults = await runWithConcurrency(
|
|
1146
|
+
partitions.map((files, index) => ({ files, subagentIndex: index + 1 })),
|
|
1147
|
+
maxConcurrent,
|
|
1148
|
+
async ({ files, subagentIndex }) => {
|
|
1149
|
+
const subagentStart = Date.now();
|
|
1150
|
+
const subagentAgent = buildSubagentPersona(normalizedAgent, subagentIndex);
|
|
1151
|
+
const subagentIdentity = buildAgentIdentity(subagentAgent);
|
|
1152
|
+
const subagentRunId = `${swarmRunId}-${subagentIndex}`;
|
|
1153
|
+
const scopedFiles = files.map((file) => file.path);
|
|
1154
|
+
const eventContext = {
|
|
1155
|
+
parentRunId,
|
|
1156
|
+
swarmRunId,
|
|
1157
|
+
subagentRunId,
|
|
1158
|
+
personaId: normalizedAgent.id,
|
|
1159
|
+
parentPersonaId: normalizedAgent.id,
|
|
1160
|
+
subagentIndex,
|
|
1161
|
+
partitionCount: partitions.length,
|
|
1162
|
+
files: scopedFiles,
|
|
1163
|
+
fileCount: scopedFiles.length,
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
try {
|
|
1167
|
+
const scopedBaseline = deterministicBaseline
|
|
1168
|
+
? {
|
|
1169
|
+
...deterministicBaseline,
|
|
1170
|
+
findings: filterFindingsByFiles(deterministicBaseline.findings, files),
|
|
1171
|
+
}
|
|
1172
|
+
: deterministicBaseline;
|
|
1173
|
+
const result = await runSinglePersonaAgenticLoop({
|
|
1174
|
+
...options,
|
|
1175
|
+
agent: subagentAgent,
|
|
1176
|
+
deterministicBaseline: scopedBaseline,
|
|
1177
|
+
seedFindings: filterFindingsByFiles(seedFindings, files),
|
|
1178
|
+
budget: subagentBudget,
|
|
1179
|
+
maxTurns: loopMaxTurns,
|
|
1180
|
+
runId: subagentRunId,
|
|
1181
|
+
eventContext,
|
|
1182
|
+
assignedFiles: files,
|
|
1183
|
+
});
|
|
1184
|
+
const findings = (result.findings || []).map((finding) => ({
|
|
1185
|
+
...finding,
|
|
1186
|
+
persona: normalizedAgent.id,
|
|
1187
|
+
swarm: {
|
|
1188
|
+
runId: swarmRunId,
|
|
1189
|
+
personaId: normalizedAgent.id,
|
|
1190
|
+
subagentIndex,
|
|
1191
|
+
partitionCount: partitions.length,
|
|
1192
|
+
files: scopedFiles,
|
|
1193
|
+
},
|
|
1194
|
+
}));
|
|
1195
|
+
return {
|
|
1196
|
+
...result,
|
|
1197
|
+
agentId: subagentAgent.id,
|
|
1198
|
+
parentAgentId: normalizedAgent.id,
|
|
1199
|
+
findings,
|
|
1200
|
+
summary: severitySummary(findings),
|
|
1201
|
+
files: scopedFiles,
|
|
1202
|
+
subagentIndex,
|
|
1203
|
+
durationMs: Math.max(0, Date.now() - subagentStart),
|
|
1204
|
+
};
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
emitSwarmLifecycle(
|
|
1207
|
+
onEvent,
|
|
1208
|
+
"agent_error",
|
|
1209
|
+
subagentIdentity,
|
|
1210
|
+
{
|
|
1211
|
+
...eventContext,
|
|
1212
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1213
|
+
durationMs: Math.max(0, Date.now() - subagentStart),
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
costUsd: 0,
|
|
1217
|
+
outputTokens: 0,
|
|
1218
|
+
toolCalls: 0,
|
|
1219
|
+
durationMs: Math.max(0, Date.now() - subagentStart),
|
|
1220
|
+
},
|
|
1221
|
+
subagentRunId
|
|
1222
|
+
);
|
|
1223
|
+
return {
|
|
1224
|
+
runId: subagentRunId,
|
|
1225
|
+
agentId: subagentAgent.id,
|
|
1226
|
+
parentAgentId: normalizedAgent.id,
|
|
1227
|
+
persona: subagentAgent.persona,
|
|
1228
|
+
domain: subagentAgent.domain,
|
|
1229
|
+
status: "error",
|
|
1230
|
+
findings: [],
|
|
1231
|
+
summary: severitySummary([]),
|
|
1232
|
+
confidence: computeConfidence(normalizedAgent, []),
|
|
1233
|
+
usage: {
|
|
1234
|
+
costUsd: 0,
|
|
1235
|
+
outputTokens: 0,
|
|
1236
|
+
toolCalls: 0,
|
|
1237
|
+
durationMs: Math.max(0, Date.now() - subagentStart),
|
|
1238
|
+
filesRead: [],
|
|
1239
|
+
},
|
|
1240
|
+
grantedTools,
|
|
1241
|
+
availableTools,
|
|
1242
|
+
isolation: normalizedIsolation,
|
|
1243
|
+
messageHistoryLength: 0,
|
|
1244
|
+
turns: 0,
|
|
1245
|
+
files: scopedFiles,
|
|
1246
|
+
subagentIndex,
|
|
1247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1248
|
+
durationMs: Math.max(0, Date.now() - subagentStart),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
subagentResults.sort((left, right) => left.subagentIndex - right.subagentIndex);
|
|
1255
|
+
const findings = subagentResults.flatMap((result) => result.findings || []);
|
|
1256
|
+
const summary = severitySummary(findings);
|
|
1257
|
+
const usage = aggregateUsage(subagentResults);
|
|
1258
|
+
usage.durationMs = Math.max(0, Date.now() - startedAt);
|
|
1259
|
+
const status = summarizeSwarmStatus(subagentResults);
|
|
1260
|
+
const confidence = computeConfidence(normalizedAgent, findings);
|
|
1261
|
+
const okCount = subagentResults.filter((result) => result.status !== "error").length;
|
|
1262
|
+
const errorCount = subagentResults.length - okCount;
|
|
1263
|
+
|
|
1264
|
+
emitSwarmLifecycle(
|
|
1265
|
+
onEvent,
|
|
1266
|
+
"swarm_complete",
|
|
1267
|
+
parentAgent,
|
|
1268
|
+
{
|
|
1269
|
+
runId: parentRunId,
|
|
1270
|
+
swarmRunId,
|
|
1271
|
+
personaId: normalizedAgent.id,
|
|
1272
|
+
subagentCount: subagentResults.length,
|
|
1273
|
+
ok: okCount,
|
|
1274
|
+
error: errorCount,
|
|
1275
|
+
findings: findings.length,
|
|
1276
|
+
summary,
|
|
1277
|
+
totalCostUsd: usage.costUsd,
|
|
1278
|
+
durationMs: usage.durationMs,
|
|
1279
|
+
},
|
|
1280
|
+
usage,
|
|
1281
|
+
parentRunId
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
emitSwarmLifecycle(
|
|
1285
|
+
onEvent,
|
|
1286
|
+
"agent_complete",
|
|
1287
|
+
parentAgent,
|
|
1288
|
+
{
|
|
1289
|
+
...summary,
|
|
1290
|
+
runId: parentRunId,
|
|
1291
|
+
status,
|
|
1292
|
+
turns: subagentResults.reduce((sum, result) => sum + normalizeNumber(result.turns, 0), 0),
|
|
1293
|
+
confidence,
|
|
1294
|
+
costUsd: usage.costUsd,
|
|
1295
|
+
durationMs: usage.durationMs,
|
|
1296
|
+
swarm: true,
|
|
1297
|
+
subagentCount: subagentResults.length,
|
|
1298
|
+
},
|
|
1299
|
+
usage,
|
|
1300
|
+
parentRunId
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
return {
|
|
1304
|
+
runId: parentRunId,
|
|
1305
|
+
agentId: normalizedAgent.id,
|
|
1306
|
+
persona: normalizedAgent.persona,
|
|
1307
|
+
domain: normalizedAgent.domain,
|
|
1308
|
+
status,
|
|
1309
|
+
findings,
|
|
1310
|
+
summary,
|
|
1311
|
+
confidence,
|
|
1312
|
+
usage,
|
|
1313
|
+
grantedTools,
|
|
1314
|
+
availableTools,
|
|
1315
|
+
isolation: normalizedIsolation,
|
|
1316
|
+
messageHistoryLength: subagentResults.reduce(
|
|
1317
|
+
(sum, result) => sum + normalizeNumber(result.messageHistoryLength, 0),
|
|
1318
|
+
0
|
|
1319
|
+
),
|
|
1320
|
+
turns: subagentResults.reduce((sum, result) => sum + normalizeNumber(result.turns, 0), 0),
|
|
1321
|
+
swarm: {
|
|
1322
|
+
runId: swarmRunId,
|
|
1323
|
+
decision,
|
|
1324
|
+
subagentCount: subagentResults.length,
|
|
1325
|
+
ok: okCount,
|
|
1326
|
+
error: errorCount,
|
|
1327
|
+
partitionSizes: partitions.map((files) => files.length),
|
|
1328
|
+
subagents: subagentResults.map((result) => ({
|
|
1329
|
+
id: result.agentId,
|
|
1330
|
+
index: result.subagentIndex,
|
|
1331
|
+
status: result.status,
|
|
1332
|
+
files: result.files || [],
|
|
1333
|
+
findings: (result.findings || []).length,
|
|
1334
|
+
costUsd: result.usage?.costUsd || 0,
|
|
1335
|
+
outputTokens: result.usage?.outputTokens || 0,
|
|
1336
|
+
toolCalls: result.usage?.toolCalls || 0,
|
|
1337
|
+
durationMs: result.durationMs || result.usage?.durationMs || 0,
|
|
1338
|
+
error: result.error || null,
|
|
1339
|
+
})),
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|