@ai-sdk/harness 1.0.101 → 1.0.102

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.101",
3
+ "version": "1.0.102",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.10",
48
48
  "@ai-sdk/provider-utils": "5.0.36",
49
- "ai": "7.0.92"
49
+ "ai": "7.0.93"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@ai-sdk/otel": "1.0.92",
61
+ "@ai-sdk/otel": "1.0.93",
62
62
  "@opentelemetry/sdk-trace-base": "2.7.1",
63
63
  "@types/node": "22.19.19",
64
64
  "@types/ws": "^8.5.13",
@@ -34,6 +34,7 @@ import type {
34
34
  import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
35
35
  import { runPrompt } from './internal/run-prompt';
36
36
  import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
37
+ import type { HarnessAgentLifecycleCallbacks } from './internal/turn-telemetry';
37
38
 
38
39
  type HarnessAgentTurnResult<
39
40
  TOOLS extends ToolSet,
@@ -207,6 +208,7 @@ export class HarnessAgentSession {
207
208
  responseFormat: HarnessV1ResponseFormat | undefined;
208
209
  output: OUTPUT | undefined;
209
210
  telemetry: TelemetryOptions | undefined;
211
+ callbacks: HarnessAgentLifecycleCallbacks<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
210
212
  stopConditions: ReadonlyArray<StopCondition<TOOLS, RUNTIME_CONTEXT>>;
211
213
  }): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
212
214
  const session = this.requireReusableSession();
