Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/trace-analysts.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ Returned directories from another resumed Claude session are parsed and included
The OpenInference file is the shared input for external engines.
The original trace and exact cited span remain available for review.

## Codex tool outcomes

Both function and custom tool outputs use the same status parser.
Explicit exit codes, error flags, and runner headers determine tool status.
Explicit failure takes precedence when structured fields conflict.
Text such as `error:`, `ENOENT`, or `command failed` inside stdout does not determine status.
An unmatched call, running process, or output without trustworthy status remains `UNSET`.
A recorded `wait_agent` timeout is a completed poll, not proof that the agent finished.
Tool output remains in `output.value`, with the existing size limit and truncation receipt.

Error and retry counts use explicit failures.
Successful follow-up counts require explicit success; unknown follow-up outcomes remain `null` in the detailed report.
The runtime span projection leaves its optional status absent for `UNSET` spans.
Use the execution report for terminal run outcomes.
The older runtime store's required run status cannot represent unknown and is not completion evidence.

## External engines

External engines are optional tools that you install separately.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/traces",
"version": "0.13.0",
"version": "0.13.1",
"description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
"type": "module",
"license": "MIT",
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/codex-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export interface CodexLine {
input?: unknown
call_id?: string
output?: unknown
is_error?: boolean
isError?: boolean
error?: unknown
event_id?: string
turn_id?: string
occurred_at_ms?: number
Expand Down
72 changes: 49 additions & 23 deletions src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ function textOf(content: unknown): string {
}

function numericStatus(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) return Number(value)
if (typeof value === 'number' && Number.isSafeInteger(value)) return value
if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) {
const code = Number(value)
if (Number.isSafeInteger(code)) return code
}
return undefined
}

Expand All @@ -120,22 +123,29 @@ function explicitOutputError(value: unknown, timeoutIsError = true): boolean | u

if (value && typeof value === 'object') {
const row = value as Record<string, unknown>
let observedSuccess = false
for (const key of ['is_error', 'isError', 'error']) {
if (row[key] === true) return true
if (row[key] === false) observedSuccess = true
}
for (const key of ['exit_code', 'exitCode']) {
const code = numericStatus(row[key])
if (code !== undefined && ('output' in row || 'chunk_id' in row || 'wall_time_seconds' in row)) {
return code !== 0
}
if (code !== undefined && code !== 0) return true
if (code === 0) observedSuccess = true
}
for (const key of ['timed_out', 'timedOut']) {
if (row[key] === true && timeoutIsError) return true
}
if (typeof row.succeeded === 'boolean' && ('value' in row || 'error' in row)) {
return !row.succeeded
if (!row.succeeded) return true
observedSuccess = true
}
if ((row.type === 'input_text' || row.type === 'text') && typeof row.text === 'string') {
return explicitOutputError(row.text, timeoutIsError)
const status = explicitOutputError(row.text, timeoutIsError)
if (status === true) return true
if (status === false) observedSuccess = true
}
return undefined
return observedSuccess ? false : undefined
}

if (typeof value !== 'string') return undefined
Expand All @@ -151,16 +161,16 @@ function explicitOutputError(value: unknown, timeoutIsError = true): boolean | u
}
const outputStart = text.indexOf('\nOutput:\n')
const header = outputStart >= 0 ? text.slice(0, outputStart) : text
if (/^Chunk ID:/i.test(header)) {
const exitCode = header.match(/^Process exited with code\s+(-?\d+)\b/im)?.[1]
if (exitCode !== undefined) return Number(exitCode) !== 0
if (/^(?:Chunk ID:|Process exited with code )/i.test(header)) {
const exitCode = numericStatus(header.match(/^Process exited with code[ \t]+(-?\d+)[ \t]*$/im)?.[1])
if (exitCode !== undefined) return exitCode !== 0
}
if (/^Script completed\s*\nWall time:/i.test(header)) return false
if (/^Script failed\s*\nWall time:/i.test(header)) return true
const scriptExitCode = header.match(/^Script error:\s*\nExit code:\s*(-?\d+)\b/im)?.[1]
if (scriptExitCode !== undefined) return Number(scriptExitCode) !== 0
const commandExitCode = text.match(/^Command failed with exit code\s+(-?\d+)\.?$/i)?.[1]
if (commandExitCode !== undefined) return Number(commandExitCode) !== 0
const scriptExitCode = numericStatus(header.match(/^Script error:[ \t]*\r?\nExit code:[ \t]*(-?\d+)[ \t]*(?:\r?\n|$)/i)?.[1])
if (scriptExitCode !== undefined) return scriptExitCode !== 0
const commandExitCode = numericStatus(text.match(/^Command failed with exit code[ \t]+(-?\d+)\.?$/i)?.[1])
if (commandExitCode !== undefined) return commandExitCode !== 0
if (/^<tool_error>[\s\S]*<\/tool_error>$/i.test(text)) return true
return undefined
}
Expand Down Expand Up @@ -192,14 +202,29 @@ function isWaitAgentOperation(name: string): boolean {
}

