@phnx-labs/agents-cli 1.21.3 → 1.22.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/CHANGELOG.md +67 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +22 -2
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { parse } from 'acorn';
|
|
3
|
+
import { knownSecretValuesFromEnv, redactSecrets, sanitizeForTerminal } from '../redact.js';
|
|
4
|
+
import { extractShellPrograms } from './shell-programs.js';
|
|
5
|
+
export const TOOL_INPUT_MAX_BYTES = 16 * 1024;
|
|
6
|
+
export const TOOL_SUCCESS_OUTPUT_MAX_BYTES = 1024;
|
|
7
|
+
export const TOOL_ERROR_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
8
|
+
export const TOOL_SESSION_EVIDENCE_MAX_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export const TOOL_PENDING_MAX_BYTES = 1024 * 1024;
|
|
10
|
+
export const TOOL_PENDING_MAX_CALLS = 256;
|
|
11
|
+
export const TOOL_CHANGED_MAX_CALLS = 10_000;
|
|
12
|
+
export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
|
|
13
|
+
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
|
|
14
|
+
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
|
|
15
|
+
export const TOOL_INDEX_VERSION = 7;
|
|
16
|
+
const SHELL_TOOLS = new Set([
|
|
17
|
+
'bash', 'exec', 'execute', 'exec_command', 'run_command', 'run_shell_command', 'shell',
|
|
18
|
+
]);
|
|
19
|
+
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
|
|
20
|
+
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
|
|
21
|
+
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
|
|
22
|
+
function byteBound(value, maxBytes) {
|
|
23
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes)
|
|
24
|
+
return value;
|
|
25
|
+
const suffix = '\n[truncated]';
|
|
26
|
+
const limit = Math.max(0, maxBytes - Buffer.byteLength(suffix));
|
|
27
|
+
let end = Math.min(value.length, limit);
|
|
28
|
+
while (end > 0 && Buffer.byteLength(value.slice(0, end), 'utf8') > limit)
|
|
29
|
+
end--;
|
|
30
|
+
return value.slice(0, end) + suffix;
|
|
31
|
+
}
|
|
32
|
+
function bytePrefix(value, maxBytes) {
|
|
33
|
+
if (value.length <= maxBytes && Buffer.byteLength(value) <= maxBytes)
|
|
34
|
+
return value;
|
|
35
|
+
let end = Math.min(value.length, maxBytes);
|
|
36
|
+
while (end > 0 && Buffer.byteLength(value.slice(0, end)) > maxBytes)
|
|
37
|
+
end--;
|
|
38
|
+
return value.slice(0, end);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Bound regex/parser work while retaining enough look-ahead to redact a secret
|
|
42
|
+
* that crosses the 64 KiB persistence boundary. An oversized configured secret
|
|
43
|
+
* makes a large source wholly opaque rather than risking a partial disclosure.
|
|
44
|
+
*/
|
|
45
|
+
function processingWindow(value, knownValues) {
|
|
46
|
+
if (value.length <= TOOL_TEXT_PROCESSING_MAX_BYTES
|
|
47
|
+
&& Buffer.byteLength(value) <= TOOL_TEXT_PROCESSING_MAX_BYTES)
|
|
48
|
+
return value;
|
|
49
|
+
const maxKnownBytes = knownValues.reduce((max, secret) => Math.max(max, Buffer.byteLength(secret)), 0);
|
|
50
|
+
if (maxKnownBytes > TOOL_TEXT_PROCESSING_MAX_BYTES)
|
|
51
|
+
return '[REDACTED oversized evidence]\n[truncated]';
|
|
52
|
+
const boundaryLookahead = Math.max(4096, maxKnownBytes);
|
|
53
|
+
return bytePrefix(value, TOOL_TEXT_PROCESSING_MAX_BYTES + boundaryLookahead);
|
|
54
|
+
}
|
|
55
|
+
export function sanitizeToolEvidenceText(value, maxBytes, knownValues = KNOWN_SECRET_VALUES) {
|
|
56
|
+
const binaryMarked = processingWindow(value, knownValues)
|
|
57
|
+
.replace(BASE64_BLOCK, (token) => `[base64:${token.length} chars]`);
|
|
58
|
+
return byteBound(sanitizeForTerminal(redactSecrets(binaryMarked, knownValues)), maxBytes);
|
|
59
|
+
}
|
|
60
|
+
function safeIdentifier(value) {
|
|
61
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (value.length <= 512 && Buffer.byteLength(value) <= 512) {
|
|
64
|
+
return sanitizeToolEvidenceText(value, 512);
|
|
65
|
+
}
|
|
66
|
+
const digest = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
67
|
+
return `${sanitizeToolEvidenceText(value, 480)}#${digest}`;
|
|
68
|
+
}
|
|
69
|
+
function boundedStableJson(value, maxBytes) {
|
|
70
|
+
const suffix = '\n[truncated]';
|
|
71
|
+
const capacity = Math.max(0, maxBytes - Buffer.byteLength(suffix));
|
|
72
|
+
const seen = new WeakSet();
|
|
73
|
+
let out = '';
|
|
74
|
+
let bytes = 0;
|
|
75
|
+
let truncated = false;
|
|
76
|
+
const append = (text) => {
|
|
77
|
+
if (truncated || text.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
const available = capacity - bytes;
|
|
80
|
+
if (available <= 0) {
|
|
81
|
+
truncated = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const prefix = bytePrefix(text, available);
|
|
85
|
+
out += prefix;
|
|
86
|
+
bytes += Buffer.byteLength(prefix);
|
|
87
|
+
if (prefix.length < text.length)
|
|
88
|
+
truncated = true;
|
|
89
|
+
};
|
|
90
|
+
const write = (item, depth) => {
|
|
91
|
+
if (truncated)
|
|
92
|
+
return;
|
|
93
|
+
if (depth > 64) {
|
|
94
|
+
append(JSON.stringify('[depth limit]'));
|
|
95
|
+
truncated = true;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof item === 'string') {
|
|
99
|
+
const prefix = bytePrefix(item, Math.max(0, capacity - bytes));
|
|
100
|
+
append(JSON.stringify(prefix));
|
|
101
|
+
if (prefix.length < item.length)
|
|
102
|
+
truncated = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (item === null || typeof item === 'number' || typeof item === 'boolean') {
|
|
106
|
+
append(JSON.stringify(item));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (typeof item !== 'object') {
|
|
110
|
+
append(JSON.stringify(String(item)));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (seen.has(item)) {
|
|
114
|
+
append(JSON.stringify('[circular]'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
seen.add(item);
|
|
118
|
+
if (Array.isArray(item)) {
|
|
119
|
+
append('[');
|
|
120
|
+
for (let i = 0; i < item.length && !truncated; i++) {
|
|
121
|
+
if (i > 0)
|
|
122
|
+
append(',');
|
|
123
|
+
write(item[i], depth + 1);
|
|
124
|
+
}
|
|
125
|
+
append(']');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const record = item;
|
|
129
|
+
const keys = [];
|
|
130
|
+
let keyBytes = 0;
|
|
131
|
+
let keysTruncated = false;
|
|
132
|
+
for (const key in record) {
|
|
133
|
+
if (!Object.prototype.hasOwnProperty.call(record, key))
|
|
134
|
+
continue;
|
|
135
|
+
keyBytes += Buffer.byteLength(key) + 4;
|
|
136
|
+
if (keys.length >= 1024 || keyBytes > capacity - bytes) {
|
|
137
|
+
keysTruncated = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
keys.push(key);
|
|
141
|
+
}
|
|
142
|
+
keys.sort((a, b) => a.localeCompare(b));
|
|
143
|
+
append('{');
|
|
144
|
+
for (let i = 0; i < keys.length && !truncated; i++) {
|
|
145
|
+
const key = keys[i];
|
|
146
|
+
if (i > 0)
|
|
147
|
+
append(',');
|
|
148
|
+
append(JSON.stringify(key));
|
|
149
|
+
append(':');
|
|
150
|
+
write(SECRET_FIELD.test(key) ? '[REDACTED]' : record[key], depth + 1);
|
|
151
|
+
}
|
|
152
|
+
append('}');
|
|
153
|
+
if (keysTruncated)
|
|
154
|
+
truncated = true;
|
|
155
|
+
};
|
|
156
|
+
write(value, 0);
|
|
157
|
+
return truncated ? bytePrefix(out, capacity) + suffix : out;
|
|
158
|
+
}
|
|
159
|
+
function canonicalInput(tool, args, command) {
|
|
160
|
+
const shellCommand = command
|
|
161
|
+
?? (typeof args?.command === 'string' ? args.command : undefined)
|
|
162
|
+
?? (typeof args?.cmd === 'string' ? args.cmd : undefined);
|
|
163
|
+
if (shellCommand)
|
|
164
|
+
return sanitizeToolEvidenceText(shellCommand, TOOL_INPUT_MAX_BYTES);
|
|
165
|
+
if (!args)
|
|
166
|
+
return tool;
|
|
167
|
+
let hasArgs = false;
|
|
168
|
+
for (const key in args) {
|
|
169
|
+
if (!Object.prototype.hasOwnProperty.call(args, key))
|
|
170
|
+
continue;
|
|
171
|
+
hasArgs = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (!hasArgs)
|
|
175
|
+
return tool;
|
|
176
|
+
try {
|
|
177
|
+
return sanitizeToolEvidenceText(boundedStableJson(args, TOOL_TEXT_PROCESSING_MAX_BYTES), TOOL_INPUT_MAX_BYTES);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return sanitizeToolEvidenceText(String(args), TOOL_INPUT_MAX_BYTES);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
|
|
184
|
+
function commandsFromCodexExec(source) {
|
|
185
|
+
const commands = [];
|
|
186
|
+
let root;
|
|
187
|
+
try {
|
|
188
|
+
root = parse(source, { ecmaVersion: 'latest', sourceType: 'module', allowAwaitOutsideFunction: true });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return commands;
|
|
192
|
+
}
|
|
193
|
+
const visit = (value) => {
|
|
194
|
+
if (!value || typeof value !== 'object')
|
|
195
|
+
return;
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
value.forEach(visit);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const node = value;
|
|
201
|
+
if (node.type === 'CallExpression'
|
|
202
|
+
&& node.callee?.type === 'MemberExpression'
|
|
203
|
+
&& node.callee.computed === false
|
|
204
|
+
&& node.callee.object?.type === 'Identifier'
|
|
205
|
+
&& node.callee.object.name === 'tools'
|
|
206
|
+
&& node.callee.property?.type === 'Identifier'
|
|
207
|
+
&& node.callee.property.name === 'exec_command'
|
|
208
|
+
&& node.arguments?.[0]?.type === 'ObjectExpression') {
|
|
209
|
+
const property = node.arguments[0].properties.find((candidate) => candidate.type === 'Property'
|
|
210
|
+
&& candidate.kind === 'init'
|
|
211
|
+
&& ((candidate.key.type === 'Identifier' && candidate.key.name === 'cmd')
|
|
212
|
+
|| (candidate.key.type === 'Literal' && candidate.key.value === 'cmd')));
|
|
213
|
+
const commandValue = property?.value;
|
|
214
|
+
if (commandValue?.type === 'Literal' && typeof commandValue.value === 'string') {
|
|
215
|
+
commands.push(commandValue.value);
|
|
216
|
+
}
|
|
217
|
+
else if (commandValue?.type === 'TemplateLiteral' && commandValue.expressions.length === 0) {
|
|
218
|
+
commands.push(commandValue.quasis[0]?.value.cooked ?? commandValue.quasis[0]?.value.raw ?? '');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const child of Object.values(node))
|
|
222
|
+
visit(child);
|
|
223
|
+
};
|
|
224
|
+
visit(root);
|
|
225
|
+
return commands;
|
|
226
|
+
}
|
|
227
|
+
function commandFor(tool, args, command) {
|
|
228
|
+
if (!SHELL_TOOLS.has(tool.toLowerCase()))
|
|
229
|
+
return undefined;
|
|
230
|
+
const direct = command
|
|
231
|
+
?? (typeof args?.command === 'string' ? args.command : undefined)
|
|
232
|
+
?? (typeof args?.cmd === 'string' ? args.cmd : undefined);
|
|
233
|
+
if (direct !== undefined)
|
|
234
|
+
return direct;
|
|
235
|
+
const input = typeof args?.input === 'string' ? args.input : undefined;
|
|
236
|
+
if (!input || tool.toLowerCase() !== 'exec')
|
|
237
|
+
return input;
|
|
238
|
+
const boundedInput = bytePrefix(input, TOOL_SHELL_PARSE_MAX_BYTES);
|
|
239
|
+
const commands = commandsFromCodexExec(boundedInput);
|
|
240
|
+
if (commands.length > 0)
|
|
241
|
+
return commands.join('\n');
|
|
242
|
+
return /\btools\s*\./.test(input) ? undefined : input;
|
|
243
|
+
}
|
|
244
|
+
function resultObject(value) {
|
|
245
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
246
|
+
? value
|
|
247
|
+
: undefined;
|
|
248
|
+
}
|
|
249
|
+
function structuredField(source, name) {
|
|
250
|
+
const metadata = resultObject(source?.metadata);
|
|
251
|
+
return source?.[name] ?? metadata?.[name];
|
|
252
|
+
}
|
|
253
|
+
function structuredNumber(source, name) {
|
|
254
|
+
const value = structuredField(source, name);
|
|
255
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
256
|
+
}
|
|
257
|
+
function structuredString(source, name) {
|
|
258
|
+
const value = structuredField(source, name);
|
|
259
|
+
return typeof value === 'string' ? value : undefined;
|
|
260
|
+
}
|
|
261
|
+
function resultText(value) {
|
|
262
|
+
if (typeof value === 'string')
|
|
263
|
+
return value;
|
|
264
|
+
const object = resultObject(value);
|
|
265
|
+
if (object) {
|
|
266
|
+
for (const key of ['output', 'stdout', 'content', 'text', 'message']) {
|
|
267
|
+
if (typeof object[key] === 'string')
|
|
268
|
+
return object[key];
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
return boundedStableJson(object, TOOL_TEXT_PROCESSING_MAX_BYTES);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return String(value);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return value == null ? '' : String(value);
|
|
278
|
+
}
|
|
279
|
+
export function structuredToolResult(value) {
|
|
280
|
+
const structured = resultObject(value);
|
|
281
|
+
const success = structuredField(structured, 'success');
|
|
282
|
+
const isError = success === false || structuredField(structured, 'is_error') === true;
|
|
283
|
+
return {
|
|
284
|
+
text: resultText(value),
|
|
285
|
+
outcome: isError ? 'error' : success === true ? 'ok' : 'unknown',
|
|
286
|
+
exitCode: structuredNumber(structured, 'exit_code'),
|
|
287
|
+
statusCode: structuredNumber(structured, 'status_code'),
|
|
288
|
+
errorCode: structuredString(structured, 'error_code'),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function buildCall(ordinal, timestamp, tool, args, command, sourceCallId) {
|
|
292
|
+
const rawShellCommand = commandFor(tool, args, command);
|
|
293
|
+
const shellCommand = rawShellCommand
|
|
294
|
+
? sanitizeToolEvidenceText(rawShellCommand, TOOL_SHELL_PARSE_MAX_BYTES)
|
|
295
|
+
: undefined;
|
|
296
|
+
const shell = shellCommand
|
|
297
|
+
? extractShellPrograms(shellCommand)
|
|
298
|
+
: { programs: [], occurrences: [], diagnostics: [] };
|
|
299
|
+
return {
|
|
300
|
+
ordinal,
|
|
301
|
+
sourceCallId: safeIdentifier(sourceCallId),
|
|
302
|
+
timestamp: sanitizeToolEvidenceText(timestamp, 128),
|
|
303
|
+
tool: sanitizeToolEvidenceText(tool, 512),
|
|
304
|
+
programs: shell.programs.map((program) => sanitizeToolEvidenceText(program, 512)),
|
|
305
|
+
programOccurrences: shell.occurrences.map((occurrence) => ({
|
|
306
|
+
program: sanitizeToolEvidenceText(occurrence.program, 512),
|
|
307
|
+
role: occurrence.role,
|
|
308
|
+
})),
|
|
309
|
+
input: canonicalInput(tool, args, command),
|
|
310
|
+
outcome: 'unknown',
|
|
311
|
+
parseError: shell.diagnostics.length > 0 ? sanitizeToolEvidenceText(shell.diagnostics.join('; '), 1024) : undefined,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
export function toolCallEvidenceBytes(call) {
|
|
315
|
+
return Buffer.byteLength([
|
|
316
|
+
call.sourceCallId, call.timestamp, call.tool, call.input, call.errorCode,
|
|
317
|
+
call.output, call.error, call.parseError, ...call.programs,
|
|
318
|
+
...call.programOccurrences.map((occurrence) => `${occurrence.role}:${occurrence.program}`),
|
|
319
|
+
].filter((value) => typeof value === 'string').join('\0'));
|
|
320
|
+
}
|
|
321
|
+
function collectionLimitCall(timestamp) {
|
|
322
|
+
return {
|
|
323
|
+
ordinal: TOOL_INDEX_LIMIT_ORDINAL,
|
|
324
|
+
timestamp,
|
|
325
|
+
tool: 'index_limit',
|
|
326
|
+
programs: [],
|
|
327
|
+
programOccurrences: [],
|
|
328
|
+
input: 'Tool evidence collection stopped at the 5 MiB or 10,000-call scan limit.',
|
|
329
|
+
outcome: 'unknown',
|
|
330
|
+
parseError: 'Additional tool calls were not retained during this scan.',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const COLLECTION_LIMIT_RESERVE_BYTES = toolCallEvidenceBytes(collectionLimitCall('1970-01-01T00:00:00.000Z'));
|
|
334
|
+
export class ToolCallCollector {
|
|
335
|
+
nextOrdinal;
|
|
336
|
+
pendingById = new Map();
|
|
337
|
+
pendingOrder = [];
|
|
338
|
+
changed = new Map();
|
|
339
|
+
changedSizes = new Map();
|
|
340
|
+
pendingBytes = 0;
|
|
341
|
+
changedBytes = 0;
|
|
342
|
+
changedTruncated = false;
|
|
343
|
+
constructor(snapshot) {
|
|
344
|
+
this.nextOrdinal = snapshot?.nextOrdinal ?? 0;
|
|
345
|
+
for (const call of snapshot?.pending ?? [])
|
|
346
|
+
this.rememberPending({ ...call });
|
|
347
|
+
}
|
|
348
|
+
start(args) {
|
|
349
|
+
const sourceCallId = safeIdentifier(args.sourceCallId);
|
|
350
|
+
if (sourceCallId) {
|
|
351
|
+
const existing = this.pendingById.get(sourceCallId);
|
|
352
|
+
if (existing)
|
|
353
|
+
return existing;
|
|
354
|
+
}
|
|
355
|
+
const timestamp = typeof args.timestamp === 'string' ? args.timestamp : '';
|
|
356
|
+
const tool = typeof args.tool === 'string' && args.tool.length > 0 ? args.tool : 'unknown';
|
|
357
|
+
const input = resultObject(args.input);
|
|
358
|
+
const command = typeof args.command === 'string' ? args.command : undefined;
|
|
359
|
+
const call = buildCall(this.nextOrdinal++, timestamp, tool, input, command, sourceCallId);
|
|
360
|
+
if (this.markChanged(call))
|
|
361
|
+
this.rememberPending(call);
|
|
362
|
+
return call;
|
|
363
|
+
}
|
|
364
|
+
finish(args) {
|
|
365
|
+
const tool = typeof args.tool === 'string' ? args.tool : undefined;
|
|
366
|
+
const call = this.takePending(args.sourceCallId, tool);
|
|
367
|
+
if (!call)
|
|
368
|
+
return undefined;
|
|
369
|
+
const outcome = args.outcome === 'ok' || args.outcome === 'error' || args.outcome === 'unknown'
|
|
370
|
+
? args.outcome
|
|
371
|
+
: undefined;
|
|
372
|
+
const error = typeof args.error === 'string' ? args.error : undefined;
|
|
373
|
+
const output = typeof args.output === 'string' ? args.output : undefined;
|
|
374
|
+
const success = typeof args.success === 'boolean' ? args.success : undefined;
|
|
375
|
+
const isError = outcome === 'error' || success === false || error !== undefined;
|
|
376
|
+
const raw = error ?? output ?? '';
|
|
377
|
+
call.outcome = isError ? 'error' : outcome ?? (success === true ? 'ok' : 'unknown');
|
|
378
|
+
call.exitCode = Number.isSafeInteger(args.exitCode) && args.exitCode >= 0 ? args.exitCode : undefined;
|
|
379
|
+
call.statusCode = Number.isSafeInteger(args.statusCode) && args.statusCode >= 0 ? args.statusCode : undefined;
|
|
380
|
+
call.errorCode = safeIdentifier(args.errorCode);
|
|
381
|
+
if (isError || call.outcome === 'error') {
|
|
382
|
+
call.error = sanitizeToolEvidenceText(raw, TOOL_ERROR_OUTPUT_MAX_BYTES);
|
|
383
|
+
call.output = undefined;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
call.output = sanitizeToolEvidenceText(raw, TOOL_SUCCESS_OUTPUT_MAX_BYTES);
|
|
387
|
+
call.error = undefined;
|
|
388
|
+
}
|
|
389
|
+
this.markChanged(call);
|
|
390
|
+
return call;
|
|
391
|
+
}
|
|
392
|
+
drainChanged() {
|
|
393
|
+
const out = [...this.changed.values()];
|
|
394
|
+
if (this.changedTruncated) {
|
|
395
|
+
out.push(collectionLimitCall(new Date().toISOString()));
|
|
396
|
+
}
|
|
397
|
+
out.sort((a, b) => a.ordinal - b.ordinal);
|
|
398
|
+
this.changed.clear();
|
|
399
|
+
this.changedSizes.clear();
|
|
400
|
+
this.changedBytes = 0;
|
|
401
|
+
this.changedTruncated = false;
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
snapshot() {
|
|
405
|
+
return {
|
|
406
|
+
v: 1,
|
|
407
|
+
nextOrdinal: this.nextOrdinal,
|
|
408
|
+
pending: this.pendingOrder.map((call) => ({ ...call })),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
recordIndexLimit() {
|
|
412
|
+
this.changedTruncated = true;
|
|
413
|
+
}
|
|
414
|
+
rememberPending(call) {
|
|
415
|
+
this.pendingOrder.push(call);
|
|
416
|
+
this.pendingBytes += toolCallEvidenceBytes(call);
|
|
417
|
+
if (call.sourceCallId)
|
|
418
|
+
this.pendingById.set(call.sourceCallId, call);
|
|
419
|
+
while (this.pendingOrder.length > TOOL_PENDING_MAX_CALLS || this.pendingBytes > TOOL_PENDING_MAX_BYTES) {
|
|
420
|
+
const dropped = this.pendingOrder.shift();
|
|
421
|
+
if (!dropped)
|
|
422
|
+
break;
|
|
423
|
+
this.pendingBytes -= toolCallEvidenceBytes(dropped);
|
|
424
|
+
if (dropped.sourceCallId)
|
|
425
|
+
this.pendingById.delete(dropped.sourceCallId);
|
|
426
|
+
dropped.parseError = 'Pending result correlation dropped at the 1 MiB continuation limit.';
|
|
427
|
+
this.markChanged(dropped);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
takePending(sourceCallId, tool) {
|
|
431
|
+
const safeCallId = safeIdentifier(sourceCallId);
|
|
432
|
+
const exact = safeCallId ? this.pendingById.get(safeCallId) : undefined;
|
|
433
|
+
if (safeCallId) {
|
|
434
|
+
if (!exact)
|
|
435
|
+
return undefined;
|
|
436
|
+
return this.removePending(exact);
|
|
437
|
+
}
|
|
438
|
+
const candidates = tool
|
|
439
|
+
? this.pendingOrder.filter((candidate) => candidate.tool === tool)
|
|
440
|
+
: this.pendingOrder;
|
|
441
|
+
// A missing harness id is safe only when one pending call can possibly own
|
|
442
|
+
// the result. Do not attach concurrent same-tool output by arrival order.
|
|
443
|
+
if (candidates.length !== 1)
|
|
444
|
+
return undefined;
|
|
445
|
+
return this.removePending(candidates[0]);
|
|
446
|
+
}
|
|
447
|
+
removePending(call) {
|
|
448
|
+
const index = this.pendingOrder.indexOf(call);
|
|
449
|
+
if (index >= 0)
|
|
450
|
+
this.pendingOrder.splice(index, 1);
|
|
451
|
+
this.pendingBytes -= toolCallEvidenceBytes(call);
|
|
452
|
+
if (call.sourceCallId)
|
|
453
|
+
this.pendingById.delete(call.sourceCallId);
|
|
454
|
+
return call;
|
|
455
|
+
}
|
|
456
|
+
markChanged(call) {
|
|
457
|
+
const previousBytes = this.changedSizes.get(call.ordinal) ?? 0;
|
|
458
|
+
const nextBytes = toolCallEvidenceBytes(call);
|
|
459
|
+
const nextTotal = this.changedBytes - previousBytes + nextBytes;
|
|
460
|
+
const isNew = !this.changed.has(call.ordinal);
|
|
461
|
+
if (nextTotal > TOOL_SESSION_EVIDENCE_MAX_BYTES - COLLECTION_LIMIT_RESERVE_BYTES
|
|
462
|
+
|| (isNew && this.changed.size >= TOOL_CHANGED_MAX_CALLS)) {
|
|
463
|
+
if (!isNew) {
|
|
464
|
+
this.changed.delete(call.ordinal);
|
|
465
|
+
this.changedSizes.delete(call.ordinal);
|
|
466
|
+
this.changedBytes -= previousBytes;
|
|
467
|
+
}
|
|
468
|
+
this.changedTruncated = true;
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
this.changed.set(call.ordinal, call);
|
|
472
|
+
this.changedSizes.set(call.ordinal, nextBytes);
|
|
473
|
+
this.changedBytes = nextTotal;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
478
|
+
export function toolCallsFromEvents(events) {
|
|
479
|
+
const collector = new ToolCallCollector();
|
|
480
|
+
for (const event of events) {
|
|
481
|
+
if (event.type === 'tool_use') {
|
|
482
|
+
collector.start({
|
|
483
|
+
timestamp: event.timestamp,
|
|
484
|
+
tool: event.tool || 'unknown',
|
|
485
|
+
input: event.args,
|
|
486
|
+
command: event.command,
|
|
487
|
+
sourceCallId: event.callId,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
else if (event.type === 'tool_result') {
|
|
491
|
+
collector.finish({
|
|
492
|
+
timestamp: event.timestamp,
|
|
493
|
+
sourceCallId: event.callId,
|
|
494
|
+
tool: event.tool,
|
|
495
|
+
success: event.success,
|
|
496
|
+
outcome: event.outcome,
|
|
497
|
+
exitCode: event.exitCode,
|
|
498
|
+
statusCode: event.statusCode,
|
|
499
|
+
errorCode: event.errorCode,
|
|
500
|
+
output: event.output,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
else if (event.type === 'error' && event.tool) {
|
|
504
|
+
const structuredError = event.outcome === 'error' || event.success === false;
|
|
505
|
+
const evidence = event.content || event.output || 'Tool execution failed';
|
|
506
|
+
collector.finish({
|
|
507
|
+
timestamp: event.timestamp,
|
|
508
|
+
sourceCallId: event.callId,
|
|
509
|
+
tool: event.tool,
|
|
510
|
+
success: structuredError ? false : undefined,
|
|
511
|
+
outcome: event.outcome,
|
|
512
|
+
exitCode: event.exitCode,
|
|
513
|
+
statusCode: event.statusCode,
|
|
514
|
+
errorCode: event.errorCode,
|
|
515
|
+
error: structuredError ? evidence : undefined,
|
|
516
|
+
output: structuredError ? undefined : evidence,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return collector.drainChanged();
|
|
521
|
+
}
|
|
522
|
+
export function toolCallKey(sessionId, ordinal) {
|
|
523
|
+
return createHash('sha256').update(`${sessionId}\0${ordinal}`).digest('hex').slice(0, 20);
|
|
524
|
+
}
|
|
525
|
+
/** Fold one raw Claude record into the resumable call collector. */
|
|
526
|
+
export function collectClaudeToolCalls(collector, parsed) {
|
|
527
|
+
const timestamp = typeof parsed?.timestamp === 'string' ? parsed.timestamp : new Date().toISOString();
|
|
528
|
+
if (parsed?.type === 'assistant') {
|
|
529
|
+
for (const block of parsed.message?.content ?? []) {
|
|
530
|
+
if (block?.type !== 'tool_use')
|
|
531
|
+
continue;
|
|
532
|
+
collector.start({
|
|
533
|
+
timestamp,
|
|
534
|
+
tool: typeof block.name === 'string' ? block.name : 'unknown',
|
|
535
|
+
input: block.input && typeof block.input === 'object' ? block.input : {},
|
|
536
|
+
command: typeof block.input?.command === 'string' ? block.input.command : undefined,
|
|
537
|
+
sourceCallId: typeof block.id === 'string' ? block.id : undefined,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else if (parsed?.type === 'user') {
|
|
542
|
+
for (const block of Array.isArray(parsed.message?.content) ? parsed.message.content : []) {
|
|
543
|
+
if (block?.type !== 'tool_result')
|
|
544
|
+
continue;
|
|
545
|
+
const text = typeof block.content === 'string'
|
|
546
|
+
? block.content
|
|
547
|
+
: Array.isArray(block.content)
|
|
548
|
+
? block.content.filter((part) => part?.type === 'text').map((part) => part.text || '').join('\n')
|
|
549
|
+
: '';
|
|
550
|
+
const structured = resultObject(block);
|
|
551
|
+
collector.finish({
|
|
552
|
+
timestamp,
|
|
553
|
+
sourceCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined,
|
|
554
|
+
success: block.is_error !== true,
|
|
555
|
+
output: block.is_error === true ? undefined : text,
|
|
556
|
+
error: block.is_error === true ? text || 'Tool execution failed' : undefined,
|
|
557
|
+
exitCode: structuredNumber(structured, 'exit_code'),
|
|
558
|
+
statusCode: structuredNumber(structured, 'status_code'),
|
|
559
|
+
errorCode: structuredString(structured, 'error_code'),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/** Fold one raw Codex rollout record into the resumable call collector. */
|
|
565
|
+
export function collectCodexToolCalls(collector, parsed) {
|
|
566
|
+
const timestamp = typeof parsed?.timestamp === 'string' ? parsed.timestamp : new Date().toISOString();
|
|
567
|
+
const payload = parsed?.payload ?? {};
|
|
568
|
+
if (parsed?.type === 'event_msg' && payload.type === 'web_search_end') {
|
|
569
|
+
collector.start({ timestamp, tool: 'WebSearch', input: { query: String(payload.query || '') } });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (parsed?.type !== 'response_item')
|
|
573
|
+
return;
|
|
574
|
+
if (payload.type === 'function_call') {
|
|
575
|
+
let input = {};
|
|
576
|
+
try {
|
|
577
|
+
input = typeof payload.arguments === 'string' ? JSON.parse(payload.arguments) : (payload.arguments || {});
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
input = { raw: String(payload.arguments || '') };
|
|
581
|
+
}
|
|
582
|
+
collector.start({
|
|
583
|
+
timestamp,
|
|
584
|
+
tool: String(payload.name || 'unknown'),
|
|
585
|
+
input,
|
|
586
|
+
command: typeof input.command === 'string' ? input.command : typeof input.cmd === 'string' ? input.cmd : undefined,
|
|
587
|
+
sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
else if (payload.type === 'custom_tool_call') {
|
|
591
|
+
collector.start({
|
|
592
|
+
timestamp,
|
|
593
|
+
tool: String(payload.name || 'unknown'),
|
|
594
|
+
input: { input: String(payload.input || '') },
|
|
595
|
+
sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
else if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
|
|
599
|
+
const result = structuredToolResult(payload.output);
|
|
600
|
+
const isError = result.outcome === 'error';
|
|
601
|
+
collector.finish({
|
|
602
|
+
timestamp,
|
|
603
|
+
sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
|
|
604
|
+
outcome: result.outcome,
|
|
605
|
+
output: isError ? undefined : result.text,
|
|
606
|
+
error: isError ? result.text || 'Tool execution failed' : undefined,
|
|
607
|
+
exitCode: result.exitCode,
|
|
608
|
+
statusCode: result.statusCode,
|
|
609
|
+
errorCode: result.errorCode,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|