@vitest-evals/harness-pi-ai 0.13.1 → 0.15.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/README.md CHANGED
@@ -164,8 +164,8 @@ const harness = piAiHarness({
164
164
  When an agent exposes both a native Pi tool and a runtime tool with the same
165
165
  name, a native tool call records in its own cassette namespace. Runtime calls of
166
166
  that same name are treated as implementation details while the native tool is
167
- executing, so delegated runtime calls do not create duplicate trace entries or
168
- overwrite the native recording.
167
+ executing, so delegated runtime calls do not create duplicate transcript entries
168
+ or overwrite the native recording.
169
169
 
170
170
  Supported modes:
171
171
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as vitest_evals_judges from 'vitest-evals/judges';
2
- import { NormalizedMessage, JsonValue, HarnessContext, HarnessRun, Harness } from 'vitest-evals/harness';
2
+ import { TranscriptMessageEvent, JsonValue, NormalizedError, HarnessContext, HarnessRun, Harness } from 'vitest-evals/harness';
3
3
  import { ReplayMode, ToolRecording, ToolReplayConfig } from 'vitest-evals/replay';
4
4
  import { Api, Model, SimpleStreamOptions } from '@mariozechner/pi-ai';
5
5
 
@@ -63,13 +63,18 @@ type PiAiAgentResult<TAgent, TInput, TTools extends PiAiToolset<TInput>> = TAgen
63
63
  } ? Awaited<TResult> : unknown;
64
64
  /** Replay mode alias used by the Pi AI harness package. */
65
65
  type PiAiReplayMode = ReplayMode;
66
- /** Event sink for recording Pi AI transcript messages. */
66
+ /** Event sink for recording Pi AI transcript events. */
67
67
  interface PiAiEventSink {
68
- message: (message: NormalizedMessage) => void;
68
+ message: (event: TranscriptMessageEvent) => void;
69
69
  system: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
70
70
  user: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
71
71
  assistant: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
72
- tool: (name: string, content: JsonValue, metadata?: Record<string, JsonValue>) => void;
72
+ /** Records a tool-result event linked to an assistant tool call by id. */
73
+ tool: (toolCallId: string, content: JsonValue, options?: {
74
+ name?: string;
75
+ error?: NormalizedError;
76
+ metadata?: Record<string, JsonValue>;
77
+ }) => void;
73
78
  }
74
79
  /** Context passed to instrumented Pi AI tool executions. */
75
80
  interface PiAiToolContext<TInput = string> {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as vitest_evals_judges from 'vitest-evals/judges';
2
- import { NormalizedMessage, JsonValue, HarnessContext, HarnessRun, Harness } from 'vitest-evals/harness';
2
+ import { TranscriptMessageEvent, JsonValue, NormalizedError, HarnessContext, HarnessRun, Harness } from 'vitest-evals/harness';
3
3
  import { ReplayMode, ToolRecording, ToolReplayConfig } from 'vitest-evals/replay';
4
4
  import { Api, Model, SimpleStreamOptions } from '@mariozechner/pi-ai';
5
5
 
@@ -63,13 +63,18 @@ type PiAiAgentResult<TAgent, TInput, TTools extends PiAiToolset<TInput>> = TAgen
63
63
  } ? Awaited<TResult> : unknown;
64
64
  /** Replay mode alias used by the Pi AI harness package. */
65
65
  type PiAiReplayMode = ReplayMode;
66
- /** Event sink for recording Pi AI transcript messages. */
66
+ /** Event sink for recording Pi AI transcript events. */
67
67
  interface PiAiEventSink {
68
- message: (message: NormalizedMessage) => void;
68
+ message: (event: TranscriptMessageEvent) => void;
69
69
  system: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
70
70
  user: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
71
71
  assistant: (content: JsonValue, metadata?: Record<string, JsonValue>) => void;
72
- tool: (name: string, content: JsonValue, metadata?: Record<string, JsonValue>) => void;
72
+ /** Records a tool-result event linked to an assistant tool call by id. */
73
+ tool: (toolCallId: string, content: JsonValue, options?: {
74
+ name?: string;
75
+ error?: NormalizedError;
76
+ metadata?: Record<string, JsonValue>;
77
+ }) => void;
73
78
  }
74
79
  /** Context passed to instrumented Pi AI tool executions. */
75
80
  interface PiAiToolContext<TInput = string> {
package/dist/index.js CHANGED
@@ -88,8 +88,9 @@ function piAiHarness(options) {
88
88
  input,
89
89
  context
90
90
  });
91
- const messages = [
91
+ const events = [
92
92
  {
93
+ type: "message",
93
94
  role: "user",
94
95
  content: (0, import_harness.normalizeContent)(input)
95
96
  }
@@ -101,7 +102,7 @@ function piAiHarness(options) {
101
102
  agent,
102
103
  input,
103
104
  context,
104
- messages,
105
+ events,
105
106
  options.tools,
106
107
  inferredTools.nativeToolsets
107
108
  );
@@ -111,7 +112,7 @@ function piAiHarness(options) {
111
112
  agent,
112
113
  input,
113
114
  context,
114
- messages,
115
+ events,
115
116
  inferredTools.runtimeTools,
116
117
  inferredTools.nativeToolsets
117
118
  );
@@ -148,7 +149,7 @@ function validateOptions(options) {
148
149
  );
149
150
  }
150
151
  }
151
- async function executePiHarnessRun(options, agent, input, context, messages, runtimeTools, nativeToolsets) {
152
+ async function executePiHarnessRun(options, agent, input, context, events, runtimeTools, nativeToolsets) {
152
153
  const trace = createTraceRecorder(options.name ?? "pi-ai");
153
154
  const executionState = createPiToolExecutionState();
154
155
  const runtime = createRuntime({
@@ -157,7 +158,7 @@ async function executePiHarnessRun(options, agent, input, context, messages, run
157
158
  tools: runtimeTools,
158
159
  toolReplay: options.toolReplay,
159
160
  executionState,
160
- messages
161
+ events
161
162
  });
162
163
  try {
163
164
  const result = await withInstrumentedAgentTools(
@@ -166,8 +167,7 @@ async function executePiHarnessRun(options, agent, input, context, messages, run
166
167
  {
167
168
  input,
168
169
  context,
169
- messages,
170
- toolCalls: runtime.toolCalls,
170
+ events,
171
171
  toolReplay: options.toolReplay,
172
172
  executionState
173
173
  },
@@ -194,8 +194,8 @@ async function executePiHarnessRun(options, agent, input, context, messages, run
194
194
  result: normalizeResult
195
195
  };
196
196
  const output = options.output ? await options.output(resultArgs) : resolveOutput(normalizeResult);
197
- const usage = resolveUsage(normalizeResult, runtime.toolCalls.length);
198
- const session = resolveSession(normalizeResult, messages, output, usage);
197
+ const usage = resolveUsage(normalizeResult, countToolCallEvents(events));
198
+ const session = resolveSession(normalizeResult, events, output, usage);
199
199
  const errors = resolveErrors(normalizeResult);
200
200
  const finishedAt = /* @__PURE__ */ new Date();
201
201
  return {
@@ -214,8 +214,8 @@ async function executePiHarnessRun(options, agent, input, context, messages, run
214
214
  ]
215
215
  };
216
216
  } catch (error) {
217
- const usage = resolveUsage(void 0, runtime.toolCalls.length);
218
- const session = resolveSession(void 0, messages, void 0, usage);
217
+ const usage = resolveUsage(void 0, countToolCallEvents(events));
218
+ const session = resolveSession(void 0, events, void 0, usage);
219
219
  const finishedAt = /* @__PURE__ */ new Date();
220
220
  const serializedError = (0, import_harness.serializeError)(error);
221
221
  const run = {
@@ -290,15 +290,7 @@ function finishPiAiTrace(trace, options) {
290
290
  })
291
291
  };
292
292
  const modelSpan = createUsageModelSpan(trace, options.usage);
293
- const spans = [
294
- rootSpan,
295
- ...modelSpan ? [modelSpan] : [],
296
- ...(0, import_harness.createToolCallSpans)((0, import_harness.toolCalls)(options.session), {
297
- traceId: trace.id,
298
- parentId: trace.rootSpanId,
299
- spanIdPrefix: `${trace.id}:tool`
300
- })
301
- ];
293
+ const spans = [rootSpan, ...modelSpan ? [modelSpan] : []];
302
294
  return {
303
295
  id: trace.id,
304
296
  name: trace.name,
@@ -447,6 +439,15 @@ async function withInstrumentedAgentTools(agent, toolsets, args, callback) {
447
439
  args.executionState,
448
440
  tool.name
449
441
  );
442
+ const call = {
443
+ type: "tool_call",
444
+ id: toolCallId,
445
+ name: tool.name,
446
+ arguments: rawArgs,
447
+ startedAt: startedAt.toISOString(),
448
+ metadata: (0, import_replay.normalizeReplayMetadata)(void 0)
449
+ };
450
+ args.events.push(call);
450
451
  try {
451
452
  const execution = await executeNativeToolWithReplay({
452
453
  toolName: tool.name,
@@ -457,45 +458,34 @@ async function withInstrumentedAgentTools(agent, toolsets, args, callback) {
457
458
  context: toolContext
458
459
  });
459
460
  const finishedAt = /* @__PURE__ */ new Date();
460
- const call = {
461
- id: toolCallId,
461
+ call.finishedAt = finishedAt.toISOString();
462
+ call.durationMs = finishedAt.getTime() - startedAt.getTime();
463
+ call.metadata = (0, import_replay.normalizeReplayMetadata)(execution.replay);
464
+ args.events.push({
465
+ type: "tool_result",
466
+ toolCallId,
462
467
  name: tool.name,
463
- arguments: rawArgs,
464
- result: execution.normalizedResult,
465
- startedAt: startedAt.toISOString(),
466
- finishedAt: finishedAt.toISOString(),
467
- durationMs: finishedAt.getTime() - startedAt.getTime(),
468
- metadata: (0, import_replay.normalizeReplayMetadata)(execution.replay)
469
- };
470
- args.toolCalls.push(call);
471
- args.messages.push({
472
- role: "assistant",
473
- toolCalls: [call]
474
- });
475
- args.messages.push({
476
- role: "tool",
477
468
  content: execution.normalizedResult,
478
- metadata: {
479
- name: tool.name
480
- }
469
+ startedAt: call.startedAt,
470
+ finishedAt: call.finishedAt,
471
+ durationMs: call.durationMs
481
472
  });
482
473
  return execution.result;
483
474
  } catch (error) {
484
475
  const finishedAt = /* @__PURE__ */ new Date();
485
- const call = {
486
- id: toolCallId,
476
+ call.finishedAt = finishedAt.toISOString();
477
+ call.durationMs = finishedAt.getTime() - startedAt.getTime();
478
+ call.metadata = (0, import_replay.normalizeReplayMetadata)(
479
+ (0, import_replay.getReplayMetadataFromError)(error)
480
+ );
481
+ args.events.push({
482
+ type: "tool_result",
483
+ toolCallId,
487
484
  name: tool.name,
488
- arguments: rawArgs,
489
485
  error: serializeToolCallError(error),
490
- startedAt: startedAt.toISOString(),
491
- finishedAt: finishedAt.toISOString(),
492
- durationMs: finishedAt.getTime() - startedAt.getTime(),
493
- metadata: (0, import_replay.normalizeReplayMetadata)((0, import_replay.getReplayMetadataFromError)(error))
494
- };
495
- args.toolCalls.push(call);
496
- args.messages.push({
497
- role: "assistant",
498
- toolCalls: [call]
486
+ startedAt: call.startedAt,
487
+ finishedAt: call.finishedAt,
488
+ durationMs: call.durationMs
499
489
  });
500
490
  throw error;
501
491
  } finally {
@@ -570,6 +560,9 @@ function serializeToolCallError(error) {
570
560
  ...typeof type === "string" ? { type } : {}
571
561
  };
572
562
  }
563
+ function createRuntimeToolCallId(toolName, index) {
564
+ return `runtime:${toolName}:${index + 1}`;
565
+ }
573
566
  function getNativeToolExecuteOrigin(execute) {
574
567
  const nativeExecute = execute;
575
568
  return nativeExecute[ORIGINAL_NATIVE_EXECUTE] ?? nativeExecute;
@@ -638,42 +631,44 @@ function createRuntime({
638
631
  tools,
639
632
  toolReplay,
640
633
  executionState,
641
- messages
634
+ events
642
635
  }) {
643
- const toolCalls = [];
644
636
  const eventSink = {
645
- message: (message) => {
646
- messages.push(message);
637
+ message: (event) => {
638
+ events.push(event);
647
639
  },
648
640
  system: (content, metadata) => {
649
- messages.push({
641
+ events.push({
642
+ type: "message",
650
643
  role: "system",
651
644
  content,
652
645
  metadata
653
646
  });
654
647
  },
655
648
  user: (content, metadata) => {
656
- messages.push({
649
+ events.push({
650
+ type: "message",
657
651
  role: "user",
658
652
  content,
659
653
  metadata
660
654
  });
661
655
  },
662
656
  assistant: (content, metadata) => {
663
- messages.push({
657
+ events.push({
658
+ type: "message",
664
659
  role: "assistant",
665
660
  content,
666
661
  metadata
667
662
  });
668
663
  },
669
- tool: (name, content, metadata) => {
670
- messages.push({
671
- role: "tool",
664
+ tool: (toolCallId, content, options) => {
665
+ events.push({
666
+ type: "tool_result",
667
+ toolCallId,
668
+ ...options?.name ? { name: options.name } : {},
672
669
  content,
673
- metadata: {
674
- name,
675
- ...metadata ?? {}
676
- }
670
+ ...options?.error ? { error: options.error } : {},
671
+ ...options?.metadata ? { metadata: options.metadata } : {}
677
672
  });
678
673
  }
679
674
  };
@@ -691,7 +686,21 @@ function createRuntime({
691
686
  signal: context.signal,
692
687
  setArtifact: context.setArtifact
693
688
  };
689
+ const toolCallId = createRuntimeToolCallId(
690
+ toolName,
691
+ countToolCallEvents(events)
692
+ );
693
+ const call = {
694
+ type: "tool_call",
695
+ id: toolCallId,
696
+ name: toolName,
697
+ arguments: args,
698
+ startedAt: startedAt.toISOString()
699
+ };
694
700
  try {
701
+ if (!isNativeImplementationCall) {
702
+ events.push(call);
703
+ }
695
704
  const execution = await executeToolWithReplay({
696
705
  toolName,
697
706
  tool,
@@ -703,26 +712,17 @@ function createRuntime({
703
712
  if (isNativeImplementationCall) {
704
713
  return execution.result;
705
714
  }
706
- const call = {
715
+ call.finishedAt = finishedAt.toISOString();
716
+ call.durationMs = finishedAt.getTime() - startedAt.getTime();
717
+ call.metadata = (0, import_replay.normalizeReplayMetadata)(execution.replay);
718
+ events.push({
719
+ type: "tool_result",
720
+ toolCallId,
707
721
  name: toolName,
708
- arguments: args,
709
- result: execution.result,
710
- startedAt: startedAt.toISOString(),
711
- finishedAt: finishedAt.toISOString(),
712
- durationMs: finishedAt.getTime() - startedAt.getTime(),
713
- metadata: (0, import_replay.normalizeReplayMetadata)(execution.replay)
714
- };
715
- toolCalls.push(call);
716
- messages.push({
717
- role: "assistant",
718
- toolCalls: [call]
719
- });
720
- messages.push({
721
- role: "tool",
722
722
  content: execution.result,
723
- metadata: {
724
- name: toolName
725
- }
723
+ startedAt: call.startedAt,
724
+ finishedAt: call.finishedAt,
725
+ durationMs: call.durationMs
726
726
  });
727
727
  return execution.result;
728
728
  } catch (error) {
@@ -730,21 +730,19 @@ function createRuntime({
730
730
  if (isNativeImplementationCall) {
731
731
  throw error;
732
732
  }
733
- const call = {
733
+ call.finishedAt = finishedAt.toISOString();
734
+ call.durationMs = finishedAt.getTime() - startedAt.getTime();
735
+ call.metadata = (0, import_replay.normalizeReplayMetadata)(
736
+ (0, import_replay.getReplayMetadataFromError)(error)
737
+ );
738
+ events.push({
739
+ type: "tool_result",
740
+ toolCallId,
734
741
  name: toolName,
735
- arguments: args,
736
742
  error: serializeToolCallError(error),
737
- startedAt: startedAt.toISOString(),
738
- finishedAt: finishedAt.toISOString(),
739
- durationMs: finishedAt.getTime() - startedAt.getTime(),
740
- metadata: (0, import_replay.normalizeReplayMetadata)(
741
- (0, import_replay.getReplayMetadataFromError)(error)
742
- )
743
- };
744
- toolCalls.push(call);
745
- messages.push({
746
- role: "assistant",
747
- toolCalls: [call]
743
+ startedAt: call.startedAt,
744
+ finishedAt: call.finishedAt,
745
+ durationMs: call.durationMs
748
746
  });
749
747
  throw error;
750
748
  }
@@ -754,8 +752,7 @@ function createRuntime({
754
752
  return {
755
753
  tools: runtimeTools,
756
754
  events: eventSink,
757
- signal: context.signal,
758
- toolCalls
755
+ signal: context.signal
759
756
  };
760
757
  }
761
758
  function resolveOutput(result) {
@@ -874,6 +871,9 @@ function isLegacyNativeToolReplayMarker(value) {
874
871
  value && typeof value === "object" && "kind" in value && value.kind === "pi-ai-native-tool-result" && "version" in value && value.version === 1
875
872
  );
876
873
  }
874
+ function countToolCallEvents(events) {
875
+ return events.filter((event) => event.type === "tool_call").length;
876
+ }
877
877
  function resolveUsage(result, toolCallCount) {
878
878
  if (!result || typeof result !== "object") {
879
879
  return toolCallCount > 0 ? { toolCalls: toolCallCount } : {};
@@ -929,26 +929,24 @@ function stringField(value) {
929
929
  function numberField(value) {
930
930
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
931
931
  }
932
- function resolveSession(result, messages, output, usage) {
932
+ function resolveSession(result, events, output, usage) {
933
933
  if ((0, import_harness.isNormalizedSession)(
934
934
  result?.session
935
935
  )) {
936
936
  return result.session;
937
937
  }
938
- if ((0, import_harness.isNormalizedSession)(result?.trace)) {
939
- return result.trace;
940
- }
941
- const sessionMessages = [...messages];
942
- if (output !== void 0 && !sessionMessages.some(
943
- (message) => message.role === "assistant" && message.content !== void 0
938
+ const sessionEvents = [...events];
939
+ if (output !== void 0 && !sessionEvents.some(
940
+ (event) => event.type === "message" && event.role === "assistant" && event.content !== void 0
944
941
  )) {
945
- sessionMessages.push({
942
+ sessionEvents.push({
943
+ type: "message",
946
944
  role: "assistant",
947
945
  content: output
948
946
  });
949
947
  }
950
948
  return {
951
- messages: sessionMessages,
949
+ events: sessionEvents,
952
950
  provider: result?.provider ?? usage.provider,
953
951
  model: result?.model ?? usage.model
954
952
  };