@ai-sdk/harness 1.0.101 → 1.0.103

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.
@@ -1,515 +1,364 @@
1
- import {
2
- generateId,
3
- type ModelMessage,
4
- type ToolSet,
1
+ import type {
2
+ Context,
3
+ InferToolSetContext,
4
+ ModelMessage,
5
+ ToolSet,
5
6
  } from '@ai-sdk/provider-utils';
6
7
  import { createTelemetryDispatcher } from 'ai/internal';
7
- import type { LanguageModelUsage, TelemetryOptions } from 'ai';
8
+ import type {
9
+ ContentPart,
10
+ GenerateTextOnEndCallback,
11
+ GenerateTextOnStartCallback,
12
+ GenerateTextOnStepEndCallback,
13
+ GenerateTextOnStepStartCallback,
14
+ LanguageModelUsage,
15
+ OnLanguageModelCallEndCallback,
16
+ OnLanguageModelCallStartCallback,
17
+ OnToolExecutionEndCallback,
18
+ OnToolExecutionStartCallback,
19
+ OutputInterface as Output,
20
+ StepResult,
21
+ TelemetryOptions,
22
+ ToolExecutionEndEvent,
23
+ ToolExecutionStartEvent,
24
+ TypedToolCall,
25
+ TypedToolError,
26
+ TypedToolResult,
27
+ } from 'ai';
8
28
  import type { HarnessV1ToolSpec } from '../../v1';
9
29
 
10
- /*
11
- * Drives AI SDK's pluggable `Telemetry` lifecycle from a harness turn.
12
- *
13
- * A harness turn is not a `streamText` call — it has no language model, prompt
14
- * standardization, or sampling settings — but the AI SDK telemetry contract is
15
- * shaped around `generateText`/`streamText` events, and `@ai-sdk/otel` (the
16
- * main integration) only produces spans when the full lifecycle fires. So we
17
- * map the turn onto that contract: turn = operation, each `finish-step` = a
18
- * step boundary, tool-calls = tool executions, `finish` = operation end. The
19
- * model-call-only event fields the harness has no value for (sampling params,
20
- * standardized prompt) are left `undefined` / cast; the fields the integrations
21
- * actually read (`callId`, `operationId`, `provider`, `modelId`,
22
- * `instructions`, `messages`, `tools`, `toolCall`, `usage`, `finishReason`)
23
- * carry real values.
24
- *
25
- * Telemetry is opt-in: the framework only drives it when `settings.telemetry`
26
- * is set (the dispatcher then also honours globally-registered integrations).
27
- */
30
+ export type HarnessAgentLifecycleCallbacks<
31
+ TOOLS extends ToolSet,
32
+ RUNTIME_CONTEXT extends Context,
33
+ OUTPUT extends Output,
34
+ > = {
35
+ onStart?: GenerateTextOnStartCallback<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
36
+ onStepStart?: GenerateTextOnStepStartCallback<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
37
+ onLanguageModelCallStart?: OnLanguageModelCallStartCallback;
38
+ onLanguageModelCallEnd?: OnLanguageModelCallEndCallback<TOOLS>;
39
+ onToolExecutionStart?: OnToolExecutionStartCallback<TOOLS>;
40
+ onToolExecutionEnd?: OnToolExecutionEndCallback<TOOLS>;
41
+ onStepEnd?: GenerateTextOnStepEndCallback<TOOLS, RUNTIME_CONTEXT>;
42
+ onEnd?: GenerateTextOnEndCallback<TOOLS, RUNTIME_CONTEXT>;
43
+ };
28
44
 
29
45
  type Dispatcher = ReturnType<typeof createTelemetryDispatcher>;
30
- type EventArg<K extends keyof Dispatcher> = Dispatcher[K] extends
31
- | ((event: infer E) => unknown)
32
- | undefined
33
- ? E
34
- : never;
35
-
36
- /**
37
- * An output content part accumulated over a step — the model's assistant turn.
38
- * Shaped for the gen_ai output-message conventions `@ai-sdk/otel` reads.
39
- */
40
- export type TurnContentPart =
41
- | { type: 'text'; text: string }
42
- | { type: 'reasoning'; text: string }
43
- | { type: 'tool-call'; toolCallId: string; toolName: string; input: unknown };
44
46
 
45
- export interface TurnTelemetry {
46
- /**
47
- * Begin the operation span. Called on `stream-start`, optionally with the
48
- * model the runtime resolved to (overriding the session's configured id).
49
- * Idempotent — the first call wins.
50
- */
47
+ export interface TurnLifecycle<
48
+ TOOLS extends ToolSet,
49
+ RUNTIME_CONTEXT extends Context,
50
+ > {
51
51
  start(modelId?: string): Promise<void>;
52
- /** Open a step span lazily, before the first content of a step. */
53
52
  ensureStepOpen(): Promise<void>;
54
- /** Close the current step (on a harness `finish-step`). */
55
- stepFinish(info: {
56
- finishReason: unknown;
57
- usage: unknown;
58
- providerMetadata?: unknown;
59
- /** The model's output content for this step (text/reasoning/tool-calls). */
60
- content?: TurnContentPart[];
53
+ languageModelCallEnd(input: {
54
+ finishReason: StepResult<TOOLS, RUNTIME_CONTEXT>['finishReason'];
55
+ usage: LanguageModelUsage;
56
+ content: ContentPart<TOOLS>[];
57
+ providerMetadata: StepResult<TOOLS, RUNTIME_CONTEXT>['providerMetadata'];
61
58
  }): Promise<void>;
62
- /** A tool execution began (on a `tool-call`). */
63
- toolStart(call: {
64
- toolCallId: string;
65
- toolName: string;
66
- input: unknown;
59
+ toolExecutionStart(input: { toolCall: TypedToolCall<TOOLS> }): Promise<void>;
60
+ toolExecutionEnd(input: {
61
+ toolCall: TypedToolCall<TOOLS>;
62
+ toolOutput: TypedToolResult<TOOLS> | TypedToolError<TOOLS>;
63
+ toolExecutionMs: number;
64
+ }): Promise<void>;
65
+ stepEnd(step: StepResult<TOOLS, RUNTIME_CONTEXT>): Promise<void>;
66
+ end(input: {
67
+ steps: StepResult<TOOLS, RUNTIME_CONTEXT>[];
68
+ usage: LanguageModelUsage;
67
69
  }): Promise<void>;
68
- /** Execute a host tool through each telemetry integration's context wrapper. */
69
70
  executeTool<T>(input: {
70
71
  toolCallId: string;
71
72
  execute: () => PromiseLike<T>;
72
73
  }): Promise<T>;
73
- /**
74
- * A tool execution completed (on its `tool-result` or after host execution).
75
- * Idempotent per `toolCallId` — the first caller wins, so provider-executed
76
- * and host-executed paths can both call it without double-counting.
77
- */
78
- toolEnd(
79
- toolCallId: string,
80
- output: { ok: true; output: unknown } | { ok: false; error: unknown },
81
- ): Promise<void>;
82
- /** The turn ended (on a harness `finish`). */
83
- end(info: { finishReason: unknown; usage: unknown }): Promise<void>;
84
- /** The turn failed. */
85
- error(err: unknown): Promise<void>;
86
- }
87
-
88
- const NOOP: TurnTelemetry = {
89
- async start() {},
90
- async ensureStepOpen() {},
91
- async stepFinish() {},
92
- async toolStart() {},
93
- async executeTool({ execute }) {
94
- return await execute();
95
- },
96
- async toolEnd() {},
97
- async end() {},
98
- async error() {},
99
- };
100
-
101
- function normalizeFinishReason(finishReason: unknown): unknown {
102
- if (
103
- finishReason != null &&
104
- typeof finishReason === 'object' &&
105
- 'unified' in finishReason
106
- ) {
107
- return (finishReason as { unified: unknown }).unified;
108
- }
109
-
110
- return finishReason;
111
- }
112
-
113
- function addTokenCounts(
114
- tokenCount1: number | undefined,
115
- tokenCount2: number | undefined,
116
- ): number | undefined {
117
- return tokenCount1 == null && tokenCount2 == null
118
- ? undefined
119
- : (tokenCount1 ?? 0) + (tokenCount2 ?? 0);
74
+ error(error: unknown): Promise<void>;
120
75
  }
121
76
 
122
- function normalizeUsage(usage: unknown): LanguageModelUsage | unknown {
123
- if (
124
- usage == null ||
125
- typeof usage !== 'object' ||
126
- !('inputTokens' in usage) ||
127
- !('outputTokens' in usage)
128
- ) {
129
- return usage;
130
- }
131
-
132
- const inputTokens = (usage as { inputTokens: unknown }).inputTokens;
133
- const outputTokens = (usage as { outputTokens: unknown }).outputTokens;
134
-
135
- if (
136
- inputTokens == null ||
137
- typeof inputTokens !== 'object' ||
138
- outputTokens == null ||
139
- typeof outputTokens !== 'object'
140
- ) {
141
- return usage;
142
- }
143
-
144
- const input = inputTokens as Record<string, number | undefined>;
145
- const output = outputTokens as Record<string, number | undefined>;
146
-
147
- return {
148
- inputTokens: input.total,
149
- inputTokenDetails: {
150
- noCacheTokens: input.noCache,
151
- cacheReadTokens: input.cacheRead,
152
- cacheWriteTokens: input.cacheWrite,
153
- },
154
- outputTokens: output.total,
155
- outputTokenDetails: {
156
- textTokens: output.text,
157
- reasoningTokens: output.reasoning,
158
- },
159
- totalTokens: addTokenCounts(input.total, output.total),
160
- raw: (usage as { raw?: LanguageModelUsage['raw'] }).raw,
161
- };
77
+ async function notify<EVENT>(
78
+ event: EVENT,
79
+ ...callbacks: Array<((event: EVENT) => unknown) | undefined>
80
+ ): Promise<void> {
81
+ await Promise.allSettled(
82
+ callbacks.map(async callback => {
83
+ await callback?.(event);
84
+ }),
85
+ );
162
86
  }
163
87
 
164
- export function createTurnTelemetry(opts: {
88
+ export function createTurnLifecycle<
89
+ TOOLS extends ToolSet,
90
+ RUNTIME_CONTEXT extends Context,
91
+ OUTPUT extends Output,
92
+ >(options: {
93
+ callId: string;
165
94
  telemetry: TelemetryOptions | undefined;
95
+ callbacks: HarnessAgentLifecycleCallbacks<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
166
96
  harnessId: string;
167
97
  modelId: string | undefined;
168
98
  instructions: string | undefined;
169
- tools: ToolSet;
99
+ tools: TOOLS;
100
+ toolsContext: InferToolSetContext<TOOLS>;
170
101
  activeToolNames: string[];
171
102
  toolSpecs: HarnessV1ToolSpec[];
172
- promptText: string;
173
- runtimeContext: unknown;
174
- }): TurnTelemetry {
175
- // Opt-in: with no telemetry settings we do no work and construct no events.
176
- if (opts.telemetry == null) return NOOP;
177
-
178
- const dispatcher = createTelemetryDispatcher({ telemetry: opts.telemetry });
179
-
180
- const callId = generateId();
181
- const provider = opts.harnessId;
182
- // The configured session model; `start(modelId)` may override it with the
183
- // model the runtime actually resolved to.
184
- let modelId = opts.modelId ?? '';
185
- const runtimeContext = opts.runtimeContext;
186
- const inputMessages: ModelMessage[] = [
187
- { role: 'user', content: opts.promptText },
188
- ];
103
+ messages: ModelMessage[];
104
+ runtimeContext: RUNTIME_CONTEXT;
105
+ output: OUTPUT | undefined;
106
+ }): TurnLifecycle<TOOLS, RUNTIME_CONTEXT> {
107
+ const telemetry =
108
+ options.telemetry == null
109
+ ? ({} as Dispatcher)
110
+ : createTelemetryDispatcher({ telemetry: options.telemetry });
111
+ const provider = `harness:${options.harnessId}`;
112
+ let modelId = options.modelId ?? '';
113
+ let started = false;
114
+ let stepOpen = false;
115
+ let stepNumber = 0;
116
+ let ended = false;
117
+ let modelCallStartedAt = 0;
118
+ const completedSteps: StepResult<TOOLS, RUNTIME_CONTEXT>[] = [];
189
119
  const languageModelTools =
190
- opts.toolSpecs.length === 0
120
+ options.toolSpecs.length === 0
191
121
  ? undefined
192
- : opts.toolSpecs.map(tool => ({
122
+ : options.toolSpecs.map(tool => ({
193
123
  type: 'function' as const,
194
124
  name: tool.name,
195
- ...(tool.description != null
196
- ? { description: tool.description }
197
- : {}),
198
- ...(tool.inputSchema != null
199
- ? { inputSchema: tool.inputSchema }
200
- : {}),
125
+ ...(tool.description == null
126
+ ? {}
127
+ : { description: tool.description }),
128
+ ...(tool.inputSchema == null
129
+ ? {}
130
+ : { inputSchema: tool.inputSchema }),
201
131
  }));
202
132
 
203
- let started = false;
204
- let stepOpen = false;
205
- let stepNumber = 0;
206
- let ended = false;
207
- let finalStepText = '';
208
- let finalStepReasoning: Array<{ text: string }> = [];
209
- let finalStepProviderMetadata: unknown;
210
- let outputToolCalls: Array<{
211
- type: 'tool-call';
212
- toolCallId: string;
213
- toolName: string;
214
- input: unknown;
215
- }> = [];
216
- /** Tool calls started in the current turn and not yet ended. */
217
- const openTools = new Map<
218
- string,
219
- { toolCallId: string; toolName: string; input: unknown }
220
- >();
221
-
222
- const cast = <K extends keyof Dispatcher>(event: unknown): EventArg<K> =>
223
- event as EventArg<K>;
224
-
225
- // onStart — open the operation (root) span. Deferred until `start()` so the
226
- // runtime-resolved model can be attached to the operation span + trace label.
227
- const fireStart = async (): Promise<void> => {
133
+ const start = async (overrideModelId?: string): Promise<void> => {
228
134
  if (started) return;
135
+ if (overrideModelId != null) modelId = overrideModelId;
229
136
  started = true;
230
- await dispatcher.onStart?.(
231
- cast<'onStart'>({
232
- callId,
233
- operationId: 'ai.harness',
234
- provider,
235
- modelId,
236
- tools: opts.tools,
237
- toolChoice: undefined,
238
- activeTools: opts.activeToolNames,
239
- maxRetries: 0,
240
- timeout: undefined,
241
- headers: undefined,
242
- providerOptions: undefined,
243
- output: undefined,
244
- toolsContext: undefined,
245
- runtimeContext,
246
- instructions: opts.instructions,
247
- messages: inputMessages,
248
- }),
137
+ const event = {
138
+ callId: options.callId,
139
+ operationId: 'ai.harness',
140
+ provider,
141
+ modelId,
142
+ tools: options.tools,
143
+ toolChoice: undefined,
144
+ activeTools: options.activeToolNames,
145
+ toolOrder: [],
146
+ maxRetries: 0,
147
+ timeout: undefined,
148
+ headers: undefined,
149
+ providerOptions: undefined,
150
+ output: options.output,
151
+ toolsContext: options.toolsContext,
152
+ runtimeContext: options.runtimeContext,
153
+ instructions: options.instructions,
154
+ messages: options.messages,
155
+ };
156
+ await notify(
157
+ event,
158
+ options.callbacks.onStart,
159
+ telemetry.onStart as typeof options.callbacks.onStart,
249
160
  );
250
161
  };
251
162
 
252
- const start = async (overrideModelId?: string): Promise<void> => {
253
- if (started) return;
254
- if (overrideModelId) modelId = overrideModelId;
255
- await fireStart();
256
- };
257
-
258
163
  const ensureStepOpen = async (): Promise<void> => {
259
- if (!started) await fireStart();
164
+ if (!started) await start();
260
165
  if (stepOpen || ended) return;
261
166
  stepOpen = true;
262
- await dispatcher.onStepStart?.(
263
- cast<'onStepStart'>({
264
- callId,
265
- provider,
266
- modelId,
267
- stepNumber,
268
- tools: opts.tools,
269
- toolChoice: undefined,
270
- activeTools: opts.activeToolNames,
271
- steps: new Array(stepNumber),
272
- providerOptions: undefined,
273
- output: undefined,
274
- runtimeContext,
275
- instructions: opts.instructions,
276
- messages: inputMessages,
277
- }),
167
+ const stepStartEvent = {
168
+ callId: options.callId,
169
+ provider,
170
+ modelId,
171
+ stepNumber,
172
+ tools: options.tools,
173
+ toolChoice: undefined,
174
+ activeTools: options.activeToolNames,
175
+ toolOrder: [],
176
+ steps: [...completedSteps],
177
+ providerOptions: undefined,
178
+ output: options.output,
179
+ runtimeContext: options.runtimeContext,
180
+ toolsContext: options.toolsContext,
181
+ instructions: options.instructions,
182
+ messages: options.messages,
183
+ };
184
+ await notify(
185
+ stepStartEvent,
186
+ options.callbacks.onStepStart,
187
+ telemetry.onStepStart as typeof options.callbacks.onStepStart,
278
188
  );
279
- // Open the inference (language-model call) span — the gen_ai home for the
280
- // step's input and (on end) output messages.
281
- await dispatcher.onLanguageModelCallStart?.(
282
- cast<'onLanguageModelCallStart'>({
283
- callId,
284
- provider,
285
- modelId,
286
- instructions: opts.instructions,
287
- messages: inputMessages,
288
- tools: languageModelTools,
289
- }),
189
+
190
+ const modelStartEvent = {
191
+ callId: options.callId,
192
+ provider,
193
+ modelId,
194
+ instructions: options.instructions,
195
+ messages: options.messages,
196
+ tools: languageModelTools,
197
+ };
198
+ modelCallStartedAt = Date.now();
199
+ await notify(
200
+ modelStartEvent,
201
+ options.callbacks.onLanguageModelCallStart,
202
+ telemetry.onLanguageModelCallStart as typeof options.callbacks.onLanguageModelCallStart,
290
203
  );
291
204
  };
292
205
 
293
- /** Close the inference span with the step's output content. */
294
- const inferenceEnd = async (info: {
295
- finishReason: unknown;
296
- usage: unknown;
297
- content: TurnContentPart[];
298
- providerMetadata?: unknown;
299
- }): Promise<void> => {
300
- const finishReason = normalizeFinishReason(info.finishReason);
301
- const usage = normalizeUsage(info.usage);
206
+ return {
207
+ start,
208
+ ensureStepOpen,
302
209
 
303
- await dispatcher.onLanguageModelCallEnd?.(
304
- cast<'onLanguageModelCallEnd'>({
305
- callId,
210
+ async languageModelCallEnd(input) {
211
+ await ensureStepOpen();
212
+ const event = {
213
+ callId: options.callId,
306
214
  provider,
307
215
  modelId,
308
- finishReason,
309
- responseId: callId,
310
- usage,
311
- content: info.content,
312
- ...(info.providerMetadata != null
313
- ? { providerMetadata: info.providerMetadata }
314
- : {}),
216
+ finishReason: input.finishReason,
217
+ usage: input.usage,
218
+ content: input.content,
219
+ responseId: `${options.callId}-${stepNumber}`,
220
+ providerMetadata: input.providerMetadata,
315
221
  performance: {
316
- responseTimeMs: undefined,
222
+ responseTimeMs: Math.max(0, Date.now() - modelCallStartedAt),
223
+ effectiveOutputTokensPerSecond: 0,
224
+ outputTokensPerSecond: undefined,
225
+ inputTokensPerSecond: undefined,
226
+ effectiveTotalTokensPerSecond: 0,
317
227
  timeToFirstOutputMs: undefined,
318
228
  timeBetweenOutputChunksMs: undefined,
319
229
  },
320
- }),
321
- );
322
- };
323
-
324
- const recordOutputContent = (content: TurnContentPart[]): void => {
325
- finalStepText = '';
326
- finalStepReasoning = [];
327
-
328
- for (const part of content) {
329
- if (part.type === 'text') {
330
- finalStepText += part.text;
331
- } else if (part.type === 'reasoning') {
332
- finalStepReasoning.push({ text: part.text });
333
- } else if (part.type === 'tool-call') {
334
- outputToolCalls.push(part);
335
- }
336
- }
337
- };
230
+ };
231
+ await notify(
232
+ event,
233
+ options.callbacks.onLanguageModelCallEnd,
234
+ telemetry.onLanguageModelCallEnd as typeof options.callbacks.onLanguageModelCallEnd,
235
+ );
236
+ },
338
237
 
339
- const closeOpenTools = async (): Promise<void> => {
340
- for (const call of openTools.values()) {
341
- await dispatcher.onToolExecutionEnd?.(
342
- cast<'onToolExecutionEnd'>({
343
- callId,
344
- toolExecutionMs: 0,
345
- messages: [],
346
- toolCall: {
347
- type: 'tool-call',
348
- toolCallId: call.toolCallId,
349
- toolName: call.toolName,
350
- input: call.input,
351
- dynamic: true,
352
- },
353
- toolContext: undefined,
354
- toolOutput: { type: 'error', error: new Error('tool span unclosed') },
355
- }),
238
+ async toolExecutionStart({ toolCall }) {
239
+ const event = {
240
+ callId: options.callId,
241
+ messages: options.messages,
242
+ toolCall,
243
+ toolContext: undefined,
244
+ } as ToolExecutionStartEvent<TOOLS>;
245
+ await notify(
246
+ event,
247
+ options.callbacks.onToolExecutionStart,
248
+ telemetry.onToolExecutionStart as typeof options.callbacks.onToolExecutionStart,
356
249
  );
357
- }
358
- openTools.clear();
359
- };
250
+ },
360
251
 
361
- return {
362
- start,
363
- ensureStepOpen,
252
+ async toolExecutionEnd({ toolCall, toolOutput, toolExecutionMs }) {
253
+ const event = {
254
+ callId: options.callId,
255
+ messages: options.messages,
256
+ toolCall,
257
+ toolContext: undefined,
258
+ toolOutput,
259
+ toolExecutionMs,
260
+ } as ToolExecutionEndEvent<TOOLS>;
261
+ await notify(
262
+ event,
263
+ options.callbacks.onToolExecutionEnd,
264
+ telemetry.onToolExecutionEnd as typeof options.callbacks.onToolExecutionEnd,
265
+ );
266
+ },
364
267
 
365
- async stepFinish(info) {
268
+ async stepEnd(step) {
366
269
  if (!stepOpen) return;
367
- const content = info.content ?? [];
368
- const finishReason = normalizeFinishReason(info.finishReason);
369
- const usage = normalizeUsage(info.usage);
370
- recordOutputContent(content);
371
- finalStepProviderMetadata = info.providerMetadata;
372
- await closeOpenTools();
373
- await inferenceEnd({
374
- finishReason,
375
- usage,
376
- content,
377
- providerMetadata: info.providerMetadata,
270
+ completedSteps.push(step);
271
+ const telemetryStepEndEvent = Object.assign(Object.create(null), {
272
+ callId: step.callId,
273
+ stepNumber: step.stepNumber,
274
+ model: step.model,
275
+ toolsContext: step.toolsContext,
276
+ runtimeContext: step.runtimeContext,
277
+ content: step.content,
278
+ finishReason: step.finishReason,
279
+ rawFinishReason: step.rawFinishReason,
280
+ usage: step.usage,
281
+ performance: step.performance,
282
+ warnings: step.warnings,
283
+ request: step.request,
284
+ response: step.response,
285
+ providerMetadata: step.providerMetadata,
286
+ text: step.text,
287
+ reasoning: step.reasoning,
288
+ reasoningText: step.reasoningText,
289
+ files: step.files,
290
+ sources: step.sources,
291
+ toolCalls: step.toolCalls,
292
+ staticToolCalls: step.staticToolCalls,
293
+ dynamicToolCalls: step.dynamicToolCalls,
294
+ toolResults: step.toolResults,
295
+ staticToolResults: step.staticToolResults,
296
+ dynamicToolResults: step.dynamicToolResults,
378
297
  });
379
- await dispatcher.onStepEnd?.(
380
- cast<'onStepEnd'>({
381
- callId,
382
- stepNumber,
383
- finishReason,
384
- usage,
385
- providerMetadata: info.providerMetadata,
386
- content,
387
- response: {
388
- id: callId,
389
- modelId,
390
- timestamp: new Date(0),
391
- messages: [],
392
- },
393
- }),
394
- );
298
+ await Promise.allSettled([
299
+ options.callbacks.onStepEnd?.(step),
300
+ telemetry.onStepEnd?.(telemetryStepEndEvent),
301
+ ]);
395
302
  stepOpen = false;
396
303
  stepNumber += 1;
397
304
  },
398
305
 
399
- async toolStart(call) {
400
- await ensureStepOpen();
401
- if (openTools.has(call.toolCallId)) return;
402
- openTools.set(call.toolCallId, call);
403
- await dispatcher.onToolExecutionStart?.(
404
- cast<'onToolExecutionStart'>({
405
- callId,
406
- messages: [],
407
- toolCall: {
408
- type: 'tool-call',
409
- toolCallId: call.toolCallId,
410
- toolName: call.toolName,
411
- input: call.input,
412
- dynamic: true,
413
- },
414
- toolContext: undefined,
415
- }),
306
+ async end({ steps, usage }) {
307
+ if (!started) await start();
308
+ if (ended || steps.length === 0) return;
309
+ ended = true;
310
+ const finalStep = steps[steps.length - 1]!;
311
+ const event = {
312
+ callId: options.callId,
313
+ stepNumber: finalStep.stepNumber,
314
+ model: finalStep.model,
315
+ toolsContext: finalStep.toolsContext,
316
+ runtimeContext: finalStep.runtimeContext,
317
+ content: steps.flatMap(step => step.content),
318
+ text: finalStep.text,
319
+ reasoning: finalStep.reasoning,
320
+ reasoningText: finalStep.reasoningText,
321
+ files: steps.flatMap(step => step.files),
322
+ sources: steps.flatMap(step => step.sources),
323
+ toolCalls: steps.flatMap(step => step.toolCalls),
324
+ staticToolCalls: steps.flatMap(step => step.staticToolCalls),
325
+ dynamicToolCalls: steps.flatMap(step => step.dynamicToolCalls),
326
+ toolResults: steps.flatMap(step => step.toolResults),
327
+ staticToolResults: steps.flatMap(step => step.staticToolResults),
328
+ dynamicToolResults: steps.flatMap(step => step.dynamicToolResults),
329
+ finishReason: finalStep.finishReason,
330
+ rawFinishReason: finalStep.rawFinishReason,
331
+ usage,
332
+ totalUsage: usage,
333
+ warnings: steps.flatMap(step => step.warnings ?? []),
334
+ request: finalStep.request,
335
+ response: finalStep.response,
336
+ providerMetadata: finalStep.providerMetadata,
337
+ responseMessages: steps.flatMap(step => step.response.messages),
338
+ steps,
339
+ finalStep,
340
+ };
341
+ await notify(
342
+ event,
343
+ options.callbacks.onEnd,
344
+ telemetry.onEnd as typeof options.callbacks.onEnd,
416
345
  );
417
346
  },
418
347
 
419
348
  async executeTool({ toolCallId, execute }) {
420
- if (dispatcher.executeTool == null) return await execute();
421
- return await dispatcher.executeTool({ callId, toolCallId, execute });
422
- },
423
-
424
- async toolEnd(toolCallId, output) {
425
- const call = openTools.get(toolCallId);
426
- const normalizedOutput = output.ok
427
- ? { type: 'tool-result' as const, output: output.output }
428
- : { type: 'error' as const, error: output.error };
429
- if (call == null) return;
430
- openTools.delete(toolCallId);
431
- await dispatcher.onToolExecutionEnd?.(
432
- cast<'onToolExecutionEnd'>({
433
- callId,
434
- toolExecutionMs: 0,
435
- messages: [],
436
- toolCall: {
437
- type: 'tool-call',
438
- toolCallId: call.toolCallId,
439
- toolName: call.toolName,
440
- input: call.input,
441
- dynamic: true,
442
- },
443
- toolContext: undefined,
444
- toolOutput: normalizedOutput,
445
- }),
446
- );
447
- },
448
-
449
- async end(info) {
450
- if (ended) return;
451
- const finishReason = normalizeFinishReason(info.finishReason);
452
- const usage = normalizeUsage(info.usage);
453
- if (!started) await fireStart();
454
- if (stepOpen) {
455
- await closeOpenTools();
456
- await inferenceEnd({
457
- finishReason,
458
- usage,
459
- content: [],
460
- });
461
- await dispatcher.onStepEnd?.(
462
- cast<'onStepEnd'>({
463
- callId,
464
- stepNumber,
465
- finishReason,
466
- usage,
467
- providerMetadata: undefined,
468
- content: [],
469
- response: {
470
- id: callId,
471
- modelId,
472
- timestamp: new Date(0),
473
- messages: [],
474
- },
475
- }),
476
- );
477
- stepOpen = false;
478
- }
479
- ended = true;
480
- await dispatcher.onEnd?.(
481
- cast<'onEnd'>({
482
- callId,
483
- operationId: 'ai.harness',
484
- finishReason,
485
- usage,
486
- totalUsage: usage,
487
- content: [],
488
- text: finalStepText,
489
- finalStep: {
490
- reasoning: finalStepReasoning,
491
- providerMetadata: finalStepProviderMetadata,
492
- },
493
- toolCalls: outputToolCalls,
494
- files: [],
495
- steps: new Array(stepNumber),
496
- response: {
497
- id: callId,
498
- modelId,
499
- timestamp: new Date(0),
500
- messages: [],
501
- },
502
- runtimeContext,
503
- }),
504
- );
349
+ if (telemetry.executeTool == null) return await execute();
350
+ return await telemetry.executeTool({
351
+ callId: options.callId,
352
+ toolCallId,
353
+ execute,
354
+ });
505
355
  },
506
356
 
507
- async error(err) {
357
+ async error(error) {
508
358
  if (ended) return;
509
- if (!started) await fireStart();
510
- await closeOpenTools();
359
+ if (!started) await start();
511
360
  ended = true;
512
- await dispatcher.onError?.(err);
361
+ await telemetry.onError?.(error);
513
362
  },
514
363
  };
515
364
  }