@wix/pathgrade 1.0.10 → 1.0.11

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.
@@ -19,9 +19,12 @@ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
19
19
  import type { AgentTurnResult } from '../../types.js';
20
20
  import type { AskUserAnswerStore } from './ask-user-answer-store.js';
21
21
  import type { ClaudeDeniedMcpEventStore } from './denied-mcp-event-store.js';
22
+ import { type ClaudeSdkMessageTiming } from './tool-results.js';
22
23
  export interface ProjectTurnInput {
23
24
  /** Buffered typed-message stream from one `query()` call. */
24
25
  messages: SDKMessage[];
26
+ /** Local receive timing captured while consuming each corresponding SDK message. */
27
+ messageTimings?: readonly (ClaudeSdkMessageTiming | undefined)[];
25
28
  /** Forwarded onto `ToolEvent.turnNumber`; optional for projector unit use. */
26
29
  turnNumber?: number;
27
30
  /** First user message of the turn — used for slash-command skill detection. */
@@ -39,6 +42,8 @@ export interface ProjectTurnInput {
39
42
  mcpServerNames?: readonly string[];
40
43
  /** Policy-denied MCP events recorded by the per-turn permission bridge. */
41
44
  deniedMcpEvents?: ClaudeDeniedMcpEventStore;
45
+ /** Exact runtime credential values to redact before tool results become trace data. */
46
+ sensitiveValues?: readonly string[];
42
47
  }
43
48
  export interface ProjectedTurn {
44
49
  result: AgentTurnResult;
@@ -23,6 +23,7 @@ const SDK_ERROR_SUBTYPES = [
23
23
  ];
24
24
  import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
25
25
  import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
26
+ import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
26
27
  export function projectSdkMessages(input) {
27
28
  let sessionId;
28
29
  let initSkills;
@@ -37,7 +38,9 @@ export function projectSdkMessages(input) {
37
38
  let costUsd;
38
39
  let errorSubtype;
39
40
  const toolEvents = [];
40
- for (const msg of input.messages) {
41
+ const pendingTools = new Map();
42
+ for (const [messageIndex, msg] of input.messages.entries()) {
43
+ const timing = input.messageTimings?.[messageIndex];
41
44
  switch (msg.type) {
42
45
  case 'system': {
43
46
  const sid = msg.session_id;
@@ -60,13 +63,30 @@ export function projectSdkMessages(input) {
60
63
  continue;
61
64
  }
62
65
  if (block.type === 'tool_use') {
63
- const event = buildToolEvent(block, input.turnNumber, input.answerStore, input.mcpServerNames ?? [], input.deniedMcpEvents);
64
- if (event)
66
+ const event = buildToolEvent(block, input.turnNumber, input.answerStore, input.mcpServerNames ?? [], input.deniedMcpEvents, timing);
67
+ if (event) {
65
68
  toolEvents.push(event);
69
+ if (event.toolUseId) {
70
+ pendingTools.set(event.toolUseId, {
71
+ event,
72
+ startedMonotonicMs: timing?.receivedMonotonicMs,
73
+ });
74
+ }
75
+ }
66
76
  }
67
77
  }
68
78
  break;
69
79
  }
80
+ case 'user': {
81
+ for (const observed of extractObservedToolResults(msg, timing)) {
82
+ const pending = pendingTools.get(observed.toolUseId);
83
+ if (!pending)
84
+ continue;
85
+ applyObservedToolResult(pending.event, observed, pending.startedMonotonicMs, input.sensitiveValues);
86
+ pendingTools.delete(observed.toolUseId);
87
+ }
88
+ break;
89
+ }
70
90
  case 'result': {
71
91
  const r = msg;
72
92
  if (r.session_id)
@@ -167,7 +187,7 @@ function prependSlashCommandSkillEvent(events, firstMessage, initSkills) {
167
187
  ...events,
168
188
  ];
169
189
  }
170
- function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMcpEvents) {
190
+ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMcpEvents, timing) {
171
191
  const providerToolName = String(block.name || 'unknown');
172
192
  const rawInput = block.input ?? undefined;
173
193
  const toolUseId = typeof block.id === 'string' ? block.id : undefined;
@@ -180,15 +200,18 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
180
200
  ...(rawInput ?? {}),
181
201
  server: mcpTool.server,
182
202
  tool: mcpTool.tool,
183
- status: 'completed',
203
+ status: 'incomplete',
184
204
  };
185
205
  return {
186
206
  action: 'mcp_tool_call',
187
207
  provider: 'claude',
188
208
  providerToolName: normalizedProviderToolName,
209
+ ...(toolUseId ? { toolUseId } : {}),
189
210
  turnNumber,
190
211
  arguments: args,
191
- summary: `MCP tool ${normalizedProviderToolName} completed`,
212
+ status: 'incomplete',
213
+ ...(timing ? { startedAt: timing.receivedAt } : {}),
214
+ summary: `MCP tool ${normalizedProviderToolName}`,
192
215
  confidence: 'high',
193
216
  rawSnippet: JSON.stringify(block).slice(0, 200),
194
217
  };
@@ -203,8 +226,11 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
203
226
  action,
204
227
  provider: 'claude',
205
228
  providerToolName,
229
+ ...(toolUseId ? { toolUseId } : {}),
206
230
  turnNumber,
207
231
  arguments: args,
232
+ status: 'incomplete',
233
+ ...(timing ? { startedAt: timing.receivedAt } : {}),
208
234
  summary,
209
235
  confidence: 'high',
210
236
  rawSnippet,
@@ -0,0 +1,19 @@
1
+ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import type { ToolEvent } from '../../tool-events.js';
3
+ export declare const TOOL_RESULT_MAX_CHARS: number;
4
+ export interface ClaudeSdkMessageTiming {
5
+ receivedAt: string;
6
+ receivedMonotonicMs: number;
7
+ }
8
+ export interface ObservedToolResult {
9
+ toolUseId: string;
10
+ isError: boolean;
11
+ structuredResult?: Record<string, unknown>;
12
+ content?: string;
13
+ completedAt?: string;
14
+ completedMonotonicMs?: number;
15
+ }
16
+ export declare function hasToolLifecycleBoundary(message: SDKMessage): boolean;
17
+ export declare function extractObservedToolResults(message: SDKMessage, timing?: ClaudeSdkMessageTiming): ObservedToolResult[];
18
+ export declare function applyObservedToolResult(event: ToolEvent, observed: ObservedToolResult, startedMonotonicMs?: number, sensitiveValues?: readonly string[]): void;
19
+ export declare function collectSensitiveEnvValues(env: Readonly<Record<string, string>>): string[];
@@ -0,0 +1,117 @@
1
+ export const TOOL_RESULT_MAX_CHARS = 64 * 1024;
2
+ const SECRET_ENV_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)/i;
3
+ export function hasToolLifecycleBoundary(message) {
4
+ if (message.type !== 'assistant' && message.type !== 'user')
5
+ return false;
6
+ const content = message.message?.content;
7
+ return Array.isArray(content) && content.some((block) => isRecord(block) && (block.type === 'tool_use' || block.type === 'tool_result'));
8
+ }
9
+ export function extractObservedToolResults(message, timing) {
10
+ if (message.type !== 'user')
11
+ return [];
12
+ const user = message;
13
+ const content = user.message?.content;
14
+ if (!Array.isArray(content))
15
+ return [];
16
+ const blocks = content.filter(isToolResultBlock);
17
+ const structuredResult = blocks.length === 1 && isRecord(user.tool_use_result)
18
+ ? user.tool_use_result
19
+ : undefined;
20
+ return blocks.flatMap((block) => {
21
+ if (typeof block.tool_use_id !== 'string')
22
+ return [];
23
+ const text = extractTextContent(block.content);
24
+ return [{
25
+ toolUseId: block.tool_use_id,
26
+ isError: block.is_error === true,
27
+ ...(structuredResult ? { structuredResult } : {}),
28
+ ...(text === undefined ? {} : { content: text }),
29
+ ...(timing ? {
30
+ completedAt: timing.receivedAt,
31
+ completedMonotonicMs: timing.receivedMonotonicMs,
32
+ } : {}),
33
+ }];
34
+ });
35
+ }
36
+ export function applyObservedToolResult(event, observed, startedMonotonicMs, sensitiveValues = []) {
37
+ const remainsBackgrounded = event.arguments?.run_in_background === true;
38
+ const result = buildBoundedToolResult(observed, sensitiveValues);
39
+ const failedExit = result.exitCode !== undefined && result.exitCode !== 0;
40
+ const failed = observed.isError || failedExit;
41
+ event.status = failed ? 'error' : (remainsBackgrounded ? 'incomplete' : 'completed');
42
+ if (!remainsBackgrounded || failed) {
43
+ if (observed.completedAt)
44
+ event.completedAt = observed.completedAt;
45
+ if (startedMonotonicMs !== undefined && observed.completedMonotonicMs !== undefined) {
46
+ event.durationMs = Math.max(0, observed.completedMonotonicMs - startedMonotonicMs);
47
+ }
48
+ }
49
+ if (Object.keys(result).length > 0)
50
+ event.result = result;
51
+ if (event.action === 'mcp_tool_call' && event.arguments) {
52
+ event.arguments.status = event.status;
53
+ }
54
+ }
55
+ export function collectSensitiveEnvValues(env) {
56
+ return [...new Set(Object.entries(env)
57
+ .filter(([key, value]) => SECRET_ENV_KEY_PATTERN.test(key) && value.length > 0)
58
+ .map(([, value]) => value))]
59
+ .sort((a, b) => b.length - a.length);
60
+ }
61
+ function buildBoundedToolResult(observed, sensitiveValues) {
62
+ const raw = observed.structuredResult;
63
+ const result = {};
64
+ let truncated = false;
65
+ const addBounded = (key, value) => {
66
+ if (typeof value !== 'string')
67
+ return;
68
+ const bounded = boundText(redactSensitiveValues(value, sensitiveValues));
69
+ result[key] = bounded.value;
70
+ truncated ||= bounded.truncated;
71
+ };
72
+ addBounded('stdout', raw?.stdout);
73
+ addBounded('stderr', raw?.stderr);
74
+ if (observed.content !== raw?.stdout)
75
+ addBounded('content', observed.content);
76
+ const exitCode = raw?.exitCode ?? raw?.exit_code;
77
+ if (typeof exitCode === 'number' && Number.isFinite(exitCode))
78
+ result.exitCode = exitCode;
79
+ if (truncated)
80
+ result.truncated = true;
81
+ return result;
82
+ }
83
+ function redactSensitiveValues(value, sensitiveValues) {
84
+ let redacted = value;
85
+ for (const secret of sensitiveValues) {
86
+ if (secret.length > 0)
87
+ redacted = redacted.split(secret).join('<redacted>');
88
+ }
89
+ return redacted;
90
+ }
91
+ function boundText(value) {
92
+ if (value.length <= TOOL_RESULT_MAX_CHARS)
93
+ return { value, truncated: false };
94
+ const marker = '\n[truncated by PathGrade]';
95
+ return {
96
+ value: `${value.slice(0, TOOL_RESULT_MAX_CHARS - marker.length)}${marker}`,
97
+ truncated: true,
98
+ };
99
+ }
100
+ function extractTextContent(content) {
101
+ if (typeof content === 'string')
102
+ return content;
103
+ if (!Array.isArray(content))
104
+ return undefined;
105
+ const text = content
106
+ .filter(isRecord)
107
+ .filter((block) => block.type === 'text' && typeof block.text === 'string')
108
+ .map((block) => block.text)
109
+ .join('');
110
+ return text || undefined;
111
+ }
112
+ function isToolResultBlock(value) {
113
+ return isRecord(value) && value.type === 'tool_result';
114
+ }
115
+ function isRecord(value) {
116
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
117
+ }
@@ -39,6 +39,10 @@ export interface ClaudeAgentDeps {
39
39
  envExecutable?: string;
40
40
  /** Optional macOS sandbox-exec profile. None today; preserves the seam. */
41
41
  sandboxProfile?: string;
42
+ /** Local wall-clock seam for deterministic tool lifecycle tests. */
43
+ now?: () => Date;
44
+ /** Monotonic clock seam for deterministic tool duration tests. */
45
+ monotonicNow?: () => number;
42
46
  }
43
47
  export interface ClaudeAgentOptions {
44
48
  /**
@@ -22,11 +22,13 @@
22
22
  * parser wholesale.
23
23
  */
24
24
  import { query as sdkQuery, } from '@anthropic-ai/claude-agent-sdk';
25
+ import { performance } from 'node:perf_hooks';
25
26
  import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
26
27
  import { createSandboxedClaudeSpawn } from '../providers/sandboxed-claude-spawn.js';
27
28
  import { assertClaudeLiveMcpSafetyPreflight, assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
28
29
  import { buildClaudeSdkOptions, resolveClaudeCodeExecutable, } from './claude/sdk-options.js';
29
30
  import { projectSdkMessages } from './claude/sdk-message-projector.js';
31
+ import { collectSensitiveEnvValues, hasToolLifecycleBoundary, } from './claude/tool-results.js';
30
32
  import { createAskUserAnswerStore } from './claude/ask-user-answer-store.js';
31
33
  import { createClaudeToolPermissionBridge } from './claude/tool-permission-bridge.js';
32
34
  import { createClaudeDeniedMcpEventStore } from './claude/denied-mcp-event-store.js';
@@ -63,6 +65,8 @@ export class ClaudeAgent extends BaseAgent {
63
65
  const platform = this.deps.platform ?? process.platform;
64
66
  const hostEnv = this.deps.hostEnv ?? process.env;
65
67
  const envExecutable = this.deps.envExecutable ?? process.env.PATHGRADE_CLAUDE_CODE_EXECUTABLE;
68
+ const runtimeEnv = getRuntimeEnv(runtime);
69
+ const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
66
70
  const sandboxedSpawn = createSandboxedClaudeSpawn({
67
71
  platform,
68
72
  hostEnv,
@@ -75,7 +79,7 @@ export class ClaudeAgent extends BaseAgent {
75
79
  const mcpMountOptions = {
76
80
  workspacePath,
77
81
  mcpConfigPath: sessionOptions?.mcpConfigPath,
78
- runtimeEnv: getRuntimeEnv(runtime),
82
+ runtimeEnv,
79
83
  };
80
84
  await assertClaudeLiveMcpSafetyPreflight({
81
85
  ...mcpMountOptions,
@@ -117,7 +121,7 @@ export class ClaudeAgent extends BaseAgent {
117
121
  workspacePath,
118
122
  spawnClaudeCodeProcess: sandboxedSpawn,
119
123
  canUseTool: bridge,
120
- runtimeEnv: getRuntimeEnv(runtime),
124
+ runtimeEnv,
121
125
  model: sessionOptions?.model,
122
126
  claudeCodeExecutable,
123
127
  resume: priorSessionId,
@@ -125,9 +129,18 @@ export class ClaudeAgent extends BaseAgent {
125
129
  abortController: createLinkedAbortController(getTurnAbortSignal(sessionOptions)),
126
130
  });
127
131
  const messages = [];
132
+ const messageTimings = [];
133
+ const now = this.deps.now ?? (() => new Date());
134
+ const monotonicNow = this.deps.monotonicNow ?? (() => performance.now());
128
135
  const stream = queryFn({ prompt: message, options: sdkOptions });
129
136
  for await (const msg of stream) {
130
137
  messages.push(msg);
138
+ messageTimings.push(hasToolLifecycleBoundary(msg)
139
+ ? {
140
+ receivedAt: now().toISOString(),
141
+ receivedMonotonicMs: monotonicNow(),
142
+ }
143
+ : undefined);
131
144
  }
132
145
  // The legacy NDJSON parser only synthesized the slash-command
133
146
  // `use_skill` event from the *opening* user message. The Claude
@@ -140,11 +153,13 @@ export class ClaudeAgent extends BaseAgent {
140
153
  const projectorFirstMessage = turnNumber === 1 ? message : undefined;
141
154
  const projected = projectSdkMessages({
142
155
  messages,
156
+ messageTimings,
143
157
  turnNumber,
144
158
  firstMessage: projectorFirstMessage,
145
159
  answerStore,
146
160
  mcpServerNames: mcpServers ? Object.keys(mcpServers) : [],
147
161
  deniedMcpEvents,
162
+ sensitiveValues,
148
163
  });
149
164
  // Capture the SDK-reported session id BEFORE checking for a bus
150
165
  // rejection so the next turn's `Options.resume` points at this
package/dist/sdk/agent.js CHANGED
@@ -9,6 +9,7 @@ import { createManagedSession } from './managed-session.js';
9
9
  import { createAgentLLM } from '../utils/llm.js';
10
10
  import { buildRunSnapshot } from './snapshots.js';
11
11
  import { buildModelAgentResultLogEntry } from './agent-result-log.js';
12
+ import { buildToolEventLogEntry } from './tool-event-log.js';
12
13
  import { getVisibleAssistantMessage } from './visible-turn.js';
13
14
  import { getCurrentCaseContext } from './case-context.js';
14
15
  import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
@@ -140,21 +141,18 @@ class AgentImpl {
140
141
  this.accumulateTurnUsage(turnResult);
141
142
  const response = getVisibleAssistantMessage(turnResult);
142
143
  const durationMs = Date.now() - turnStart;
144
+ const turnCompletedAt = timestamp();
145
+ for (const toolEvent of turnResult.toolEvents) {
146
+ this._log.push(buildToolEventLogEntry(toolEvent, turnCompletedAt));
147
+ this.verbose.toolEvent({ action: toolEvent.action, summary: toolEvent.summary });
148
+ }
143
149
  this._log.push(buildModelAgentResultLogEntry({
144
- timestamp: timestamp(),
150
+ timestamp: turnCompletedAt,
145
151
  turnNumber,
146
152
  durationMs,
147
153
  turnResult,
148
154
  assistantMessage: response,
149
155
  }));
150
- for (const toolEvent of turnResult.toolEvents) {
151
- this._log.push({
152
- type: 'tool_event',
153
- timestamp: timestamp(),
154
- tool_event: toolEvent,
155
- });
156
- this.verbose.toolEvent({ action: toolEvent.action, summary: toolEvent.summary });
157
- }
158
156
  this._messages.push({ role: 'agent', content: response });
159
157
  this.verbose.turnEnd({
160
158
  turn: turnNumber,
@@ -234,11 +232,7 @@ class AgentImpl {
234
232
  // it undefined, in which case this is a no-op.
235
233
  this.accumulateTurnUsage(turnResult);
236
234
  for (const toolEvent of turnResult.toolEvents) {
237
- this._log.push({
238
- type: 'tool_event',
239
- timestamp: new Date().toISOString(),
240
- tool_event: toolEvent,
241
- });
235
+ this._log.push(buildToolEventLogEntry(toolEvent, new Date().toISOString()));
242
236
  }
243
237
  // Exit-code failures are no longer thrown here. The runConversation
244
238
  // loop projects the partial-turn through `pushModelAgentMessage`
package/dist/sdk/chat.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { buildModelAgentResultLogEntry } from './agent-result-log.js';
2
2
  import { getVisibleAssistantMessage } from './visible-turn.js';
3
+ import { buildToolEventLogEntry } from './tool-event-log.js';
3
4
  export class ChatSessionImpl {
4
5
  _turn;
5
6
  _done = false;
@@ -51,24 +52,21 @@ export class ChatSessionImpl {
51
52
  }
52
53
  const response = getVisibleAssistantMessage(turnResult);
53
54
  const durationMs = Date.now() - turnStart;
54
- this.deps.log.push(buildModelAgentResultLogEntry({
55
- timestamp: timestamp(),
56
- turnNumber,
57
- durationMs,
58
- turnResult,
59
- assistantMessage: response,
60
- }));
55
+ const turnCompletedAt = timestamp();
61
56
  for (const toolEvent of turnResult.toolEvents) {
62
- this.deps.log.push({
63
- type: 'tool_event',
64
- timestamp: timestamp(),
65
- tool_event: toolEvent,
66
- });
57
+ this.deps.log.push(buildToolEventLogEntry(toolEvent, turnCompletedAt));
67
58
  this.deps.verbose?.toolEvent({
68
59
  action: toolEvent.action,
69
60
  summary: toolEvent.summary,
70
61
  });
71
62
  }
63
+ this.deps.log.push(buildModelAgentResultLogEntry({
64
+ timestamp: turnCompletedAt,
65
+ turnNumber,
66
+ durationMs,
67
+ turnResult,
68
+ assistantMessage: response,
69
+ }));
72
70
  this.deps.messages.push({ role: 'agent', content: response });
73
71
  this.deps.verbose?.turnEnd({
74
72
  turn: turnNumber,
@@ -206,7 +206,28 @@ export async function getToolEvents(events, actionFilter) {
206
206
  const filtered = actionFilter
207
207
  ? events.filter((e) => e.action.includes(actionFilter))
208
208
  : events;
209
- return JSON.stringify(filtered);
209
+ const serialized = JSON.stringify(filtered);
210
+ const maxChars = 200 * 1024;
211
+ if (serialized.length <= maxChars)
212
+ return serialized;
213
+ const included = [];
214
+ for (const event of filtered) {
215
+ const candidate = JSON.stringify({
216
+ events: [...included, event],
217
+ truncated: true,
218
+ totalEvents: filtered.length,
219
+ includedEvents: included.length + 1,
220
+ });
221
+ if (candidate.length > maxChars)
222
+ break;
223
+ included.push(event);
224
+ }
225
+ return JSON.stringify({
226
+ events: included,
227
+ truncated: true,
228
+ totalEvents: filtered.length,
229
+ includedEvents: included.length,
230
+ });
210
231
  }
211
232
  export const DEFAULT_TOOL_REGISTRY = new Map([
212
233
  ['readFile', {
@@ -269,7 +290,7 @@ export const DEFAULT_TOOL_REGISTRY = new Map([
269
290
  ['getToolEvents', {
270
291
  schema: {
271
292
  name: 'getToolEvents',
272
- description: 'Retrieve the agent session tool events as JSON. Optional actionFilter substring-matches against event.action.',
293
+ description: 'Retrieve the agent session tool events as JSON. Optional actionFilter substring-matches against event.action. Oversized responses use a valid { events, truncated, totalEvents, includedEvents } JSON envelope.',
273
294
  input_schema: {
274
295
  type: 'object',
275
296
  properties: { actionFilter: { type: 'string' } },
@@ -2,6 +2,7 @@ import { createAgentSession } from '../types.js';
2
2
  import { createAgentEnvironment } from '../agents/registry.js';
3
3
  import { withAbortTimeout } from '../utils/timeout.js';
4
4
  import { buildModelAgentResultLogEntry } from './agent-result-log.js';
5
+ import { buildToolEventLogEntry } from './tool-event-log.js';
5
6
  import { planRuntimePolicies } from './runtime-policy.js';
6
7
  import { getVisibleAssistantMessage } from './visible-turn.js';
7
8
  import { createAskBus } from './ask-bus/bus.js';
@@ -84,14 +85,15 @@ export function createManagedSession(deps) {
84
85
  messages.push({ role: 'user', content: message });
85
86
  const turnResult = await executeTurn(message);
86
87
  const response = getVisibleAssistantMessage(turnResult);
88
+ const turnCompletedAt = new Date().toISOString();
89
+ for (const toolEvent of turnResult.toolEvents) {
90
+ log.push(buildToolEventLogEntry(toolEvent, turnCompletedAt));
91
+ }
87
92
  log.push(buildModelAgentResultLogEntry({
88
- timestamp: new Date().toISOString(),
93
+ timestamp: turnCompletedAt,
89
94
  turnResult,
90
95
  assistantMessage: response,
91
96
  }));
92
- for (const toolEvent of turnResult.toolEvents) {
93
- log.push({ type: 'tool_event', timestamp: new Date().toISOString(), tool_event: toolEvent });
94
- }
95
97
  messages.push({ role: 'agent', content: response });
96
98
  if (turnResult.exitCode !== 0) {
97
99
  if (turnResult.timedOut)
@@ -0,0 +1,3 @@
1
+ import type { ToolEvent } from '../tool-events.js';
2
+ import type { LogEntry } from '../types.js';
3
+ export declare function buildToolEventLogEntry(toolEvent: ToolEvent, fallbackTimestamp: string): LogEntry;
@@ -0,0 +1,7 @@
1
+ export function buildToolEventLogEntry(toolEvent, fallbackTimestamp) {
2
+ return {
3
+ type: 'tool_event',
4
+ timestamp: toolEvent.startedAt ?? fallbackTimestamp,
5
+ tool_event: toolEvent,
6
+ };
7
+ }
@@ -3,13 +3,32 @@ export interface ToolEvent {
3
3
  action: ToolAction;
4
4
  provider: 'claude' | 'codex' | 'cursor' | 'opencode';
5
5
  providerToolName: string;
6
+ /** Provider correlation identifier joining a tool invocation to its result. */
7
+ toolUseId?: string;
6
8
  turnNumber?: number;
7
9
  arguments?: Record<string, unknown>;
10
+ /** Lifecycle state observed by PathGrade. Absent on legacy provider events. */
11
+ status?: 'completed' | 'error' | 'incomplete';
12
+ /** Local wall-clock receive time for the invocation boundary. */
13
+ startedAt?: string;
14
+ /** Local wall-clock receive time for the matching result boundary. */
15
+ completedAt?: string;
16
+ /** Monotonic receive-time delta between invocation and result. */
17
+ durationMs?: number;
18
+ /** Bounded provider result data. Fields remain absent when unavailable. */
19
+ result?: ToolEventResult;
8
20
  summary: string;
9
21
  confidence: 'high' | 'medium' | 'low';
10
22
  rawSnippet: string;
11
23
  skillName?: string;
12
24
  }
25
+ export interface ToolEventResult {
26
+ content?: string;
27
+ stdout?: string;
28
+ stderr?: string;
29
+ exitCode?: number;
30
+ truncated?: boolean;
31
+ }
13
32
  export declare function summarizeToolEvents(events: ToolEvent[]): string;
14
33
  /**
15
34
  * Map from provider-specific tool names to normalized Pathgrade actions.
package/dist/viewer.html CHANGED
@@ -1127,10 +1127,10 @@
1127
1127
  + (e.value?.toFixed(2) || '0.00') + '</span>'
1128
1128
  + (e.output ? '<div class="scorer-details" style="margin-top:0.25rem">' + esc(e.output) + '</div>' : '');
1129
1129
  break;
1130
- case 'tool_event':
1131
- body = '<span class="badge badge-type">' + esc(e.tool_event?.action || 'unknown') + '</span> '
1132
- + '<span class="scorer-details">' + esc(e.tool_event?.summary || '') + '</span>';
1133
- break;
1130
+ case 'tool_event': {
1131
+ const tool = e.tool_event || {}; const toolResult = tool.result || {}; const resultText = toolResult.content || toolResult.stdout || ''; const stderrText = toolResult.stderr || ''; const duration = typeof tool.durationMs === 'number' ? `${Math.round(tool.durationMs)}ms` : ''; const exit = typeof toolResult.exitCode === 'number' ? `exit ${toolResult.exitCode}` : ''; const meta = [tool.status || 'unknown', duration, exit].filter(Boolean).join(' · '); const output = resultText || stderrText ? '<div class="log-command-output"><pre class="code-block">' + esc(resultText) + (stderrText ? '<span class="stderr-text">' + esc(stderrText) + '</span>' : '') + '</pre></div>' : '';
1132
+ body = '<details class="log-command-details"><summary class="log-command-summary"><span class="badge badge-type">' + esc(tool.action || 'unknown') + '</span> <span class="scorer-details">' + esc(tool.summary || '') + '</span><span class="log-command-meta">' + esc(meta) + '</span></summary>' + output + '</details>'; break;
1133
+ }
1134
1134
  case 'judge_tool_call': {
1135
1135
  const tc = e.judge_tool_call || {};
1136
1136
  const okBadge = tc.ok
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -132,5 +132,5 @@
132
132
  "typescript": "^5.9.3",
133
133
  "zod": "4.3.6"
134
134
  },
135
- "falconPackageHash": "be877aa61dfb2cd1b58c716b704cffee9592428675a660382e86e358"
135
+ "falconPackageHash": "0bfcf787a0b4fe0c48683c6aa662dd0e3c9679800fdcc9811414571d"
136
136
  }