@@ -246,6 +248,7 @@ export class HarnessAgentSession {
246
248
  responseFormat: options.responseFormat,
247
249
  output: options.output,
248
250
  telemetry: options.telemetry,
251
+ callbacks: options.callbacks,
249
252
  stopConditions: options.stopConditions,
250
253
  toolApproval: this.toolApproval,
251
254
  pendingToolApprovals: this.getPendingToolApprovals(),
@@ -305,6 +308,7 @@ export class HarnessAgentSession {
305
308
  responseFormat: HarnessV1ResponseFormat | undefined;
306
309
  output: OUTPUT | undefined;
307
310
  telemetry: TelemetryOptions | undefined;
311
+ callbacks: HarnessAgentLifecycleCallbacks<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
308
312
  stopConditions: ReadonlyArray<StopCondition<TOOLS, RUNTIME_CONTEXT>>;
309
313
  toolApprovalContinuations?: readonly ToolApprovalResponse[] | undefined;
310
314
  toolResultContinuations?: readonly ToolResultPart[] | undefined;
@@ -341,6 +345,7 @@ export class HarnessAgentSession {
341
345
  responseFormat: options.responseFormat,
342
346
  output: options.output,
343
347
  telemetry: options.telemetry,
348
+ callbacks: options.callbacks,
344
349
  stopConditions: options.stopConditions,
345
350
  toolApproval: this.toolApproval,
346
351
  pendingToolApprovals: this.getPendingToolApprovals(),
@@ -19,6 +19,14 @@ import type {
19
19
  import type {
20
20
  ActiveTools,
21
21
  AgentCallParameters,
22
+ GenerateTextOnEndCallback,
23
+ GenerateTextOnStartCallback,
24
+ GenerateTextOnStepEndCallback,
25
+ GenerateTextOnStepStartCallback,
26
+ OnLanguageModelCallEndCallback,
27
+ OnLanguageModelCallStartCallback,
28
+ OnToolExecutionEndCallback,
29
+ OnToolExecutionStartCallback,
22
30
  OutputInterface as Output,
23
31
  Prompt,
24
32
  StopCondition,
@@ -78,10 +86,12 @@ type HarnessTools<TOOLS extends ToolSet> = ActiveTools<NoInfer<TOOLS>>;
78
86
  /**
79
87
  * Construction-time settings for a `HarnessAgent`.
80
88
  *
81
- * Prompt, abortSignal, callbacks, and custom call options belong on the
89
+ * Prompt, abortSignal, and custom call options belong on the
82
90
  * `AgentCallParameters` / `AgentStreamParameters` passed to `generate` /
83
- * `stream`. `prepareCall` can derive turn-scoped model, skills, instructions,
84
- * and tools from those custom call options.
91
+ * `stream`. Lifecycle callbacks can be configured here for every call, while
92
+ * the callbacks supported by `AgentCallParameters` can also be added per call.
93
+ * `prepareCall` can derive turn-scoped model, skills, instructions, and tools
94
+ * from custom call options.
85
95
  */
86
96
  type HarnessAgentToolFilteringSettings<TOOLS extends ToolSet> =
87
97
  | {
@@ -153,6 +163,14 @@ export type HarnessAgentSettings<
153
163
  */
154
164
  readonly instructions?: string;
155
165
 
166
+ /**
167
+ * Additional HTTP headers to be sent with every model request.
168
+ *
169
+ * `authorization`, `x-api-key`, `user-agent`, and `x-client-app` are
170
+ * managed by the harness and are not allowed.
171
+ */
172
+ readonly headers?: Record<string, string | undefined>;
173
+
156
174
  /**
157
175
  * Schema for validating the custom options passed to each agent call.
158
176
  */
@@ -242,6 +260,67 @@ export type HarnessAgentSettings<
242
260
  >
243
261
  >;
244
262
 
263
+ /**
264
+ * Called when an agent call begins, before any model steps.
265
+ */
266
+ readonly onStart?: GenerateTextOnStartCallback<
267
+ NoInfer<HarnessAllTools<THarness, TUserTools>>,
268
+ RUNTIME_CONTEXT,
269
+ NoInfer<OUTPUT>
270
+ >;
271
+
272
+ /**
273
+ * Called when a model step begins.
274
+ */
275
+ readonly onStepStart?: GenerateTextOnStepStartCallback<
276
+ NoInfer<HarnessAllTools<THarness, TUserTools>>,
277
+ NoInfer<RUNTIME_CONTEXT>,
278
+ NoInfer<OUTPUT>
279
+ >;
280
+
281
+ /**
282
+ * Called immediately before the harness begins emitting a model response.
283
+ */
284
+ readonly onLanguageModelCallStart?: OnLanguageModelCallStartCallback;
285
+
286
+ /**
287
+ * Called after a model response is complete and before its tool execution
288
+ * lifecycle callbacks are delivered.
289
+ */
290
+ readonly onLanguageModelCallEnd?: OnLanguageModelCallEndCallback<
291
+ NoInfer<HarnessAllTools<THarness, TUserTools>>
292
+ >;
293
+
294
+ /**
295
+ * Called before each harness or host tool execution is reported.
296
+ */
297
+ readonly onToolExecutionStart?: OnToolExecutionStartCallback<
298
+ NoInfer<HarnessAllTools<THarness, TUserTools>>
299
+ >;
300
+
301
+ /**
302
+ * Called after each harness or host tool execution is reported.
303
+ */
304
+ readonly onToolExecutionEnd?: OnToolExecutionEndCallback<
305
+ NoInfer<HarnessAllTools<THarness, TUserTools>>
306
+ >;
307
+
308
+ /**
309
+ * Called after each completed model step.
310
+ */
311
+ readonly onStepEnd?: GenerateTextOnStepEndCallback<
312
+ NoInfer<HarnessAllTools<THarness, TUserTools>>,
313
+ NoInfer<RUNTIME_CONTEXT>
314
+ >;
315
+
316
+ /**
317
+ * Called when an agent call completes successfully.
318
+ */
319
+ readonly onEnd?: GenerateTextOnEndCallback<
320
+ NoInfer<HarnessAllTools<THarness, TUserTools>>,
321
+ NoInfer<RUNTIME_CONTEXT>
322
+ >;
323
+
245
324
  /**
246
325
  * Built-in tool permission mode. Defaults to `'allow-all'`, preserving the
247
326
  * existing bypass-permissions behavior unless users opt in.
@@ -10,6 +10,7 @@ import {
10
10
  asArray,
11
11
  asSchema,
12
12
  generateId,
13
+ normalizeHeaders,
13
14
  validateTypes,
14
15
  type Context,
15
16
  type Experimental_SandboxSession as SandboxSession,
@@ -18,6 +19,7 @@ import {
18
19
  type ToolResultPart,
19
20
  type ToolSet,
20
21
  } from '@ai-sdk/provider-utils';
22
+ import { mergeCallbacks } from 'ai/internal';
21
23
  import type {
22
24
  Agent,
23
25
  AgentCallParameters,
@@ -65,6 +67,7 @@ import {
65
67
  import { resolveHarnessAgentToolFiltering } from './internal/tool-filtering';
66
68
  import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
67
69
  import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
70
+ import type { HarnessAgentLifecycleCallbacks } from './internal/turn-telemetry';
68
71
 
69
72
  export type { HarnessAllTools } from './harness-agent-tool-types';
70
73
 
@@ -203,6 +206,7 @@ export class HarnessAgent<
203
206
  | HarnessV1BuiltinToolFiltering
204
207
  | undefined;
205
208
  private readonly permissionMode: HarnessAgentPermissionMode;
209
+ private readonly headers: Readonly<Record<string, string>> | undefined;
206
210
 
207
211
  constructor(
208
212
  settings: HarnessAgentSettings<
@@ -219,6 +223,22 @@ export class HarnessAgent<
219
223
  this.stopConditions =
220
224
  settings.stopWhen == null ? [] : asArray(settings.stopWhen);
221
225
  this.sandboxConfig = sandboxConfig;
226
+ const forbiddenHeaders = new Set([
227
+ 'authorization',
228
+ 'x-api-key',
229
+ 'user-agent',
230
+ 'x-client-app',
231
+ ]);
232
+ const forbiddenHeader = Object.keys(settings.headers ?? {})
233
+ .map(name => name.toLowerCase())
234
+ .find(name => forbiddenHeaders.has(name));
235
+ if (forbiddenHeader != null) {
236
+ throw new Error(
237
+ `HarnessAgent: \`headers\` must not include the managed header \`${forbiddenHeader}\`.`,
238
+ );
239
+ }
240
+ const headers = normalizeHeaders(settings.headers);
241
+ this.headers = Object.keys(headers).length === 0 ? undefined : headers;
222
242
  this.id = settings.id;
223
243
  const userTools = settings.tools ?? ({} as TUserTools);
224
244
  assertNoReservedQuestionTool({
@@ -502,6 +522,7 @@ export class HarnessAgent<
502
522
  try {
503
523
  const baseStartOptions = {
504
524
  sessionId,
525
+ ...(this.headers == null ? {} : { headers: this.headers }),
505
526
  resumeFrom: validatedResumeFrom,
506
527
  continueFrom: effectiveContinueFrom,
507
528
  permissionMode: this.permissionMode,
@@ -708,6 +729,7 @@ export class HarnessAgent<
708
729
  runtimeContext: input.runtimeContext,
709
730
  abortSignal: input.options.abortSignal,
710
731
  responseFormat,
732
+ callbacks: this._resolveLifecycleCallbacks(input.options),
711
733
  }),
712
734
  prompt: turnInput.prompt,
713
735
  });
@@ -734,6 +756,7 @@ export class HarnessAgent<
734
756
  runtimeContext: input.runtimeContext,
735
757
  abortSignal: input.abortSignal,
736
758
  responseFormat,
759
+ callbacks: this._resolveLifecycleCallbacks(),
737
760
  }),
738
761
  toolApprovalContinuations: turnInput.toolApprovalContinuations,
739
762
  toolResultContinuations: turnInput.toolResultContinuations,
@@ -745,6 +768,11 @@ export class HarnessAgent<
745
768
  runtimeContext: RUNTIME_CONTEXT;
746
769
  abortSignal: AbortSignal | undefined;
747
770
  responseFormat: HarnessV1ResponseFormat | undefined;
771
+ callbacks: HarnessAgentLifecycleCallbacks<
772
+ HarnessAllTools<THarness, TUserTools>,
773
+ RUNTIME_CONTEXT,
774
+ OUTPUT
775
+ >;
748
776
  }) {
749
777
  return {
750
778
  model: input.turnSettings.model,
@@ -759,10 +787,49 @@ export class HarnessAgent<
759
787
  responseFormat: input.responseFormat,
760
788
  output: this.settings.output,
761
789
  telemetry: this.settings.telemetry,
790
+ callbacks: input.callbacks,
762
791
  stopConditions: this.stopConditions,
763
792
  };
764
793
  }
765
794
 
795
+ private _resolveLifecycleCallbacks(
796
+ call?: AgentCallParameters<
797
+ CALL_OPTIONS,
798
+ HarnessAllTools<THarness, TUserTools>,
799
+ RUNTIME_CONTEXT
800
+ >,
801
+ ): HarnessAgentLifecycleCallbacks<
802
+ HarnessAllTools<THarness, TUserTools>,
803
+ RUNTIME_CONTEXT,
804
+ OUTPUT
805
+ > {
806
+ return {
807
+ onStart: mergeCallbacks(
808
+ this.settings.onStart,
809
+ call?.onStart ?? call?.experimental_onStart,
810
+ ),
811
+ onStepStart: mergeCallbacks(
812
+ this.settings.onStepStart,
813
+ call?.onStepStart ?? call?.experimental_onStepStart,
814
+ ),
815
+ onLanguageModelCallStart: this.settings.onLanguageModelCallStart,
816
+ onLanguageModelCallEnd: this.settings.onLanguageModelCallEnd,
817
+ onToolExecutionStart: mergeCallbacks(
818
+ this.settings.onToolExecutionStart,
819
+ call?.onToolExecutionStart ?? call?.experimental_onToolCallStart,
820
+ ),
821
+ onToolExecutionEnd: mergeCallbacks(
822
+ this.settings.onToolExecutionEnd,
823
+ call?.onToolExecutionEnd ?? call?.experimental_onToolCallFinish,
824
+ ),
825
+ onStepEnd: mergeCallbacks(
826
+ this.settings.onStepEnd,
827
+ call?.onStepEnd ?? call?.onStepFinish,
828
+ ),
829
+ onEnd: mergeCallbacks(this.settings.onEnd, call?.onEnd ?? call?.onFinish),
830
+ };
831
+ }
832
+
766
833
  private _resolveContinueTurnInput(options: {
767
834
  prompt?: string | ModelMessage[];
768
835
  messages?: ModelMessage[];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DelayedPromise,
3
- generateId,
3
+ type InferToolSetContext,
4
4
  type AssistantModelMessage,
5
5
  type Context,
6
6
  type ToolModelMessage,
@@ -154,9 +154,10 @@ export class HarnessStreamTextResult<
154
154
 
155
155
  private readonly tools: TOOLS;
156
156
  private readonly runtimeContext: RUNTIME_CONTEXT;
157
- private readonly toolsContext: never;
157
+ private readonly toolsContext: InferToolSetContext<TOOLS>;
158
158
  private readonly providerName: string;
159
- private readonly modelId: string;
159
+ private readonly callId: string;
160
+ private modelId: string;
160
161
  private readonly outputSpecification: OUTPUT | undefined;
161
162
 
162
163
  // Accumulators that span the whole turn.
@@ -170,16 +171,18 @@ export class HarnessStreamTextResult<
170
171
  constructor(options: {
171
172
  tools: TOOLS;
172
173
  runtimeContext: RUNTIME_CONTEXT;
173
- toolsContext: never;
174
+ toolsContext: InferToolSetContext<TOOLS>;
174
175
  harnessId: string;
175
- sessionId: string;
176
+ callId: string;
177
+ modelId: string;
176
178
  output: OUTPUT | undefined;
177
179
  }) {
178
180
  this.tools = options.tools;
179
181
  this.runtimeContext = options.runtimeContext;
180
182
  this.toolsContext = options.toolsContext;
181
183
  this.providerName = `harness:${options.harnessId}`;
182
- this.modelId = options.sessionId;
184
+ this.callId = options.callId;
185
+ this.modelId = options.modelId;
183
186
  this.outputSpecification = options.output;
184
187
 
185
188
  let controllerRef!: ReadableStreamDefaultController<TextStreamPart<TOOLS>>;
@@ -224,6 +227,12 @@ export class HarnessStreamTextResult<
224
227
  this.appendToCurrentStepContent(part);
225
228
  }
226
229
 
230
+ setModelId(modelId: string): void {
231
+ if (this.stepsBuffer.length === 0) {
232
+ this.modelId = modelId;
233
+ }
234
+ }
235
+
227
236
  /**
228
237
  * Push a continuation input into the consumer stream without attributing it
229
238
  * to the next model step. Approval responses and client tool results arrive
@@ -262,7 +271,7 @@ export class HarnessStreamTextResult<
262
271
  const rawFinishReason = input.finishReason.raw;
263
272
 
264
273
  const step = new DefaultStepResult<TOOLS, RUNTIME_CONTEXT>({
265
- callId: generateId(),
274
+ callId: this.callId,
266
275
  stepNumber: this.stepNumber,
267
276
  provider: this.providerName,
268
277
  modelId: this.modelId,
@@ -276,7 +285,7 @@ export class HarnessStreamTextResult<
276
285
  warnings: input.warnings.length > 0 ? input.warnings : undefined,
277
286
  request: {},
278
287
  response: {
279
- id: generateId(),
288
+ id: `${this.callId}-${this.stepNumber}`,
280
289
  timestamp: new Date(),
281
290
  modelId: this.modelId,
282
291
  messages: [],
@@ -363,7 +372,7 @@ export class HarnessStreamTextResult<
363
372
  this.stepsBuffer.length > 0
364
373
  ? this.stepsBuffer[this.stepsBuffer.length - 1]!
365
374
  : new DefaultStepResult<TOOLS, RUNTIME_CONTEXT>({
366
- callId: generateId(),
375
+ callId: this.callId,
367
376
  stepNumber: 0,
368
377
  provider: this.providerName,
369
378
  modelId: this.modelId,
@@ -377,7 +386,7 @@ export class HarnessStreamTextResult<
377
386
  warnings: undefined,
378
387
  request: {},
379
388
  response: {
380
- id: generateId(),
389
+ id: `${this.callId}-0`,
381
390
  timestamp: new Date(),
382
391
  modelId: this.modelId,
383
392
  messages: [],
@@ -823,6 +832,19 @@ export class HarnessStreamTextResult<
823
832
  }
824
833
  return;
825
834
  }
835
+ case 'reasoning-delta': {
836
+ const last =
837
+ this.currentStepContent[this.currentStepContent.length - 1];
838
+ if (last && last.type === 'reasoning') {
839
+ (last as { text: string }).text += part.text;
840
+ } else {
841
+ this.currentStepContent.push({
842
+ type: 'reasoning',
843
+ text: part.text,
844
+ } as ContentPart<TOOLS>);
845
+ }
846
+ return;
847
+ }
826
848
  case 'tool-call':
827
849
  case 'tool-approval-request':
828
850
  case 'tool-approval-response':
@@ -833,9 +855,7 @@ export class HarnessStreamTextResult<
833
855
  } as ContentPart<TOOLS>);
834
856
  return;
835
857
  default:
836
- // text-start/end, reasoning-*, raw, error, finish-step, finish are
837
- // not directly stored as ContentParts. (Reasoning content parts
838
- // would belong here; we omit them for v0.)
858
+ // Boundary, raw, error, and finish parts are not ContentParts.
839
859
  return;
840
860
  }
841
861
  }