sentinelayer-cli 0.1.2 → 0.4.4
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 +998 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +63 -54
- package/src/agents/jules/config/definition.js +209 -209
- package/src/agents/jules/config/system-prompt.js +175 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +377 -377
- package/src/agents/jules/loop.js +367 -367
- package/src/agents/jules/pulse.js +327 -319
- 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 +308 -308
- package/src/agents/jules/tools/auth-audit.js +557 -222
- package/src/agents/jules/tools/dispatch.js +327 -327
- package/src/agents/jules/tools/file-edit.js +180 -180
- package/src/agents/jules/tools/file-read.js +100 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +168 -168
- package/src/agents/jules/tools/grep.js +228 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +161 -161
- package/src/agents/jules/tools/runtime-audit.js +503 -493
- package/src/agents/jules/tools/shell.js +383 -383
- package/src/agents/jules/tools/url-policy.js +100 -0
- package/src/ai/aidenid.js +972 -945
- package/src/ai/client.js +508 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- 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 +45 -11
- package/src/auth/http.js +270 -113
- package/src/auth/service.js +891 -848
- package/src/auth/session-store.js +359 -345
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1337
- package/src/commands/ai/provision-governance.js +1272 -1246
- 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 +375 -366
- 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 -404
- package/src/commands/omargate.js +15 -15
- 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 +866 -788
- 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 +510 -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/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- 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/legacy-cli.js +2592 -2435
- 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 -106
- package/src/review/ai-review.js +669 -669
- package/src/review/local-review.js +1295 -1284
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -0
- package/src/scaffold/templates.js +150 -0
- package/src/scan/generator.js +418 -351
- package/src/scan/gh-secrets.js +107 -0
- 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/sync.js +107 -61
- package/src/ui/markdown.js +220 -220
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { evaluateBudget } from "../../../cost/budget.js";
|
|
3
|
-
import {
|
|
4
|
-
normalizeRunEvent,
|
|
5
|
-
appendRunEvent,
|
|
6
|
-
} from "../../../telemetry/ledger.js";
|
|
7
|
-
import { fileRead } from "./file-read.js";
|
|
8
|
-
import { grep } from "./grep.js";
|
|
9
|
-
import { glob } from "./glob.js";
|
|
10
|
-
import { shell } from "./shell.js";
|
|
11
|
-
import { fileEdit } from "./file-edit.js";
|
|
12
|
-
import { frontendAnalyze } from "./frontend-analyze.js";
|
|
13
|
-
import { runtimeAudit } from "./runtime-audit.js";
|
|
14
|
-
import { authAudit } from "./auth-audit.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Central tool dispatcher for Jules agents.
|
|
18
|
-
* Every tool call: budget check → telemetry emit → execute → telemetry result → return.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const TOOL_MAP = {
|
|
22
|
-
FileRead: fileRead,
|
|
23
|
-
Grep: grep,
|
|
24
|
-
Glob: glob,
|
|
25
|
-
Shell: shell,
|
|
26
|
-
FileEdit: fileEdit,
|
|
27
|
-
FrontendAnalyze: frontendAnalyze,
|
|
28
|
-
RuntimeAudit: runtimeAudit,
|
|
29
|
-
AuthAudit: authAudit,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const READ_ONLY_TOOLS = new Set(["FileRead", "Grep", "Glob", "FrontendAnalyze", "RuntimeAudit", "AuthAudit"]);
|
|
33
|
-
|
|
34
|
-
const RESULT_PERSIST_THRESHOLD = 5000;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {string} toolName
|
|
38
|
-
* @param {object} input
|
|
39
|
-
* @param {AgentContext} ctx
|
|
40
|
-
* @returns {Promise<ToolResult>}
|
|
41
|
-
*/
|
|
42
|
-
export async function dispatchTool(toolName, input, ctx) {
|
|
43
|
-
const handler = TOOL_MAP[toolName];
|
|
44
|
-
if (!handler) {
|
|
45
|
-
throw new ToolDispatchError(`Unknown tool: ${toolName}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 1. Pre-flight budget check
|
|
49
|
-
const budgetCheck = evaluateBudget({
|
|
50
|
-
maxCostUsd: ctx.budget.maxCostUsd,
|
|
51
|
-
maxOutputTokens: ctx.budget.maxOutputTokens,
|
|
52
|
-
maxRuntimeMs: ctx.budget.maxRuntimeMs,
|
|
53
|
-
maxToolCalls: ctx.budget.maxToolCalls,
|
|
54
|
-
warningThresholdPercent: ctx.budget.warningThresholdPercent ?? 70,
|
|
55
|
-
maxNoProgress: 0,
|
|
56
|
-
sessionSummary: {
|
|
57
|
-
costUsd: ctx.usage.costUsd,
|
|
58
|
-
outputTokens: ctx.usage.outputTokens,
|
|
59
|
-
durationMs: Date.now() - ctx.startedAt,
|
|
60
|
-
toolCalls: ctx.usage.toolCalls + 1,
|
|
61
|
-
noProgressStreak: 0,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (budgetCheck.blocking) {
|
|
66
|
-
const stopEvent = {
|
|
67
|
-
eventType: "run_stop",
|
|
68
|
-
sessionId: ctx.sessionId,
|
|
69
|
-
runId: ctx.runId,
|
|
70
|
-
stop: {
|
|
71
|
-
stopClass: budgetCheck.reasons[0]?.code || "MAX_TOOL_CALLS_EXCEEDED",
|
|
72
|
-
blocking: true,
|
|
73
|
-
reasonCodes: budgetCheck.reasons.map((r) => r.code),
|
|
74
|
-
},
|
|
75
|
-
usage: snapshotUsage(ctx),
|
|
76
|
-
metadata: { tool: toolName, phase: "pre_flight" },
|
|
77
|
-
};
|
|
78
|
-
await safeAppendEvent(ctx, stopEvent);
|
|
79
|
-
|
|
80
|
-
if (ctx.onEvent) {
|
|
81
|
-
ctx.onEvent({
|
|
82
|
-
stream: "sl_event",
|
|
83
|
-
event: "budget_stop",
|
|
84
|
-
agent: ctx.agentIdentity,
|
|
85
|
-
payload: {
|
|
86
|
-
stopClass: stopEvent.stop.stopClass,
|
|
87
|
-
reasons: budgetCheck.reasons,
|
|
88
|
-
},
|
|
89
|
-
usage: snapshotUsage(ctx),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
throw new BudgetExhaustedError(budgetCheck);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Emit budget warnings
|
|
97
|
-
if (budgetCheck.warnings.length > 0 && ctx.onEvent) {
|
|
98
|
-
ctx.onEvent({
|
|
99
|
-
stream: "sl_event",
|
|
100
|
-
event: "budget_warning",
|
|
101
|
-
agent: ctx.agentIdentity,
|
|
102
|
-
payload: { warnings: budgetCheck.warnings },
|
|
103
|
-
usage: snapshotUsage(ctx),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 2. Emit tool_call event
|
|
108
|
-
const eventId = randomUUID();
|
|
109
|
-
const callEvent = {
|
|
110
|
-
eventType: "tool_call",
|
|
111
|
-
sessionId: ctx.sessionId,
|
|
112
|
-
runId: ctx.runId,
|
|
113
|
-
metadata: {
|
|
114
|
-
eventId,
|
|
115
|
-
tool: toolName,
|
|
116
|
-
input: sanitizeInput(toolName, input),
|
|
117
|
-
agentId: ctx.agentIdentity?.id,
|
|
118
|
-
persona: ctx.agentIdentity?.persona,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
await safeAppendEvent(ctx, callEvent);
|
|
122
|
-
|
|
123
|
-
if (ctx.onEvent) {
|
|
124
|
-
ctx.onEvent({
|
|
125
|
-
stream: "sl_event",
|
|
126
|
-
event: "tool_call",
|
|
127
|
-
agent: ctx.agentIdentity,
|
|
128
|
-
payload: { tool: toolName, input: sanitizeInput(toolName, input) },
|
|
129
|
-
usage: snapshotUsage(ctx),
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 3. Execute
|
|
134
|
-
const startMs = Date.now();
|
|
135
|
-
let result;
|
|
136
|
-
let error;
|
|
137
|
-
try {
|
|
138
|
-
result = handler(input);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
error = err;
|
|
141
|
-
}
|
|
142
|
-
const durationMs = Date.now() - startMs;
|
|
143
|
-
|
|
144
|
-
// 4. Update accumulated usage
|
|
145
|
-
ctx.usage.toolCalls++;
|
|
146
|
-
ctx.usage.runtimeMs = Date.now() - ctx.startedAt;
|
|
147
|
-
ctx.lastToolCallAt = Date.now();
|
|
148
|
-
ctx.lastToolName = toolName;
|
|
149
|
-
|
|
150
|
-
// 5. Emit tool_result event
|
|
151
|
-
const resultEvent = {
|
|
152
|
-
eventType: "tool_call",
|
|
153
|
-
sessionId: ctx.sessionId,
|
|
154
|
-
runId: ctx.runId,
|
|
155
|
-
usage: {
|
|
156
|
-
durationMs,
|
|
157
|
-
toolCalls: 1,
|
|
158
|
-
},
|
|
159
|
-
metadata: {
|
|
160
|
-
eventId,
|
|
161
|
-
phase: "result",
|
|
162
|
-
tool: toolName,
|
|
163
|
-
success: !error,
|
|
164
|
-
error: error?.message,
|
|
165
|
-
agentId: ctx.agentIdentity?.id,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
await safeAppendEvent(ctx, resultEvent);
|
|
169
|
-
|
|
170
|
-
if (ctx.onEvent) {
|
|
171
|
-
ctx.onEvent({
|
|
172
|
-
stream: "sl_event",
|
|
173
|
-
event: "tool_result",
|
|
174
|
-
agent: ctx.agentIdentity,
|
|
175
|
-
payload: {
|
|
176
|
-
tool: toolName,
|
|
177
|
-
durationMs,
|
|
178
|
-
success: !error,
|
|
179
|
-
error: error?.message,
|
|
180
|
-
},
|
|
181
|
-
usage: snapshotUsage(ctx),
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (error) throw error;
|
|
186
|
-
|
|
187
|
-
// 6. Large result persistence
|
|
188
|
-
const serialized = JSON.stringify(result);
|
|
189
|
-
if (serialized.length > RESULT_PERSIST_THRESHOLD && ctx.artifactDir) {
|
|
190
|
-
const refPath = `${ctx.artifactDir}/tool-results/${eventId}.json`;
|
|
191
|
-
const fsp = await import("node:fs/promises");
|
|
192
|
-
await fsp.mkdir(`${ctx.artifactDir}/tool-results`, { recursive: true });
|
|
193
|
-
await fsp.writeFile(refPath, serialized, "utf-8");
|
|
194
|
-
return {
|
|
195
|
-
_persisted: true,
|
|
196
|
-
_refPath: refPath,
|
|
197
|
-
_summary: summarizeResult(toolName, result),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Register an additional tool (e.g., FrontendAnalyze from PR J-2).
|
|
206
|
-
*/
|
|
207
|
-
export function registerTool(name, handler, { readOnly = false } = {}) {
|
|
208
|
-
TOOL_MAP[name] = handler;
|
|
209
|
-
if (readOnly) READ_ONLY_TOOLS.add(name);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Check if a tool is read-only (safe for concurrent execution).
|
|
214
|
-
*/
|
|
215
|
-
export function isReadOnlyTool(toolName) {
|
|
216
|
-
return READ_ONLY_TOOLS.has(toolName);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Get list of available tool names.
|
|
221
|
-
*/
|
|
222
|
-
export function listTools() {
|
|
223
|
-
return Object.keys(TOOL_MAP);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Create an agent context for tool dispatch.
|
|
228
|
-
*/
|
|
229
|
-
export function createAgentContext({
|
|
230
|
-
agentIdentity,
|
|
231
|
-
budget,
|
|
232
|
-
sessionId,
|
|
233
|
-
runId,
|
|
234
|
-
artifactDir,
|
|
235
|
-
onEvent,
|
|
236
|
-
}) {
|
|
237
|
-
return {
|
|
238
|
-
agentIdentity,
|
|
239
|
-
budget: {
|
|
240
|
-
maxCostUsd: budget?.maxCostUsd ?? 5.0,
|
|
241
|
-
maxOutputTokens: budget?.maxOutputTokens ?? 12000,
|
|
242
|
-
maxRuntimeMs: budget?.maxRuntimeMs ?? 300000,
|
|
243
|
-
maxToolCalls: budget?.maxToolCalls ?? 150,
|
|
244
|
-
warningThresholdPercent: budget?.warningThresholdPercent ?? 70,
|
|
245
|
-
},
|
|
246
|
-
usage: {
|
|
247
|
-
costUsd: 0,
|
|
248
|
-
outputTokens: 0,
|
|
249
|
-
toolCalls: 0,
|
|
250
|
-
runtimeMs: 0,
|
|
251
|
-
},
|
|
252
|
-
sessionId: sessionId || randomUUID(),
|
|
253
|
-
runId: runId || `jules-${Date.now()}-${randomUUID().slice(0, 8)}`,
|
|
254
|
-
artifactDir,
|
|
255
|
-
startedAt: Date.now(),
|
|
256
|
-
lastToolCallAt: Date.now(),
|
|
257
|
-
lastToolName: null,
|
|
258
|
-
onEvent,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function snapshotUsage(ctx) {
|
|
263
|
-
return {
|
|
264
|
-
costUsd: ctx.usage.costUsd,
|
|
265
|
-
outputTokens: ctx.usage.outputTokens,
|
|
266
|
-
toolCalls: ctx.usage.toolCalls,
|
|
267
|
-
durationMs: Date.now() - ctx.startedAt,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function sanitizeInput(toolName, input) {
|
|
272
|
-
// Strip file content from telemetry (only log metadata)
|
|
273
|
-
const sanitized = { ...input };
|
|
274
|
-
if (sanitized.content && sanitized.content.length > 200) {
|
|
275
|
-
sanitized.content = `[${sanitized.content.length} chars]`;
|
|
276
|
-
}
|
|
277
|
-
return sanitized;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function summarizeResult(toolName, result) {
|
|
281
|
-
if (toolName === "FileRead") {
|
|
282
|
-
return `Read ${result.numLines} lines from ${result.filePath}`;
|
|
283
|
-
}
|
|
284
|
-
if (toolName === "Grep") {
|
|
285
|
-
return `${result.numMatches} matches in ${result.numFiles} files`;
|
|
286
|
-
}
|
|
287
|
-
if (toolName === "Glob") {
|
|
288
|
-
return `${result.numFiles} files matched`;
|
|
289
|
-
}
|
|
290
|
-
if (toolName === "Shell") {
|
|
291
|
-
return `Exit ${result.exitCode} in ${result.durationMs}ms`;
|
|
292
|
-
}
|
|
293
|
-
return `${toolName} completed`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function safeAppendEvent(ctx, eventData) {
|
|
297
|
-
try {
|
|
298
|
-
const normalized = normalizeRunEvent({
|
|
299
|
-
...eventData,
|
|
300
|
-
sessionId: ctx.sessionId,
|
|
301
|
-
runId: ctx.runId,
|
|
302
|
-
});
|
|
303
|
-
if (ctx.artifactDir) {
|
|
304
|
-
await appendRunEvent(
|
|
305
|
-
{ targetPath: ctx.artifactDir, outputDir: ctx.artifactDir },
|
|
306
|
-
normalized,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
// Telemetry failures must not block tool execution
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export class ToolDispatchError extends Error {
|
|
315
|
-
constructor(message) {
|
|
316
|
-
super(message);
|
|
317
|
-
this.name = "ToolDispatchError";
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export class BudgetExhaustedError extends Error {
|
|
322
|
-
constructor(budgetCheck) {
|
|
323
|
-
super(`Budget exhausted: ${budgetCheck.reasons.map((r) => r.code).join(", ")}`);
|
|
324
|
-
this.name = "BudgetExhaustedError";
|
|
325
|
-
this.budgetCheck = budgetCheck;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evaluateBudget } from "../../../cost/budget.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeRunEvent,
|
|
5
|
+
appendRunEvent,
|
|
6
|
+
} from "../../../telemetry/ledger.js";
|
|
7
|
+
import { fileRead } from "./file-read.js";
|
|
8
|
+
import { grep } from "./grep.js";
|
|
9
|
+
import { glob } from "./glob.js";
|
|
10
|
+
import { shell } from "./shell.js";
|
|
11
|
+
import { fileEdit } from "./file-edit.js";
|
|
12
|
+
import { frontendAnalyze } from "./frontend-analyze.js";
|
|
13
|
+
import { runtimeAudit } from "./runtime-audit.js";
|
|
14
|
+
import { authAudit } from "./auth-audit.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Central tool dispatcher for Jules agents.
|
|
18
|
+
* Every tool call: budget check → telemetry emit → execute → telemetry result → return.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const TOOL_MAP = {
|
|
22
|
+
FileRead: fileRead,
|
|
23
|
+
Grep: grep,
|
|
24
|
+
Glob: glob,
|
|
25
|
+
Shell: shell,
|
|
26
|
+
FileEdit: fileEdit,
|
|
27
|
+
FrontendAnalyze: frontendAnalyze,
|
|
28
|
+
RuntimeAudit: runtimeAudit,
|
|
29
|
+
AuthAudit: authAudit,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const READ_ONLY_TOOLS = new Set(["FileRead", "Grep", "Glob", "FrontendAnalyze", "RuntimeAudit", "AuthAudit"]);
|
|
33
|
+
|
|
34
|
+
const RESULT_PERSIST_THRESHOLD = 5000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} toolName
|
|
38
|
+
* @param {object} input
|
|
39
|
+
* @param {AgentContext} ctx
|
|
40
|
+
* @returns {Promise<ToolResult>}
|
|
41
|
+
*/
|
|
42
|
+
export async function dispatchTool(toolName, input, ctx) {
|
|
43
|
+
const handler = TOOL_MAP[toolName];
|
|
44
|
+
if (!handler) {
|
|
45
|
+
throw new ToolDispatchError(`Unknown tool: ${toolName}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 1. Pre-flight budget check
|
|
49
|
+
const budgetCheck = evaluateBudget({
|
|
50
|
+
maxCostUsd: ctx.budget.maxCostUsd,
|
|
51
|
+
maxOutputTokens: ctx.budget.maxOutputTokens,
|
|
52
|
+
maxRuntimeMs: ctx.budget.maxRuntimeMs,
|
|
53
|
+
maxToolCalls: ctx.budget.maxToolCalls,
|
|
54
|
+
warningThresholdPercent: ctx.budget.warningThresholdPercent ?? 70,
|
|
55
|
+
maxNoProgress: 0,
|
|
56
|
+
sessionSummary: {
|
|
57
|
+
costUsd: ctx.usage.costUsd,
|
|
58
|
+
outputTokens: ctx.usage.outputTokens,
|
|
59
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
60
|
+
toolCalls: ctx.usage.toolCalls + 1,
|
|
61
|
+
noProgressStreak: 0,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (budgetCheck.blocking) {
|
|
66
|
+
const stopEvent = {
|
|
67
|
+
eventType: "run_stop",
|
|
68
|
+
sessionId: ctx.sessionId,
|
|
69
|
+
runId: ctx.runId,
|
|
70
|
+
stop: {
|
|
71
|
+
stopClass: budgetCheck.reasons[0]?.code || "MAX_TOOL_CALLS_EXCEEDED",
|
|
72
|
+
blocking: true,
|
|
73
|
+
reasonCodes: budgetCheck.reasons.map((r) => r.code),
|
|
74
|
+
},
|
|
75
|
+
usage: snapshotUsage(ctx),
|
|
76
|
+
metadata: { tool: toolName, phase: "pre_flight" },
|
|
77
|
+
};
|
|
78
|
+
await safeAppendEvent(ctx, stopEvent);
|
|
79
|
+
|
|
80
|
+
if (ctx.onEvent) {
|
|
81
|
+
ctx.onEvent({
|
|
82
|
+
stream: "sl_event",
|
|
83
|
+
event: "budget_stop",
|
|
84
|
+
agent: ctx.agentIdentity,
|
|
85
|
+
payload: {
|
|
86
|
+
stopClass: stopEvent.stop.stopClass,
|
|
87
|
+
reasons: budgetCheck.reasons,
|
|
88
|
+
},
|
|
89
|
+
usage: snapshotUsage(ctx),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new BudgetExhaustedError(budgetCheck);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Emit budget warnings
|
|
97
|
+
if (budgetCheck.warnings.length > 0 && ctx.onEvent) {
|
|
98
|
+
ctx.onEvent({
|
|
99
|
+
stream: "sl_event",
|
|
100
|
+
event: "budget_warning",
|
|
101
|
+
agent: ctx.agentIdentity,
|
|
102
|
+
payload: { warnings: budgetCheck.warnings },
|
|
103
|
+
usage: snapshotUsage(ctx),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Emit tool_call event
|
|
108
|
+
const eventId = randomUUID();
|
|
109
|
+
const callEvent = {
|
|
110
|
+
eventType: "tool_call",
|
|
111
|
+
sessionId: ctx.sessionId,
|
|
112
|
+
runId: ctx.runId,
|
|
113
|
+
metadata: {
|
|
114
|
+
eventId,
|
|
115
|
+
tool: toolName,
|
|
116
|
+
input: sanitizeInput(toolName, input),
|
|
117
|
+
agentId: ctx.agentIdentity?.id,
|
|
118
|
+
persona: ctx.agentIdentity?.persona,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
await safeAppendEvent(ctx, callEvent);
|
|
122
|
+
|
|
123
|
+
if (ctx.onEvent) {
|
|
124
|
+
ctx.onEvent({
|
|
125
|
+
stream: "sl_event",
|
|
126
|
+
event: "tool_call",
|
|
127
|
+
agent: ctx.agentIdentity,
|
|
128
|
+
payload: { tool: toolName, input: sanitizeInput(toolName, input) },
|
|
129
|
+
usage: snapshotUsage(ctx),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Execute
|
|
134
|
+
const startMs = Date.now();
|
|
135
|
+
let result;
|
|
136
|
+
let error;
|
|
137
|
+
try {
|
|
138
|
+
result = handler(input);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
error = err;
|
|
141
|
+
}
|
|
142
|
+
const durationMs = Date.now() - startMs;
|
|
143
|
+
|
|
144
|
+
// 4. Update accumulated usage
|
|
145
|
+
ctx.usage.toolCalls++;
|
|
146
|
+
ctx.usage.runtimeMs = Date.now() - ctx.startedAt;
|
|
147
|
+
ctx.lastToolCallAt = Date.now();
|
|
148
|
+
ctx.lastToolName = toolName;
|
|
149
|
+
|
|
150
|
+
// 5. Emit tool_result event
|
|
151
|
+
const resultEvent = {
|
|
152
|
+
eventType: "tool_call",
|
|
153
|
+
sessionId: ctx.sessionId,
|
|
154
|
+
runId: ctx.runId,
|
|
155
|
+
usage: {
|
|
156
|
+
durationMs,
|
|
157
|
+
toolCalls: 1,
|
|
158
|
+
},
|
|
159
|
+
metadata: {
|
|
160
|
+
eventId,
|
|
161
|
+
phase: "result",
|
|
162
|
+
tool: toolName,
|
|
163
|
+
success: !error,
|
|
164
|
+
error: error?.message,
|
|
165
|
+
agentId: ctx.agentIdentity?.id,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
await safeAppendEvent(ctx, resultEvent);
|
|
169
|
+
|
|
170
|
+
if (ctx.onEvent) {
|
|
171
|
+
ctx.onEvent({
|
|
172
|
+
stream: "sl_event",
|
|
173
|
+
event: "tool_result",
|
|
174
|
+
agent: ctx.agentIdentity,
|
|
175
|
+
payload: {
|
|
176
|
+
tool: toolName,
|
|
177
|
+
durationMs,
|
|
178
|
+
success: !error,
|
|
179
|
+
error: error?.message,
|
|
180
|
+
},
|
|
181
|
+
usage: snapshotUsage(ctx),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (error) throw error;
|
|
186
|
+
|
|
187
|
+
// 6. Large result persistence
|
|
188
|
+
const serialized = JSON.stringify(result);
|
|
189
|
+
if (serialized.length > RESULT_PERSIST_THRESHOLD && ctx.artifactDir) {
|
|
190
|
+
const refPath = `${ctx.artifactDir}/tool-results/${eventId}.json`;
|
|
191
|
+
const fsp = await import("node:fs/promises");
|
|
192
|
+
await fsp.mkdir(`${ctx.artifactDir}/tool-results`, { recursive: true });
|
|
193
|
+
await fsp.writeFile(refPath, serialized, "utf-8");
|
|
194
|
+
return {
|
|
195
|
+
_persisted: true,
|
|
196
|
+
_refPath: refPath,
|
|
197
|
+
_summary: summarizeResult(toolName, result),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Register an additional tool (e.g., FrontendAnalyze from PR J-2).
|
|
206
|
+
*/
|
|
207
|
+
export function registerTool(name, handler, { readOnly = false } = {}) {
|
|
208
|
+
TOOL_MAP[name] = handler;
|
|
209
|
+
if (readOnly) READ_ONLY_TOOLS.add(name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a tool is read-only (safe for concurrent execution).
|
|
214
|
+
*/
|
|
215
|
+
export function isReadOnlyTool(toolName) {
|
|
216
|
+
return READ_ONLY_TOOLS.has(toolName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get list of available tool names.
|
|
221
|
+
*/
|
|
222
|
+
export function listTools() {
|
|
223
|
+
return Object.keys(TOOL_MAP);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create an agent context for tool dispatch.
|
|
228
|
+
*/
|
|
229
|
+
export function createAgentContext({
|
|
230
|
+
agentIdentity,
|
|
231
|
+
budget,
|
|
232
|
+
sessionId,
|
|
233
|
+
runId,
|
|
234
|
+
artifactDir,
|
|
235
|
+
onEvent,
|
|
236
|
+
}) {
|
|
237
|
+
return {
|
|
238
|
+
agentIdentity,
|
|
239
|
+
budget: {
|
|
240
|
+
maxCostUsd: budget?.maxCostUsd ?? 5.0,
|
|
241
|
+
maxOutputTokens: budget?.maxOutputTokens ?? 12000,
|
|
242
|
+
maxRuntimeMs: budget?.maxRuntimeMs ?? 300000,
|
|
243
|
+
maxToolCalls: budget?.maxToolCalls ?? 150,
|
|
244
|
+
warningThresholdPercent: budget?.warningThresholdPercent ?? 70,
|
|
245
|
+
},
|
|
246
|
+
usage: {
|
|
247
|
+
costUsd: 0,
|
|
248
|
+
outputTokens: 0,
|
|
249
|
+
toolCalls: 0,
|
|
250
|
+
runtimeMs: 0,
|
|
251
|
+
},
|
|
252
|
+
sessionId: sessionId || randomUUID(),
|
|
253
|
+
runId: runId || `jules-${Date.now()}-${randomUUID().slice(0, 8)}`,
|
|
254
|
+
artifactDir,
|
|
255
|
+
startedAt: Date.now(),
|
|
256
|
+
lastToolCallAt: Date.now(),
|
|
257
|
+
lastToolName: null,
|
|
258
|
+
onEvent,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function snapshotUsage(ctx) {
|
|
263
|
+
return {
|
|
264
|
+
costUsd: ctx.usage.costUsd,
|
|
265
|
+
outputTokens: ctx.usage.outputTokens,
|
|
266
|
+
toolCalls: ctx.usage.toolCalls,
|
|
267
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sanitizeInput(toolName, input) {
|
|
272
|
+
// Strip file content from telemetry (only log metadata)
|
|
273
|
+
const sanitized = { ...input };
|
|
274
|
+
if (sanitized.content && sanitized.content.length > 200) {
|
|
275
|
+
sanitized.content = `[${sanitized.content.length} chars]`;
|
|
276
|
+
}
|
|
277
|
+
return sanitized;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function summarizeResult(toolName, result) {
|
|
281
|
+
if (toolName === "FileRead") {
|
|
282
|
+
return `Read ${result.numLines} lines from ${result.filePath}`;
|
|
283
|
+
}
|
|
284
|
+
if (toolName === "Grep") {
|
|
285
|
+
return `${result.numMatches} matches in ${result.numFiles} files`;
|
|
286
|
+
}
|
|
287
|
+
if (toolName === "Glob") {
|
|
288
|
+
return `${result.numFiles} files matched`;
|
|
289
|
+
}
|
|
290
|
+
if (toolName === "Shell") {
|
|
291
|
+
return `Exit ${result.exitCode} in ${result.durationMs}ms`;
|
|
292
|
+
}
|
|
293
|
+
return `${toolName} completed`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function safeAppendEvent(ctx, eventData) {
|
|
297
|
+
try {
|
|
298
|
+
const normalized = normalizeRunEvent({
|
|
299
|
+
...eventData,
|
|
300
|
+
sessionId: ctx.sessionId,
|
|
301
|
+
runId: ctx.runId,
|
|
302
|
+
});
|
|
303
|
+
if (ctx.artifactDir) {
|
|
304
|
+
await appendRunEvent(
|
|
305
|
+
{ targetPath: ctx.artifactDir, outputDir: ctx.artifactDir },
|
|
306
|
+
normalized,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
// Telemetry failures must not block tool execution
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class ToolDispatchError extends Error {
|
|
315
|
+
constructor(message) {
|
|
316
|
+
super(message);
|
|
317
|
+
this.name = "ToolDispatchError";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export class BudgetExhaustedError extends Error {
|
|
322
|
+
constructor(budgetCheck) {
|
|
323
|
+
super(`Budget exhausted: ${budgetCheck.reasons.map((r) => r.code).join(", ")}`);
|
|
324
|
+
this.name = "BudgetExhaustedError";
|
|
325
|
+
this.budgetCheck = budgetCheck;
|
|
326
|
+
}
|
|
327
|
+
}
|