/** Only protocol-level status fields count; arbitrary tool output may itself contain code or logs mentioning errors. */
function outputStatus(name: string, output: unknown): { error: boolean; message: string; pollOutcome?: 'timeout' } {
function outputStatus(
name: string,
source: NonNullable<CodexLine['payload']>,
): { status: OtlpSpan['status']; pollOutcome?: 'timeout' } {
const output = source.output
const waitAgent = isWaitAgentOperation(name)
const error = explicitOutputError(output, !waitAgent) === true
const message = typeof output === 'string' ? output : JSON.stringify(output ?? '')
// Error metadata on the source envelope is authoritative. An `error` string
// inside arbitrary output remains domain data, not execution status.
const sourceError = source.is_error === true || source.isError === true
|| (source.error !== undefined && source.error !== null && source.error !== false)
const outputError = explicitOutputError(output, !waitAgent)
const error = sourceError || outputError === true
const pollTimeout = !error && waitAgent && explicitTimeout(output)
const code = error ? 'ERROR'
: outputError === false || source.is_error === false || source.isError === false || pollTimeout ? 'OK'
: 'UNSET'
const message = sourceError ? source.error ?? output : output
return {
error,
message: error ? message.slice(0, 500) : '',
...(!error && waitAgent && explicitTimeout(output) ? { pollOutcome: 'timeout' as const } : {}),
status: {
code,
...(error ? { message: (typeof message === 'string' ? message : JSON.stringify(message ?? '')).slice(0, 500) } : {}),
},
...(pollTimeout ? { pollOutcome: 'timeout' as const } : {}),
}
}

Expand Down Expand Up @@ -782,6 +807,7 @@ export class CodexAdapter implements HarnessTraceAdapter {
name: `tool.${name}`,
kind: 'TOOL',
startTime: ts,
status: 'UNSET',
service: SERVICE,
agent: SERVICE,
tool: name,
Expand Down Expand Up @@ -818,9 +844,9 @@ export class CodexAdapter implements HarnessTraceAdapter {
const t = toolByCallId.get(l.payload.call_id ?? '')
if (t) {
const name = String(t.attributes['tool.name'] ?? '')
const { error, message, pollOutcome } = outputStatus(name, l.payload.output)
const { status, pollOutcome } = outputStatus(name, l.payload)
closeSpanAt(t, ts)
t.status = error ? { code: 'ERROR', message } : { code: 'OK' }
t.status = status
if (pollOutcome) t.attributes['traces.poll.outcome'] = pollOutcome
recordToolOutput(t, l.payload.output)
const operation = t.attributes['traces.codex.agent_operation']
Expand Down
7 changes: 4 additions & 3 deletions src/failure-followup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface FailureFollowUp {
/** Null when the tool was never called again after the failure. */
followUpSpanId: string | null
kind: FollowUpKind
/** Whether the follow-up call ended without an error; null when kind is 'none'. */
/** Captured follow-up outcome; null when absent or unknown. */
followUpSucceeded: boolean | null
}

Expand All @@ -44,7 +44,7 @@ export interface FailureFollowUpReport {
adapted: number
/** Arguments were not captured or not comparable on one side of the pair. */
argsUnknown: number
/** Follow-ups that ended without an error, out of `followed`. */
/** Follow-ups with explicit success, out of `followed`. */
followUpSucceeded: number
/** Blind-retry count per tool, largest offender first when rendered. */
blindByTool: Record<string, number>
Expand Down Expand Up @@ -122,7 +122,8 @@ export function classifyFailureFollowUps(spans: readonly OtlpSpan[]): FailureFol
failedSpanId: call.span.span_id,
followUpSpanId: followUp?.span.span_id ?? null,
kind,
followUpSucceeded: followUp ? followUp.span.status.code !== 'ERROR' : null,
followUpSucceeded: !followUp || followUp.span.status.code === 'UNSET'
? null : followUp.span.status.code === 'OK',
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/runtime-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export async function toRuntimeStore(spans: readonly OtlpSpan[]): Promise<Runtim
name: s.name,
startedAt,
endedAt,
status: s.status.code === 'ERROR' ? ('error' as const) : ('ok' as const),
status: s.status.code === 'ERROR' ? ('error' as const)
: s.status.code === 'OK' ? ('ok' as const) : undefined,
error: s.status.message,
attributes,
}
Expand Down
20 changes: 10 additions & 10 deletions tests/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2147,9 +2147,9 @@ describe('codex current tool and subagent events', () => {
expect(tools).toHaveLength(9)
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:read-source'))?.status).toEqual({ code: 'OK' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:failed-command'))?.status.code).toBe('ERROR')
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:domain-result'))?.status).toEqual({ code: 'OK' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:captured-log'))?.status).toEqual({ code: 'OK' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:captured-exit'))?.status).toEqual({ code: 'OK' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:domain-result'))?.status).toEqual({ code: 'UNSET' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:captured-log'))?.status).toEqual({ code: 'UNSET' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:captured-exit'))?.status).toEqual({ code: 'UNSET' })
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:timed-out'))?.status.code).toBe('ERROR')
expect(tools.find((item) => item.span_id === codexSpanId('codex-tool-status', 'tool:poll-timed-out'))).toMatchObject({
status: { code: 'OK' },
Expand Down Expand Up @@ -2206,7 +2206,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:04.200Z',
payload: { type: 'custom_tool_call_output', call_id: 'custom-2', output: 'Script completed' },
payload: { type: 'custom_tool_call_output', call_id: 'custom-2', output: 'Script completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2221,7 +2221,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:04.400Z',
payload: { type: 'custom_tool_call_output', call_id: 'custom-3', output: 'Script completed' },
payload: { type: 'custom_tool_call_output', call_id: 'custom-3', output: 'Script completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2236,7 +2236,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:04.600Z',
payload: { type: 'custom_tool_call_output', call_id: 'custom-4', output: 'Script completed' },
payload: { type: 'custom_tool_call_output', call_id: 'custom-4', output: 'Script completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2251,7 +2251,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:04.800Z',
payload: { type: 'function_call_output', call_id: 'blocking-1', output: 'Completed' },
payload: { type: 'function_call_output', call_id: 'blocking-1', output: 'Completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2266,7 +2266,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:05.000Z',
payload: { type: 'function_call_output', call_id: 'domain-wait-1', output: 'Completed' },
payload: { type: 'function_call_output', call_id: 'domain-wait-1', output: 'Completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2281,7 +2281,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:05.200Z',
payload: { type: 'custom_tool_call_output', call_id: 'malformed-input-1', output: 'Completed' },
payload: { type: 'custom_tool_call_output', call_id: 'malformed-input-1', output: 'Completed', is_error: false },
},
{
type: 'response_item',
Expand All @@ -2296,7 +2296,7 @@ describe('codex current tool and subagent events', () => {
{
type: 'response_item',
timestamp: '2026-07-11T09:00:05.400Z',
payload: { type: 'custom_tool_call_output', call_id: 'write-stdin-1', output: 'Completed' },
payload: { type: 'custom_tool_call_output', call_id: 'write-stdin-1', output: 'Completed', is_error: false },
},
{
type: 'event_msg',
Expand Down
Loading