sidekick-shared 0.29.0 → 0.30.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 +48 -0
- package/README.md +198 -56
- package/dist/aggregation/EventAggregator.d.ts +96 -2
- package/dist/aggregation/EventAggregator.js +535 -42
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +38 -1
- package/dist/browser.d.ts +9 -1
- package/dist/browser.js +29 -3
- package/dist/context/sessionContext.js +8 -0
- package/dist/esm/aggregation/EventAggregator.js +535 -42
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/browser.js +3 -0
- package/dist/esm/context/sessionContext.js +8 -0
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/modelCatalog.js +5 -4
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +4 -3
- package/dist/esm/node.js +3 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +372 -7
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +37 -12
- package/dist/esm/parsers/subagentTraceParser.js +103 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +60 -29
- package/dist/esm/providers/codex.js +235 -201
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +63 -14
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +363 -0
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +8 -0
- package/dist/esm/turns/assistantTurn.js +6 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +45 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +82 -48
- package/dist/esm/watchers/jsonlWatcher.js +61 -4
- package/dist/esm/watchers/providerReaderWatcher.js +18 -4
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +48 -10
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +5 -1
- package/dist/node.js +18 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +107 -0
- package/dist/parsers/codexParser.js +373 -7
- package/dist/parsers/openCodeParser.js +2 -1
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +37 -12
- package/dist/parsers/subagentTraceParser.d.ts +33 -5
- package/dist/parsers/subagentTraceParser.js +103 -37
- package/dist/pricingCatalog.js +3 -2
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +59 -28
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +232 -198
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +36 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +51 -10
- package/dist/schemas/sessionEvent.js +63 -14
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +175 -0
- package/dist/subagentSpawn.js +385 -0
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +8 -0
- package/dist/turns/assistantTurn.d.ts +4 -0
- package/dist/turns/assistantTurn.js +6 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +45 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +27 -4
- package/dist/watchers/jsonlTail.js +84 -48
- package/dist/watchers/jsonlWatcher.d.ts +22 -1
- package/dist/watchers/jsonlWatcher.js +60 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +18 -4
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { isAggregateCodexLimit } from '../types/codex';
|
|
15
15
|
import { normalizeToolName } from './openCodeParser';
|
|
16
16
|
import { normalizeProviderUsage } from '../usageNormalization';
|
|
17
|
+
import { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, parseCodexAgentMessageText, } from '../subagentActivity';
|
|
17
18
|
function reportedCompaction(tokensBefore, tokensAfter) {
|
|
18
19
|
if (typeof tokensBefore !== 'number' ||
|
|
19
20
|
typeof tokensAfter !== 'number' ||
|
|
@@ -171,6 +172,84 @@ export function extractPatchFilePaths(input) {
|
|
|
171
172
|
}
|
|
172
173
|
/** Default cap on each piece of per-call correlation state the parser keeps. */
|
|
173
174
|
export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
|
|
175
|
+
/** Entries per table {@link CodexRolloutParser.exportResumeState} keeps by default. */
|
|
176
|
+
const DEFAULT_RESUME_STATE_LIMIT = 256;
|
|
177
|
+
/** Raw names of Codex's multi-agent tools (namespace `collaboration`). */
|
|
178
|
+
const COLLABORATION_TOOL_NAMES = new Set([
|
|
179
|
+
'spawn_agent',
|
|
180
|
+
'followup_task',
|
|
181
|
+
'send_message',
|
|
182
|
+
'interrupt_agent',
|
|
183
|
+
'wait_agent',
|
|
184
|
+
'list_agents',
|
|
185
|
+
]);
|
|
186
|
+
const SUBAGENT_ACTIVITY_KINDS = new Set([
|
|
187
|
+
'started',
|
|
188
|
+
'completed',
|
|
189
|
+
'interacted',
|
|
190
|
+
'interrupted',
|
|
191
|
+
]);
|
|
192
|
+
function nonEmptyString(value) {
|
|
193
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
194
|
+
}
|
|
195
|
+
/** `subagent_history_start_ordinal` of a session_meta payload, when valid. */
|
|
196
|
+
function historyStartOrdinalOf(meta) {
|
|
197
|
+
const value = meta?.subagent_history_start_ordinal;
|
|
198
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
199
|
+
}
|
|
200
|
+
function isPlainObject(value) {
|
|
201
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
202
|
+
}
|
|
203
|
+
function cloneJson(value) {
|
|
204
|
+
return JSON.parse(JSON.stringify(value));
|
|
205
|
+
}
|
|
206
|
+
function isOptionalString(value) {
|
|
207
|
+
return value === undefined || typeof value === 'string';
|
|
208
|
+
}
|
|
209
|
+
function isStringArray(value) {
|
|
210
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
211
|
+
}
|
|
212
|
+
function isTokenCount(value) {
|
|
213
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
214
|
+
}
|
|
215
|
+
/** A valid `CodexTokenUsage` (copied), `null` for `null`, or `undefined` when invalid. Internal. */
|
|
216
|
+
export function codexTokenUsageFrom(value) {
|
|
217
|
+
if (value === null)
|
|
218
|
+
return null;
|
|
219
|
+
if (!isPlainObject(value))
|
|
220
|
+
return undefined;
|
|
221
|
+
if (!isTokenCount(value.input_tokens) || !isTokenCount(value.output_tokens))
|
|
222
|
+
return undefined;
|
|
223
|
+
const usage = {
|
|
224
|
+
input_tokens: value.input_tokens,
|
|
225
|
+
output_tokens: value.output_tokens,
|
|
226
|
+
};
|
|
227
|
+
for (const field of ['cached_input_tokens', 'reasoning_output_tokens', 'total_tokens']) {
|
|
228
|
+
const count = value[field];
|
|
229
|
+
if (count === undefined)
|
|
230
|
+
continue;
|
|
231
|
+
if (!isTokenCount(count))
|
|
232
|
+
return undefined;
|
|
233
|
+
usage[field] = count;
|
|
234
|
+
}
|
|
235
|
+
return usage;
|
|
236
|
+
}
|
|
237
|
+
/** Last `limit` items of an insertion-ordered collection, oldest first. */
|
|
238
|
+
function newest(items, size, limit) {
|
|
239
|
+
const count = limit >= 0 ? Math.min(size, Math.floor(limit)) : 0;
|
|
240
|
+
const kept = [];
|
|
241
|
+
let skip = size - count;
|
|
242
|
+
for (const item of items) {
|
|
243
|
+
if (kept.length >= count)
|
|
244
|
+
break;
|
|
245
|
+
if (skip > 0) {
|
|
246
|
+
skip -= 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
kept.push(item);
|
|
250
|
+
}
|
|
251
|
+
return kept;
|
|
252
|
+
}
|
|
174
253
|
/** Fixed per-entry cost added to each pending call's byte estimate. */
|
|
175
254
|
const PENDING_ENTRY_OVERHEAD_BYTES = 64;
|
|
176
255
|
/** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
|
|
@@ -235,6 +314,15 @@ export class CodexRolloutParser {
|
|
|
235
314
|
/** Byte budget for pending calls; `null` = unlimited (no accounting). */
|
|
236
315
|
maxPendingBytes;
|
|
237
316
|
metadataOnly;
|
|
317
|
+
includeSubagentActivity;
|
|
318
|
+
/**
|
|
319
|
+
* Set once a forked child's rollout embeds an ancestor's session_meta: its
|
|
320
|
+
* own `subagent_history_start_ordinal`, below which records are the
|
|
321
|
+
* parent's history and are skipped. `null` while nothing is skipped.
|
|
322
|
+
*/
|
|
323
|
+
historyStartOrdinal = null;
|
|
324
|
+
/** Multi-agent tool calls (call id → raw name), only with `includeSubagentActivity`. */
|
|
325
|
+
collaborationCalls = new Map();
|
|
238
326
|
/** Pending calls in insertion order, for the byte budget. */
|
|
239
327
|
pendingLedger = new Map();
|
|
240
328
|
pendingBytes = 0;
|
|
@@ -250,6 +338,7 @@ export class CodexRolloutParser {
|
|
|
250
338
|
? Math.floor(maxBytes)
|
|
251
339
|
: null;
|
|
252
340
|
this.metadataOnly = options.metadataOnly === true;
|
|
341
|
+
this.includeSubagentActivity = options.includeSubagentActivity === true;
|
|
253
342
|
}
|
|
254
343
|
/** Counters for the pending tool-call state and what the caps evicted. */
|
|
255
344
|
getPendingStats() {
|
|
@@ -282,13 +371,172 @@ export class CodexRolloutParser {
|
|
|
282
371
|
getLastRateLimits() {
|
|
283
372
|
return this.lastRateLimits;
|
|
284
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* In a forked subagent's rollout, the ordinal of its first own record:
|
|
376
|
+
* records with a lower `ordinal` are the parent history the fork embeds
|
|
377
|
+
* and produce no events. Known once the rollout's second line (an embedded
|
|
378
|
+
* ancestor `session_meta`) was parsed; `null` otherwise, and for every
|
|
379
|
+
* rollout that is not such a fork.
|
|
380
|
+
*/
|
|
381
|
+
getInheritedHistoryEnd() {
|
|
382
|
+
return this.historyStartOrdinal;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The state later lines depend on — session metadata, current model,
|
|
386
|
+
* previous cumulative token total, context window, rate limits, plan mode,
|
|
387
|
+
* and the tool-call correlation tables (newest `limit` entries each) — as
|
|
388
|
+
* JSON-serializable data. Eviction counters are not included.
|
|
389
|
+
*/
|
|
390
|
+
exportResumeState(limit = DEFAULT_RESUME_STATE_LIMIT) {
|
|
391
|
+
return cloneJson({
|
|
392
|
+
version: 1,
|
|
393
|
+
sessionMeta: this.sessionMeta,
|
|
394
|
+
currentModel: this.currentModel,
|
|
395
|
+
prevTotalUsage: this.prevTotalUsage,
|
|
396
|
+
lastTokenUsage: this.lastTokenUsage,
|
|
397
|
+
modelContextWindow: this.modelContextWindow,
|
|
398
|
+
lastRateLimits: this.lastRateLimits,
|
|
399
|
+
hasAggregateRateLimits: this.hasAggregateRateLimits,
|
|
400
|
+
inPlanMode: this.inPlanMode,
|
|
401
|
+
emittedToolUseIds: newest(this.emittedToolUseIds, this.emittedToolUseIds.size, limit),
|
|
402
|
+
patchExpandedToolUseIds: newest(this.patchExpandedToolUseIds, this.patchExpandedToolUseIds.size, limit),
|
|
403
|
+
pendingExecCommands: newest(this.pendingExecCommands.values(), this.pendingExecCommands.size, limit),
|
|
404
|
+
pendingMcpToolCalls: newest(this.pendingMcpToolCalls.values(), this.pendingMcpToolCalls.size, limit),
|
|
405
|
+
...(this.historyStartOrdinal !== null
|
|
406
|
+
? { inheritedHistoryEnd: this.historyStartOrdinal }
|
|
407
|
+
: {}),
|
|
408
|
+
...(this.collaborationCalls.size > 0
|
|
409
|
+
? {
|
|
410
|
+
collaborationCalls: newest(this.collaborationCalls, this.collaborationCalls.size, limit),
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Replaces this parser's state with one from {@link exportResumeState}, as
|
|
417
|
+
* if it had parsed the rollout up to where that state was taken. Returns
|
|
418
|
+
* false, changing nothing, when `state` is not a valid version-1 state.
|
|
419
|
+
* This parser's own `maxTrackedToolCalls`, `maxPendingBytes`, and
|
|
420
|
+
* `metadataOnly` options apply to the imported tables.
|
|
421
|
+
*/
|
|
422
|
+
importResumeState(state) {
|
|
423
|
+
if (!isPlainObject(state) || state.version !== 1)
|
|
424
|
+
return false;
|
|
425
|
+
const s = state;
|
|
426
|
+
const prevTotalUsage = codexTokenUsageFrom(s.prevTotalUsage);
|
|
427
|
+
const lastTokenUsage = codexTokenUsageFrom(s.lastTokenUsage);
|
|
428
|
+
if (prevTotalUsage === undefined || lastTokenUsage === undefined)
|
|
429
|
+
return false;
|
|
430
|
+
if (s.sessionMeta !== null && !isPlainObject(s.sessionMeta))
|
|
431
|
+
return false;
|
|
432
|
+
if (s.lastRateLimits !== null && !isPlainObject(s.lastRateLimits))
|
|
433
|
+
return false;
|
|
434
|
+
if (s.currentModel !== null && typeof s.currentModel !== 'string')
|
|
435
|
+
return false;
|
|
436
|
+
const contextWindow = s.modelContextWindow;
|
|
437
|
+
if (contextWindow !== null &&
|
|
438
|
+
!(typeof contextWindow === 'number' && Number.isFinite(contextWindow) && contextWindow > 0)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (typeof s.hasAggregateRateLimits !== 'boolean' || typeof s.inPlanMode !== 'boolean') {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (!isStringArray(s.emittedToolUseIds))
|
|
445
|
+
return false;
|
|
446
|
+
const patches = s.patchExpandedToolUseIds;
|
|
447
|
+
if (!Array.isArray(patches) ||
|
|
448
|
+
!patches.every((entry) => Array.isArray(entry) &&
|
|
449
|
+
entry.length === 2 &&
|
|
450
|
+
typeof entry[0] === 'string' &&
|
|
451
|
+
isStringArray(entry[1]))) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const execs = s.pendingExecCommands;
|
|
455
|
+
if (!Array.isArray(execs) ||
|
|
456
|
+
!execs.every((entry) => isPlainObject(entry) &&
|
|
457
|
+
typeof entry.call_id === 'string' &&
|
|
458
|
+
isStringArray(entry.command) &&
|
|
459
|
+
isOptionalString(entry.workdir) &&
|
|
460
|
+
typeof entry.timestamp === 'string')) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const mcps = s.pendingMcpToolCalls;
|
|
464
|
+
if (!Array.isArray(mcps) ||
|
|
465
|
+
!mcps.every((entry) => isPlainObject(entry) &&
|
|
466
|
+
typeof entry.call_id === 'string' &&
|
|
467
|
+
typeof entry.tool_name === 'string' &&
|
|
468
|
+
isOptionalString(entry.server_name) &&
|
|
469
|
+
(entry.arguments === undefined || isPlainObject(entry.arguments)) &&
|
|
470
|
+
typeof entry.timestamp === 'string')) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const inheritedEnd = s.inheritedHistoryEnd;
|
|
474
|
+
if (inheritedEnd !== undefined &&
|
|
475
|
+
inheritedEnd !== null &&
|
|
476
|
+
!(typeof inheritedEnd === 'number' && Number.isInteger(inheritedEnd) && inheritedEnd > 0)) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const collaborationCalls = s.collaborationCalls;
|
|
480
|
+
if (collaborationCalls !== undefined &&
|
|
481
|
+
!(Array.isArray(collaborationCalls) &&
|
|
482
|
+
collaborationCalls.every((entry) => Array.isArray(entry) &&
|
|
483
|
+
entry.length === 2 &&
|
|
484
|
+
typeof entry[0] === 'string' &&
|
|
485
|
+
typeof entry[1] === 'string'))) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const copy = cloneJson({
|
|
489
|
+
sessionMeta: s.sessionMeta,
|
|
490
|
+
lastRateLimits: s.lastRateLimits,
|
|
491
|
+
patches: patches,
|
|
492
|
+
execs: execs,
|
|
493
|
+
mcps: mcps,
|
|
494
|
+
});
|
|
495
|
+
this.reset();
|
|
496
|
+
this.sessionMeta = copy.sessionMeta;
|
|
497
|
+
this.historyStartOrdinal = typeof inheritedEnd === 'number' ? inheritedEnd : null;
|
|
498
|
+
this.currentModel = s.currentModel;
|
|
499
|
+
this.prevTotalUsage = prevTotalUsage;
|
|
500
|
+
this.lastTokenUsage = lastTokenUsage;
|
|
501
|
+
this.modelContextWindow = contextWindow;
|
|
502
|
+
this.lastRateLimits = copy.lastRateLimits;
|
|
503
|
+
this.hasAggregateRateLimits = s.hasAggregateRateLimits;
|
|
504
|
+
this.inPlanMode = s.inPlanMode;
|
|
505
|
+
for (const id of s.emittedToolUseIds) {
|
|
506
|
+
rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
|
|
507
|
+
}
|
|
508
|
+
for (const [callId, toolUseIds] of copy.patches)
|
|
509
|
+
this.putPendingPatch(callId, toolUseIds);
|
|
510
|
+
for (const pending of copy.execs) {
|
|
511
|
+
this.putPendingExec(this.metadataOnly
|
|
512
|
+
? { call_id: pending.call_id, command: [], timestamp: pending.timestamp }
|
|
513
|
+
: pending);
|
|
514
|
+
}
|
|
515
|
+
for (const pending of copy.mcps) {
|
|
516
|
+
this.putPendingMcp(this.metadataOnly ? { ...pending, arguments: undefined } : pending);
|
|
517
|
+
}
|
|
518
|
+
if (this.includeSubagentActivity && Array.isArray(collaborationCalls)) {
|
|
519
|
+
for (const [callId, name] of collaborationCalls) {
|
|
520
|
+
this.rememberCollaborationCall(callId, name);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
285
525
|
/**
|
|
286
526
|
* Converts a single rollout line to zero or more session events.
|
|
287
527
|
*/
|
|
288
528
|
convertLine(line) {
|
|
529
|
+
if (line.type === 'session_meta') {
|
|
530
|
+
return this.handleSessionMeta(line.timestamp, line.payload, line.ordinal);
|
|
531
|
+
}
|
|
532
|
+
// A forked subagent rollout embeds the parent's history before its own
|
|
533
|
+
// records; that history is the parent's, not the child's.
|
|
534
|
+
if (this.historyStartOrdinal !== null &&
|
|
535
|
+
typeof line.ordinal === 'number' &&
|
|
536
|
+
line.ordinal < this.historyStartOrdinal) {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
289
539
|
switch (line.type) {
|
|
290
|
-
case 'session_meta':
|
|
291
|
-
return this.handleSessionMeta(line.timestamp, line.payload);
|
|
292
540
|
case 'response_item':
|
|
293
541
|
return this.handleResponseItem(line.timestamp, line.payload);
|
|
294
542
|
case 'compacted':
|
|
@@ -304,6 +552,8 @@ export class CodexRolloutParser {
|
|
|
304
552
|
/** Reset all parser state. */
|
|
305
553
|
reset() {
|
|
306
554
|
this.sessionMeta = null;
|
|
555
|
+
this.historyStartOrdinal = null;
|
|
556
|
+
this.collaborationCalls.clear();
|
|
307
557
|
this.currentModel = null;
|
|
308
558
|
this.pendingExecCommands.clear();
|
|
309
559
|
this.pendingMcpToolCalls.clear();
|
|
@@ -353,6 +603,16 @@ export class CodexRolloutParser {
|
|
|
353
603
|
this.evictedByBytes += 1;
|
|
354
604
|
}
|
|
355
605
|
}
|
|
606
|
+
putPendingExec(pending) {
|
|
607
|
+
this.putPending(this.pendingExecCommands, pending.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
|
|
608
|
+
}
|
|
609
|
+
putPendingMcp(pending) {
|
|
610
|
+
this.putPending(this.pendingMcpToolCalls, pending.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, pending.timestamp) +
|
|
611
|
+
approxJsonBytes(pending.arguments));
|
|
612
|
+
}
|
|
613
|
+
putPendingPatch(callId, toolUseIds) {
|
|
614
|
+
this.putPending(this.patchExpandedToolUseIds, callId, toolUseIds, () => stringBytes(callId, ...toolUseIds));
|
|
615
|
+
}
|
|
356
616
|
/** Removes and returns a pending call. */
|
|
357
617
|
takePending(table, callId) {
|
|
358
618
|
const value = table.get(callId);
|
|
@@ -383,7 +643,25 @@ export class CodexRolloutParser {
|
|
|
383
643
|
return this.metadataOnly ? '' : content;
|
|
384
644
|
}
|
|
385
645
|
// --- Handlers ---
|
|
386
|
-
handleSessionMeta(timestamp, payload) {
|
|
646
|
+
handleSessionMeta(timestamp, payload, ordinal) {
|
|
647
|
+
if (this.sessionMeta) {
|
|
648
|
+
// Only the rollout's own (first) session_meta counts. A forked child
|
|
649
|
+
// embeds its parent's right after its own, followed by the parent's
|
|
650
|
+
// history up to the child's `subagent_history_start_ordinal`; from
|
|
651
|
+
// here on, records below that ordinal are skipped. (Children whose
|
|
652
|
+
// rollout has no embedded meta keep every record: in some, Codex
|
|
653
|
+
// recorded a start ordinal past their own records.)
|
|
654
|
+
const start = historyStartOrdinalOf(this.sessionMeta);
|
|
655
|
+
if (this.historyStartOrdinal === null &&
|
|
656
|
+
start !== null &&
|
|
657
|
+
typeof ordinal === 'number' &&
|
|
658
|
+
ordinal < start) {
|
|
659
|
+
this.historyStartOrdinal = start;
|
|
660
|
+
}
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
if (!payload || typeof payload !== 'object')
|
|
664
|
+
return [];
|
|
387
665
|
this.sessionMeta = payload;
|
|
388
666
|
const text = payload.base_instructions?.text?.trim();
|
|
389
667
|
return [
|
|
@@ -423,10 +701,91 @@ export class CodexRolloutParser {
|
|
|
423
701
|
return this.handleCustomToolCall(timestamp, payload);
|
|
424
702
|
case 'custom_tool_call_output':
|
|
425
703
|
return this.handleCustomToolCallOutput(timestamp, payload);
|
|
704
|
+
case 'agent_message':
|
|
705
|
+
return this.handleAgentMessage(timestamp, payload);
|
|
426
706
|
default:
|
|
427
707
|
return [];
|
|
428
708
|
}
|
|
429
709
|
}
|
|
710
|
+
// --- Subagent activity ---
|
|
711
|
+
rememberCollaborationCall(callId, name) {
|
|
712
|
+
this.collaborationCalls.delete(callId);
|
|
713
|
+
this.collaborationCalls.set(callId, name);
|
|
714
|
+
while (this.collaborationCalls.size > this.maxTrackedToolCalls) {
|
|
715
|
+
this.collaborationCalls.delete(this.collaborationCalls.keys().next().value);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
subagentActivityEvent(timestamp, id, activity) {
|
|
719
|
+
return {
|
|
720
|
+
type: 'system',
|
|
721
|
+
timestamp,
|
|
722
|
+
origin: { kind: SUBAGENT_ACTIVITY_ORIGIN_KIND },
|
|
723
|
+
message: {
|
|
724
|
+
role: 'system',
|
|
725
|
+
id: `subagent-activity:${id}`,
|
|
726
|
+
sourceLabel: 'subagent activity',
|
|
727
|
+
content: [{ type: SUBAGENT_ACTIVITY_BLOCK_TYPE, ...activity }],
|
|
728
|
+
},
|
|
729
|
+
providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** A child's `FINAL_ANSWER` message to its parent (opt-in). */
|
|
733
|
+
handleAgentMessage(timestamp, item) {
|
|
734
|
+
if (!this.includeSubagentActivity || !Array.isArray(item.content))
|
|
735
|
+
return [];
|
|
736
|
+
const first = item.content.find((part) => !!part && typeof part === 'object' && typeof part.text === 'string');
|
|
737
|
+
const parsed = parseCodexAgentMessageText(first?.text);
|
|
738
|
+
if (!parsed || parsed.messageType !== 'FINAL_ANSWER')
|
|
739
|
+
return [];
|
|
740
|
+
const agentPath = nonEmptyString(item.author) ?? parsed.sender;
|
|
741
|
+
const recipient = nonEmptyString(item.recipient) ?? parsed.taskName;
|
|
742
|
+
const text = this.metadataOnly ? undefined : nonEmptyString(parsed.payload.trim());
|
|
743
|
+
const activity = {
|
|
744
|
+
kind: 'final-answer',
|
|
745
|
+
...(agentPath ? { agentPath } : {}),
|
|
746
|
+
...(recipient ? { recipient } : {}),
|
|
747
|
+
...(text ? { text } : {}),
|
|
748
|
+
};
|
|
749
|
+
return [
|
|
750
|
+
this.subagentActivityEvent(timestamp, nonEmptyString(item.id) ?? `final-answer-${timestamp}-${agentPath ?? ''}`, activity),
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
/** A `SubAgentActivity` turn item (opt-in). */
|
|
754
|
+
handleItemCompleted(timestamp, event) {
|
|
755
|
+
if (!this.includeSubagentActivity)
|
|
756
|
+
return [];
|
|
757
|
+
const item = event.item;
|
|
758
|
+
if (!item || item.type !== 'SubAgentActivity')
|
|
759
|
+
return [];
|
|
760
|
+
const kind = typeof item.kind === 'string' ? item.kind : '';
|
|
761
|
+
if (!SUBAGENT_ACTIVITY_KINDS.has(kind))
|
|
762
|
+
return [];
|
|
763
|
+
const itemId = nonEmptyString(item.id);
|
|
764
|
+
const completedTurn = kind === 'completed' && itemId?.startsWith('subagent-completed-')
|
|
765
|
+
? nonEmptyString(itemId.slice('subagent-completed-'.length))
|
|
766
|
+
: undefined;
|
|
767
|
+
const callId = kind === 'completed' ? undefined : itemId;
|
|
768
|
+
const tool = callId ? this.collaborationCalls.get(callId) : undefined;
|
|
769
|
+
const fields = {
|
|
770
|
+
callId,
|
|
771
|
+
tool,
|
|
772
|
+
turnId: completedTurn,
|
|
773
|
+
agentThreadId: nonEmptyString(item.agent_thread_id),
|
|
774
|
+
agentPath: nonEmptyString(item.agent_path),
|
|
775
|
+
model: nonEmptyString(item.model),
|
|
776
|
+
reasoningEffort: nonEmptyString(item.reasoning_effort),
|
|
777
|
+
parentThreadId: nonEmptyString(event.thread_id),
|
|
778
|
+
parentTurnId: nonEmptyString(event.turn_id),
|
|
779
|
+
};
|
|
780
|
+
const activity = { kind: kind };
|
|
781
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
782
|
+
if (value !== undefined)
|
|
783
|
+
activity[key] = value;
|
|
784
|
+
}
|
|
785
|
+
return [
|
|
786
|
+
this.subagentActivityEvent(timestamp, itemId ?? `${kind}-${timestamp}-${activity.agentThreadId ?? ''}`, activity),
|
|
787
|
+
];
|
|
788
|
+
}
|
|
430
789
|
handleMessage(timestamp, item) {
|
|
431
790
|
const text = extractTextFromContent(item.content);
|
|
432
791
|
if (!text)
|
|
@@ -508,6 +867,11 @@ export class CodexRolloutParser {
|
|
|
508
867
|
const canonicalName = normalizeCodexToolName(item.name);
|
|
509
868
|
const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
|
|
510
869
|
rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
|
|
870
|
+
if (this.includeSubagentActivity &&
|
|
871
|
+
typeof item.call_id === 'string' &&
|
|
872
|
+
(item.namespace === 'collaboration' || COLLABORATION_TOOL_NAMES.has(item.name))) {
|
|
873
|
+
this.rememberCollaborationCall(item.call_id, item.name);
|
|
874
|
+
}
|
|
511
875
|
return [
|
|
512
876
|
{
|
|
513
877
|
type: 'assistant',
|
|
@@ -582,7 +946,7 @@ export class CodexRolloutParser {
|
|
|
582
946
|
if (filePaths.length === 0)
|
|
583
947
|
return [];
|
|
584
948
|
const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
|
|
585
|
-
this.
|
|
949
|
+
this.putPendingPatch(item.call_id, toolUseIds);
|
|
586
950
|
for (const id of toolUseIds) {
|
|
587
951
|
rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
|
|
588
952
|
}
|
|
@@ -732,7 +1096,7 @@ export class CodexRolloutParser {
|
|
|
732
1096
|
const pending = this.metadataOnly
|
|
733
1097
|
? { call_id: e.call_id, command: [], timestamp }
|
|
734
1098
|
: { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
|
|
735
|
-
this.
|
|
1099
|
+
this.putPendingExec(pending);
|
|
736
1100
|
return [];
|
|
737
1101
|
}
|
|
738
1102
|
case 'exec_command_end': {
|
|
@@ -795,8 +1159,7 @@ export class CodexRolloutParser {
|
|
|
795
1159
|
arguments: this.metadataOnly ? undefined : e.arguments,
|
|
796
1160
|
timestamp,
|
|
797
1161
|
};
|
|
798
|
-
this.
|
|
799
|
-
approxJsonBytes(pending.arguments));
|
|
1162
|
+
this.putPendingMcp(pending);
|
|
800
1163
|
return [];
|
|
801
1164
|
}
|
|
802
1165
|
case 'mcp_tool_call_end': {
|
|
@@ -965,6 +1328,8 @@ export class CodexRolloutParser {
|
|
|
965
1328
|
timestamp,
|
|
966
1329
|
},
|
|
967
1330
|
];
|
|
1331
|
+
case 'item_completed':
|
|
1332
|
+
return this.handleItemCompleted(timestamp, event);
|
|
968
1333
|
case 'turn_aborted':
|
|
969
1334
|
case 'agent_reasoning':
|
|
970
1335
|
case 'agent_message':
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @module parsers/openCodeParser
|
|
12
12
|
*/
|
|
13
13
|
import { normalizeProviderUsage } from '../usageNormalization';
|
|
14
|
+
import { ownValue } from '../ownKey';
|
|
14
15
|
/**
|
|
15
16
|
* OpenCode → Claude Code tool name mapping.
|
|
16
17
|
* OpenCode uses lowercase names; downstream code expects PascalCase.
|
|
@@ -46,7 +47,7 @@ export function normalizeToolName(name) {
|
|
|
46
47
|
if (!name)
|
|
47
48
|
return name;
|
|
48
49
|
// Check mapping (case-insensitive lookup via lowercase key)
|
|
49
|
-
const mapped = TOOL_NAME_MAP
|
|
50
|
+
const mapped = ownValue(TOOL_NAME_MAP, name.toLowerCase());
|
|
50
51
|
if (mapped)
|
|
51
52
|
return mapped;
|
|
52
53
|
// Already PascalCase (starts with uppercase)? Pass through.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a Claude Code subagent transcript ended cleanly, read line by line.
|
|
3
|
+
*
|
|
4
|
+
* Clean means: the last user or assistant line is an assistant line that
|
|
5
|
+
* stopped with `end_turn` or `stop_sequence`, and every `tool_use` in the
|
|
6
|
+
* transcript has its `tool_result`. A user line after that (a resumed agent's
|
|
7
|
+
* new prompt) makes it unfinished again.
|
|
8
|
+
*
|
|
9
|
+
* Pure. Internal to the subagent parsers.
|
|
10
|
+
*
|
|
11
|
+
* @module parsers/subagentEndState
|
|
12
|
+
*/
|
|
13
|
+
const ENDED_STOP_REASONS = new Set(['end_turn', 'stop_sequence']);
|
|
14
|
+
/** Bounds the unanswered-call set for pathological transcripts. */
|
|
15
|
+
const MAX_PENDING_TOOL_USES = 4096;
|
|
16
|
+
export class SubagentEndTracker {
|
|
17
|
+
unanswered = new Set();
|
|
18
|
+
lastWasCleanAssistant = false;
|
|
19
|
+
/** Feeds one parsed JSONL line, in file order. */
|
|
20
|
+
observe(raw) {
|
|
21
|
+
if (!raw || typeof raw !== 'object')
|
|
22
|
+
return;
|
|
23
|
+
const line = raw;
|
|
24
|
+
if (line.type !== 'assistant' && line.type !== 'user')
|
|
25
|
+
return;
|
|
26
|
+
const message = line.message && typeof line.message === 'object'
|
|
27
|
+
? line.message
|
|
28
|
+
: undefined;
|
|
29
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
30
|
+
if (line.type === 'user') {
|
|
31
|
+
for (const block of content) {
|
|
32
|
+
if (!block || typeof block !== 'object')
|
|
33
|
+
continue;
|
|
34
|
+
const typed = block;
|
|
35
|
+
if (typed.type === 'tool_result' && typeof typed.tool_use_id === 'string') {
|
|
36
|
+
this.unanswered.delete(typed.tool_use_id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.lastWasCleanAssistant = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let callsTool = false;
|
|
43
|
+
for (const block of content) {
|
|
44
|
+
if (!block || typeof block !== 'object')
|
|
45
|
+
continue;
|
|
46
|
+
const typed = block;
|
|
47
|
+
if (typed.type !== 'tool_use')
|
|
48
|
+
continue;
|
|
49
|
+
callsTool = true;
|
|
50
|
+
if (typeof typed.id === 'string' && this.unanswered.size < MAX_PENDING_TOOL_USES) {
|
|
51
|
+
this.unanswered.add(typed.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const stop = typeof message?.stop_reason === 'string' ? message.stop_reason : '';
|
|
55
|
+
this.lastWasCleanAssistant = !callsTool && ENDED_STOP_REASONS.has(stop);
|
|
56
|
+
}
|
|
57
|
+
/** Whether the lines so far end cleanly. */
|
|
58
|
+
endedCleanly() {
|
|
59
|
+
return this.lastWasCleanAssistant && this.unanswered.size === 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -12,28 +12,50 @@ import * as path from 'path';
|
|
|
12
12
|
import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
13
13
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
14
14
|
import { cachedSubagentStats } from './subagentStatsCache';
|
|
15
|
+
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
16
|
+
import { SubagentEndTracker } from './subagentEndState';
|
|
15
17
|
/**
|
|
16
18
|
* Pattern for matching subagent JSONL files.
|
|
17
19
|
* Files are named like: agent-<hash>.jsonl
|
|
18
20
|
*/
|
|
19
21
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
22
|
+
function metaString(value) {
|
|
23
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
24
|
+
}
|
|
20
25
|
/**
|
|
21
26
|
* Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
|
|
22
|
-
* for the spawn's agent type
|
|
23
|
-
* yield an empty object
|
|
27
|
+
* for the spawn's agent type, description, spawning tool_use id, parent agent,
|
|
28
|
+
* and completion flags. Missing or malformed files yield an empty object;
|
|
29
|
+
* wrongly typed fields are left out.
|
|
24
30
|
*/
|
|
25
31
|
export function readAgentMeta(agentFilePath) {
|
|
26
32
|
try {
|
|
27
33
|
const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
|
|
28
34
|
const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
36
|
+
return {};
|
|
37
|
+
const record = parsed;
|
|
38
|
+
const meta = {};
|
|
39
|
+
const agentType = metaString(record.agentType);
|
|
40
|
+
if (agentType)
|
|
41
|
+
meta.agentType = agentType;
|
|
42
|
+
const description = metaString(record.description);
|
|
43
|
+
if (description)
|
|
44
|
+
meta.description = description;
|
|
45
|
+
const toolUseId = metaString(record.toolUseId);
|
|
46
|
+
if (toolUseId)
|
|
47
|
+
meta.toolUseId = toolUseId;
|
|
48
|
+
const parentAgentId = metaString(record.parentAgentId);
|
|
49
|
+
if (parentAgentId)
|
|
50
|
+
meta.parentAgentId = parentAgentId;
|
|
51
|
+
const requestShape = metaString(record.requestShape);
|
|
52
|
+
if (requestShape)
|
|
53
|
+
meta.requestShape = requestShape;
|
|
54
|
+
if (typeof record.finished === 'boolean')
|
|
55
|
+
meta.finished = record.finished;
|
|
56
|
+
if (typeof record.stoppedByUser === 'boolean')
|
|
57
|
+
meta.stoppedByUser = record.stoppedByUser;
|
|
58
|
+
return meta;
|
|
37
59
|
}
|
|
38
60
|
catch {
|
|
39
61
|
return {};
|
|
@@ -139,6 +161,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
139
161
|
let description;
|
|
140
162
|
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
141
163
|
const usageDeduper = new ClaudeUsageDeduper();
|
|
164
|
+
const endTracker = new SubagentEndTracker();
|
|
142
165
|
let startTime;
|
|
143
166
|
let endTime;
|
|
144
167
|
// Task tracking state
|
|
@@ -155,6 +178,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
155
178
|
if (!startTime)
|
|
156
179
|
startTime = eventTimestamp;
|
|
157
180
|
endTime = eventTimestamp;
|
|
181
|
+
endTracker.observe(event);
|
|
158
182
|
// Extract token usage from assistant messages. Split lines of one
|
|
159
183
|
// response repeat its usage, so count each response once.
|
|
160
184
|
const usage = dedupedRawClaudeUsage(event, usageDeduper);
|
|
@@ -208,8 +232,8 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
toolCalls.push(toolCall);
|
|
211
|
-
// If this is
|
|
212
|
-
if (toolUse.name
|
|
235
|
+
// If this is an Agent/Task call (spawning another subagent), extract info
|
|
236
|
+
if (isSubagentSpawnTool(toolUse.name) && toolUse.input) {
|
|
213
237
|
if (toolUse.input.subagent_type && !agentType) {
|
|
214
238
|
agentType = String(toolUse.input.subagent_type);
|
|
215
239
|
}
|
|
@@ -277,6 +301,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
277
301
|
startTime,
|
|
278
302
|
endTime,
|
|
279
303
|
durationMs,
|
|
304
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
280
305
|
};
|
|
281
306
|
}
|
|
282
307
|
return null;
|