sentinelayer-cli 0.4.4 → 0.6.2
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 +996 -998
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +63 -63
- package/src/agents/jules/config/definition.js +160 -209
- package/src/agents/jules/config/system-prompt.js +182 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -377
- package/src/agents/jules/loop.js +450 -367
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +309 -308
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1691 -557
- package/src/agents/jules/tools/dispatch.js +335 -327
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +507 -503
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/persona-visuals.js +61 -0
- package/src/agents/shared-tools/dispatch-core.js +315 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +1009 -972
- package/src/ai/client.js +553 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -0
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +371 -126
- package/src/auth/http.js +611 -270
- package/src/auth/service.js +1106 -891
- package/src/auth/session-store.js +813 -359
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +419 -375
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +29 -21
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +872 -866
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/fix-cycle.js +377 -0
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/interactive/index.js +97 -95
- package/src/legacy-cli.js +2994 -2592
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -118
- package/src/review/ai-review.js +679 -669
- package/src/review/local-review.js +1305 -1295
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +300 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/scan-modes.js +42 -0
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -118
- package/src/telemetry/sync.js +203 -199
- package/src/ui/command-hints.js +13 -0
- package/src/ui/markdown.js +220 -220
|
@@ -1,308 +1,309 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { createAgentContext, dispatchTool, isReadOnlyTool, BudgetExhaustedError } from "../tools/dispatch.js";
|
|
3
|
-
import { createMultiProviderApiClient } from "../../../ai/client.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* JulesSubAgent — lightweight isolated agent for parallel audit work.
|
|
7
|
-
*
|
|
8
|
-
* Each sub-agent gets:
|
|
9
|
-
* - Own conversation context (no parent history)
|
|
10
|
-
* - Own tool access (subset of Jules' tools)
|
|
11
|
-
* - Own budget slice (clamped to parent allocation)
|
|
12
|
-
* - Shared blackboard (append-only)
|
|
13
|
-
* - Own telemetry session
|
|
14
|
-
* - AbortController linked to parent (kill propagation)
|
|
15
|
-
*
|
|
16
|
-
* Sub-agents are NOT full Jules instances. They are focused workers:
|
|
17
|
-
* - FileScanner: reads file batches, extracts structured summaries
|
|
18
|
-
* - PatternHunter: searches for specific issue classes
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const DEFAULT_MAX_TURNS = 10;
|
|
22
|
-
const DEFAULT_TEMPERATURE = 0;
|
|
23
|
-
|
|
24
|
-
export class JulesSubAgent {
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} config
|
|
27
|
-
* @param {string} config.id - Unique identifier (e.g., "file-scanner-dashboard")
|
|
28
|
-
* @param {string} config.role - "FileScanner" | "PatternHunter" | "custom"
|
|
29
|
-
* @param {string} config.systemPrompt - System instruction for this sub-agent
|
|
30
|
-
* @param {string[]} config.allowedTools - Tool names this agent can use
|
|
31
|
-
* @param {object} config.scope - { files: string[], patterns: string[] }
|
|
32
|
-
* @param {object} config.budget - Budget slice { maxCostUsd, maxOutputTokens, maxRuntimeMs, maxToolCalls }
|
|
33
|
-
* @param {object} config.blackboard - Shared blackboard instance (appendEntry, query)
|
|
34
|
-
* @param {object} [config.provider] - { provider, model, apiKey } overrides
|
|
35
|
-
* @param {number} [config.maxTurns] - Max agentic loop iterations
|
|
36
|
-
* @param {AbortController} [config.parentAbort] - Linked to parent for kill propagation
|
|
37
|
-
* @param {function} [config.onEvent] - Streaming event callback
|
|
38
|
-
*/
|
|
39
|
-
constructor(config) {
|
|
40
|
-
this.id = config.id || `subagent-${randomUUID().slice(0, 8)}`;
|
|
41
|
-
this.role = config.role;
|
|
42
|
-
this.systemPrompt = config.systemPrompt;
|
|
43
|
-
this.allowedTools = new Set(config.allowedTools || ["FileRead", "Grep", "Glob", "FrontendAnalyze"]);
|
|
44
|
-
this.scope = config.scope || {};
|
|
45
|
-
this.maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
46
|
-
this.blackboard = config.blackboard;
|
|
47
|
-
this.onEvent = config.onEvent;
|
|
48
|
-
|
|
49
|
-
// Isolated context
|
|
50
|
-
this.conversation = [];
|
|
51
|
-
this.findings = [];
|
|
52
|
-
this.turnCount = 0;
|
|
53
|
-
|
|
54
|
-
// Budget-gated agent context
|
|
55
|
-
this.ctx = createAgentContext({
|
|
56
|
-
agentIdentity: {
|
|
57
|
-
id: this.id,
|
|
58
|
-
persona: `Jules Sub-Agent (${this.role})`,
|
|
59
|
-
parentId: "frontend",
|
|
60
|
-
},
|
|
61
|
-
budget: config.budget || {
|
|
62
|
-
maxCostUsd: 1.0,
|
|
63
|
-
maxOutputTokens: 4000,
|
|
64
|
-
maxRuntimeMs: 120000,
|
|
65
|
-
maxToolCalls: 50,
|
|
66
|
-
},
|
|
67
|
-
sessionId: randomUUID(),
|
|
68
|
-
runId: `sub-${this.id}-${Date.now()}`,
|
|
69
|
-
onEvent: config.onEvent,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// LLM client
|
|
73
|
-
this.client = createMultiProviderApiClient(config.provider || {});
|
|
74
|
-
|
|
75
|
-
// Abort linkage
|
|
76
|
-
this.abortController = new AbortController();
|
|
77
|
-
if (config.parentAbort) {
|
|
78
|
-
config.parentAbort.signal.addEventListener("abort", () => {
|
|
79
|
-
this.abortController.abort();
|
|
80
|
-
}, { once: true });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Execute the sub-agent's task.
|
|
86
|
-
* Runs an agentic loop: LLM → tool_use → execute → feed back → repeat.
|
|
87
|
-
* Returns structured results.
|
|
88
|
-
*/
|
|
89
|
-
async execute() {
|
|
90
|
-
this.emitEvent("agent_start", { role: this.role, scope: this.scope });
|
|
91
|
-
|
|
92
|
-
// Build initial messages
|
|
93
|
-
const messages = [
|
|
94
|
-
{ role: "user", content: this.buildTaskPrompt() },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
while (this.turnCount < this.maxTurns) {
|
|
99
|
-
if (this.abortController.signal.aborted) {
|
|
100
|
-
this.emitEvent("agent_abort", { reason: "parent_killed" });
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
this.turnCount++;
|
|
105
|
-
|
|
106
|
-
// Call LLM
|
|
107
|
-
const response = await this.client.invoke({
|
|
108
|
-
systemPrompt: this.systemPrompt,
|
|
109
|
-
messages,
|
|
110
|
-
temperature: DEFAULT_TEMPERATURE,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Track cost
|
|
114
|
-
this.ctx.usage.outputTokens += estimateTokens(response.text);
|
|
115
|
-
this.ctx.usage.costUsd += estimateCost(response.text);
|
|
116
|
-
|
|
117
|
-
// Parse tool_use blocks from response
|
|
118
|
-
const toolCalls = parseToolCalls(response.text);
|
|
119
|
-
|
|
120
|
-
if (toolCalls.length === 0) {
|
|
121
|
-
// No more tool calls — sub-agent is done
|
|
122
|
-
const structured = parseStructuredOutput(response.text);
|
|
123
|
-
if (structured.findings) {
|
|
124
|
-
for (const finding of structured.findings) {
|
|
125
|
-
this.findings.push(finding);
|
|
126
|
-
if (this.blackboard) {
|
|
127
|
-
await this.blackboard.appendEntry({
|
|
128
|
-
agentId: this.id,
|
|
129
|
-
source: this.role,
|
|
130
|
-
...finding,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
messages.push({ role: "assistant", content: response.text });
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Execute tool calls
|
|
140
|
-
const toolResults = [];
|
|
141
|
-
for (const call of toolCalls) {
|
|
142
|
-
if (!this.allowedTools.has(call.tool)) {
|
|
143
|
-
toolResults.push({ tool: call.tool, error: `Tool ${call.tool} not allowed for this sub-agent` });
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const result = await dispatchTool(call.tool, call.input, this.ctx);
|
|
148
|
-
toolResults.push({ tool: call.tool, result });
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (err instanceof BudgetExhaustedError) {
|
|
151
|
-
this.emitEvent("budget_stop", { reason: err.message });
|
|
152
|
-
return this.buildResult("budget_exhausted");
|
|
153
|
-
}
|
|
154
|
-
toolResults.push({ tool: call.tool, error: err.message });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Feed results back to conversation
|
|
159
|
-
messages.push({ role: "assistant", content: response.text });
|
|
160
|
-
messages.push({
|
|
161
|
-
role: "user",
|
|
162
|
-
content: formatToolResults(toolResults),
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
} catch (err) {
|
|
166
|
-
this.emitEvent("agent_error", { error: err.message });
|
|
167
|
-
return this.buildResult("error", err.message);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
this.emitEvent("agent_complete", {
|
|
171
|
-
findings: this.findings.length,
|
|
172
|
-
turns: this.turnCount,
|
|
173
|
-
toolCalls: this.ctx.usage.toolCalls,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return this.buildResult("completed");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
buildTaskPrompt() {
|
|
180
|
-
const parts = [];
|
|
181
|
-
if (this.scope.files && this.scope.files.length > 0) {
|
|
182
|
-
parts.push(`Files in your scope:\n${this.scope.files.join("\n")}`);
|
|
183
|
-
}
|
|
184
|
-
if (this.scope.patterns && this.scope.patterns.length > 0) {
|
|
185
|
-
parts.push(`Patterns to search for:\n${this.scope.patterns.join("\n")}`);
|
|
186
|
-
}
|
|
187
|
-
parts.push("Return your findings as a JSON array in a ```json code block.");
|
|
188
|
-
return parts.join("\n\n");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
buildResult(status, error) {
|
|
192
|
-
return {
|
|
193
|
-
agentId: this.id,
|
|
194
|
-
role: this.role,
|
|
195
|
-
status,
|
|
196
|
-
error: error || null,
|
|
197
|
-
findings: this.findings,
|
|
198
|
-
usage: {
|
|
199
|
-
turns: this.turnCount,
|
|
200
|
-
toolCalls: this.ctx.usage.toolCalls,
|
|
201
|
-
costUsd: this.ctx.usage.costUsd,
|
|
202
|
-
outputTokens: this.ctx.usage.outputTokens,
|
|
203
|
-
durationMs: Date.now() - this.ctx.startedAt,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createAgentContext, dispatchTool, isReadOnlyTool, BudgetExhaustedError } from "../tools/dispatch.js";
|
|
3
|
+
import { createMultiProviderApiClient } from "../../../ai/client.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JulesSubAgent — lightweight isolated agent for parallel audit work.
|
|
7
|
+
*
|
|
8
|
+
* Each sub-agent gets:
|
|
9
|
+
* - Own conversation context (no parent history)
|
|
10
|
+
* - Own tool access (subset of Jules' tools)
|
|
11
|
+
* - Own budget slice (clamped to parent allocation)
|
|
12
|
+
* - Shared blackboard (append-only)
|
|
13
|
+
* - Own telemetry session
|
|
14
|
+
* - AbortController linked to parent (kill propagation)
|
|
15
|
+
*
|
|
16
|
+
* Sub-agents are NOT full Jules instances. They are focused workers:
|
|
17
|
+
* - FileScanner: reads file batches, extracts structured summaries
|
|
18
|
+
* - PatternHunter: searches for specific issue classes
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MAX_TURNS = 10;
|
|
22
|
+
const DEFAULT_TEMPERATURE = 0;
|
|
23
|
+
|
|
24
|
+
export class JulesSubAgent {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} config
|
|
27
|
+
* @param {string} config.id - Unique identifier (e.g., "file-scanner-dashboard")
|
|
28
|
+
* @param {string} config.role - "FileScanner" | "PatternHunter" | "custom"
|
|
29
|
+
* @param {string} config.systemPrompt - System instruction for this sub-agent
|
|
30
|
+
* @param {string[]} config.allowedTools - Tool names this agent can use
|
|
31
|
+
* @param {object} config.scope - { files: string[], patterns: string[] }
|
|
32
|
+
* @param {object} config.budget - Budget slice { maxCostUsd, maxOutputTokens, maxRuntimeMs, maxToolCalls }
|
|
33
|
+
* @param {object} config.blackboard - Shared blackboard instance (appendEntry, query)
|
|
34
|
+
* @param {object} [config.provider] - { provider, model, apiKey } overrides
|
|
35
|
+
* @param {number} [config.maxTurns] - Max agentic loop iterations
|
|
36
|
+
* @param {AbortController} [config.parentAbort] - Linked to parent for kill propagation
|
|
37
|
+
* @param {function} [config.onEvent] - Streaming event callback
|
|
38
|
+
*/
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.id = config.id || `subagent-${randomUUID().slice(0, 8)}`;
|
|
41
|
+
this.role = config.role;
|
|
42
|
+
this.systemPrompt = config.systemPrompt;
|
|
43
|
+
this.allowedTools = new Set(config.allowedTools || ["FileRead", "Grep", "Glob", "FrontendAnalyze"]);
|
|
44
|
+
this.scope = config.scope || {};
|
|
45
|
+
this.maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
46
|
+
this.blackboard = config.blackboard;
|
|
47
|
+
this.onEvent = config.onEvent;
|
|
48
|
+
|
|
49
|
+
// Isolated context
|
|
50
|
+
this.conversation = [];
|
|
51
|
+
this.findings = [];
|
|
52
|
+
this.turnCount = 0;
|
|
53
|
+
|
|
54
|
+
// Budget-gated agent context
|
|
55
|
+
this.ctx = createAgentContext({
|
|
56
|
+
agentIdentity: {
|
|
57
|
+
id: this.id,
|
|
58
|
+
persona: `Jules Sub-Agent (${this.role})`,
|
|
59
|
+
parentId: "frontend",
|
|
60
|
+
},
|
|
61
|
+
budget: config.budget || {
|
|
62
|
+
maxCostUsd: 1.0,
|
|
63
|
+
maxOutputTokens: 4000,
|
|
64
|
+
maxRuntimeMs: 120000,
|
|
65
|
+
maxToolCalls: 50,
|
|
66
|
+
},
|
|
67
|
+
sessionId: randomUUID(),
|
|
68
|
+
runId: `sub-${this.id}-${Date.now()}`,
|
|
69
|
+
onEvent: config.onEvent,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// LLM client
|
|
73
|
+
this.client = createMultiProviderApiClient(config.provider || {});
|
|
74
|
+
|
|
75
|
+
// Abort linkage
|
|
76
|
+
this.abortController = new AbortController();
|
|
77
|
+
if (config.parentAbort) {
|
|
78
|
+
config.parentAbort.signal.addEventListener("abort", () => {
|
|
79
|
+
this.abortController.abort();
|
|
80
|
+
}, { once: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute the sub-agent's task.
|
|
86
|
+
* Runs an agentic loop: LLM → tool_use → execute → feed back → repeat.
|
|
87
|
+
* Returns structured results.
|
|
88
|
+
*/
|
|
89
|
+
async execute() {
|
|
90
|
+
this.emitEvent("agent_start", { role: this.role, scope: this.scope });
|
|
91
|
+
|
|
92
|
+
// Build initial messages
|
|
93
|
+
const messages = [
|
|
94
|
+
{ role: "user", content: this.buildTaskPrompt() },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
while (this.turnCount < this.maxTurns) {
|
|
99
|
+
if (this.abortController.signal.aborted) {
|
|
100
|
+
this.emitEvent("agent_abort", { reason: "parent_killed" });
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.turnCount++;
|
|
105
|
+
|
|
106
|
+
// Call LLM
|
|
107
|
+
const response = await this.client.invoke({
|
|
108
|
+
systemPrompt: this.systemPrompt,
|
|
109
|
+
messages,
|
|
110
|
+
temperature: DEFAULT_TEMPERATURE,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Track cost
|
|
114
|
+
this.ctx.usage.outputTokens += estimateTokens(response.text);
|
|
115
|
+
this.ctx.usage.costUsd += estimateCost(response.text);
|
|
116
|
+
|
|
117
|
+
// Parse tool_use blocks from response
|
|
118
|
+
const toolCalls = parseToolCalls(response.text);
|
|
119
|
+
|
|
120
|
+
if (toolCalls.length === 0) {
|
|
121
|
+
// No more tool calls — sub-agent is done
|
|
122
|
+
const structured = parseStructuredOutput(response.text);
|
|
123
|
+
if (structured.findings) {
|
|
124
|
+
for (const finding of structured.findings) {
|
|
125
|
+
this.findings.push(finding);
|
|
126
|
+
if (this.blackboard) {
|
|
127
|
+
await this.blackboard.appendEntry({
|
|
128
|
+
agentId: this.id,
|
|
129
|
+
source: this.role,
|
|
130
|
+
...finding,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
messages.push({ role: "assistant", content: response.text });
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Execute tool calls
|
|
140
|
+
const toolResults = [];
|
|
141
|
+
for (const call of toolCalls) {
|
|
142
|
+
if (!this.allowedTools.has(call.tool)) {
|
|
143
|
+
toolResults.push({ tool: call.tool, error: `Tool ${call.tool} not allowed for this sub-agent` });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const result = await dispatchTool(call.tool, call.input, this.ctx);
|
|
148
|
+
toolResults.push({ tool: call.tool, result });
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err instanceof BudgetExhaustedError) {
|
|
151
|
+
this.emitEvent("budget_stop", { reason: err.message });
|
|
152
|
+
return this.buildResult("budget_exhausted");
|
|
153
|
+
}
|
|
154
|
+
toolResults.push({ tool: call.tool, error: err.message });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Feed results back to conversation
|
|
159
|
+
messages.push({ role: "assistant", content: response.text });
|
|
160
|
+
messages.push({
|
|
161
|
+
role: "user",
|
|
162
|
+
content: formatToolResults(toolResults),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.emitEvent("agent_error", { error: err.message });
|
|
167
|
+
return this.buildResult("error", err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.emitEvent("agent_complete", {
|
|
171
|
+
findings: this.findings.length,
|
|
172
|
+
turns: this.turnCount,
|
|
173
|
+
toolCalls: this.ctx.usage.toolCalls,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return this.buildResult("completed");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
buildTaskPrompt() {
|
|
180
|
+
const parts = [];
|
|
181
|
+
if (this.scope.files && this.scope.files.length > 0) {
|
|
182
|
+
parts.push(`Files in your scope:\n${this.scope.files.join("\n")}`);
|
|
183
|
+
}
|
|
184
|
+
if (this.scope.patterns && this.scope.patterns.length > 0) {
|
|
185
|
+
parts.push(`Patterns to search for:\n${this.scope.patterns.join("\n")}`);
|
|
186
|
+
}
|
|
187
|
+
parts.push("Return your findings as a JSON array in a ```json code block.");
|
|
188
|
+
return parts.join("\n\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
buildResult(status, error) {
|
|
192
|
+
return {
|
|
193
|
+
agentId: this.id,
|
|
194
|
+
role: this.role,
|
|
195
|
+
status,
|
|
196
|
+
error: error || null,
|
|
197
|
+
findings: this.findings,
|
|
198
|
+
usage: {
|
|
199
|
+
turns: this.turnCount,
|
|
200
|
+
toolCalls: this.ctx.usage.toolCalls,
|
|
201
|
+
costUsd: this.ctx.usage.costUsd,
|
|
202
|
+
outputTokens: this.ctx.usage.outputTokens,
|
|
203
|
+
durationMs: Date.now() - this.ctx.startedAt,
|
|
204
|
+
filesRead: [...(this.ctx.usage.filesRead || [])],
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
emitEvent(event, payload) {
|
|
210
|
+
if (this.onEvent) {
|
|
211
|
+
this.onEvent({
|
|
212
|
+
stream: "sl_event",
|
|
213
|
+
event,
|
|
214
|
+
agent: { id: this.id, persona: `Jules Sub-Agent (${this.role})`, parentId: "frontend" },
|
|
215
|
+
payload,
|
|
216
|
+
usage: {
|
|
217
|
+
costUsd: this.ctx.usage.costUsd,
|
|
218
|
+
toolCalls: this.ctx.usage.toolCalls,
|
|
219
|
+
durationMs: Date.now() - this.ctx.startedAt,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run a batch of sub-agents with concurrency control.
|
|
228
|
+
*/
|
|
229
|
+
export async function runSubAgentBatch(agents, { maxConcurrent = 4 } = {}) {
|
|
230
|
+
const results = [];
|
|
231
|
+
const queue = [...agents];
|
|
232
|
+
|
|
233
|
+
async function runNext() {
|
|
234
|
+
while (queue.length > 0) {
|
|
235
|
+
const agent = queue.shift();
|
|
236
|
+
const result = await agent.execute();
|
|
237
|
+
results.push(result);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const workers = Array.from(
|
|
242
|
+
{ length: Math.min(maxConcurrent, agents.length) },
|
|
243
|
+
() => runNext(),
|
|
244
|
+
);
|
|
245
|
+
await Promise.all(workers);
|
|
246
|
+
return results;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function parseToolCalls(text) {
|
|
252
|
+
// Parse tool_use blocks from LLM response
|
|
253
|
+
// Format: ```tool_use\n{"tool":"FileRead","input":{...}}\n```
|
|
254
|
+
const calls = [];
|
|
255
|
+
const regex = /```tool_use\s*\n([\s\S]*?)```/g;
|
|
256
|
+
let match;
|
|
257
|
+
while ((match = regex.exec(text)) !== null) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(match[1].trim());
|
|
260
|
+
if (parsed.tool && parsed.input) {
|
|
261
|
+
calls.push(parsed);
|
|
262
|
+
}
|
|
263
|
+
} catch { /* skip malformed */ }
|
|
264
|
+
}
|
|
265
|
+
return calls;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseStructuredOutput(text) {
|
|
269
|
+
// Parse JSON findings from LLM response
|
|
270
|
+
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)```/);
|
|
271
|
+
if (jsonMatch) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
274
|
+
if (Array.isArray(parsed)) {
|
|
275
|
+
return { findings: parsed };
|
|
276
|
+
}
|
|
277
|
+
if (parsed.findings && Array.isArray(parsed.findings)) {
|
|
278
|
+
return parsed;
|
|
279
|
+
}
|
|
280
|
+
} catch { /* skip malformed */ }
|
|
281
|
+
}
|
|
282
|
+
return { findings: [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatToolResults(results) {
|
|
286
|
+
return results.map(r => {
|
|
287
|
+
if (r.error) return `Tool ${r.tool} failed: ${r.error}`;
|
|
288
|
+
const summary = typeof r.result === "string" ? r.result :
|
|
289
|
+
JSON.stringify(r.result).slice(0, 2000);
|
|
290
|
+
return `Tool ${r.tool} result:\n${summary}`;
|
|
291
|
+
}).join("\n\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function estimateTokens(text) {
|
|
295
|
+
return Math.ceil((text || "").length / 4);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function estimateCost(text) {
|
|
299
|
+
// Rough: $15/M output tokens for Claude Sonnet
|
|
300
|
+
const tokens = estimateTokens(text);
|
|
301
|
+
return (tokens / 1_000_000) * 15;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export class SubAgentError extends Error {
|
|
305
|
+
constructor(message) {
|
|
306
|
+
super(message);
|
|
307
|
+
this.name = "SubAgentError";
|
|
308
|
+
}
|
|
309
|
+
}
|