@ai-sdk/harness 0.0.0 → 1.0.0-canary.3

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +13 -0
  3. package/README.md +142 -0
  4. package/agent/index.ts +17 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1480 -0
  7. package/dist/agent/index.js +2554 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/bridge/index.d.ts +111 -0
  10. package/dist/bridge/index.js +414 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1510 -0
  13. package/dist/index.js +15834 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/observability/index.d.ts +97 -0
  16. package/dist/observability/index.js +225 -0
  17. package/dist/observability/index.js.map +1 -0
  18. package/dist/utils/index.d.ts +196 -0
  19. package/dist/utils/index.js +327 -0
  20. package/dist/utils/index.js.map +1 -0
  21. package/package.json +104 -1
  22. package/src/agent/harness-agent-session.ts +352 -0
  23. package/src/agent/harness-agent-settings.ts +131 -0
  24. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  25. package/src/agent/harness-agent.ts +750 -0
  26. package/src/agent/harness-diagnostics.ts +88 -0
  27. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  28. package/src/agent/internal/bridge-port-registry.ts +52 -0
  29. package/src/agent/internal/harness-stream-text-result.ts +720 -0
  30. package/src/agent/internal/permission-mode.ts +50 -0
  31. package/src/agent/internal/resolve-observability.ts +128 -0
  32. package/src/agent/internal/resume-state-validation.ts +51 -0
  33. package/src/agent/internal/run-prompt.ts +811 -0
  34. package/src/agent/internal/strip-work-dir.ts +68 -0
  35. package/src/agent/internal/to-harness-stream.ts +75 -0
  36. package/src/agent/internal/translate-stream-part.ts +221 -0
  37. package/src/agent/internal/turn-telemetry.ts +359 -0
  38. package/src/agent/prewarm.ts +46 -0
  39. package/src/bridge/index.ts +700 -0
  40. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  41. package/src/errors/harness-error.ts +22 -0
  42. package/src/index.ts +3 -0
  43. package/src/observability/file-reporter.ts +209 -0
  44. package/src/observability/index.ts +13 -0
  45. package/src/observability/trace-tree-reporter.ts +122 -0
  46. package/src/utils/classify-disk-log.ts +43 -0
  47. package/src/utils/index.ts +7 -0
  48. package/src/utils/sandbox-channel.ts +453 -0
  49. package/src/v1/harness-v1-bootstrap.ts +46 -0
  50. package/src/v1/harness-v1-bridge-protocol.ts +310 -0
  51. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  52. package/src/v1/harness-v1-call-warning.ts +22 -0
  53. package/src/v1/harness-v1-diagnostic.ts +66 -0
  54. package/src/v1/harness-v1-metadata.ts +13 -0
  55. package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
  56. package/src/v1/harness-v1-observability.ts +20 -0
  57. package/src/v1/harness-v1-permission-mode.ts +11 -0
  58. package/src/v1/harness-v1-prompt-control.ts +41 -0
  59. package/src/v1/harness-v1-prompt.ts +11 -0
  60. package/src/v1/harness-v1-resume-state.ts +46 -0
  61. package/src/v1/harness-v1-sandbox-provider.ts +76 -0
  62. package/src/v1/harness-v1-session.ts +268 -0
  63. package/src/v1/harness-v1-skill.ts +22 -0
  64. package/src/v1/harness-v1-stream-part.ts +363 -0
  65. package/src/v1/harness-v1-tool-spec.ts +31 -0
  66. package/src/v1/harness-v1.ts +83 -0
  67. package/src/v1/index.ts +93 -0
  68. package/utils/index.ts +1 -0
@@ -0,0 +1,359 @@
1
+ import { generateId, type ModelMessage } from '@ai-sdk/provider-utils';
2
+ import { createTelemetryDispatcher } from 'ai/internal';
3
+ import type { TelemetryOptions } from 'ai';
4
+
5
+ /*
6
+ * Drives AI SDK's pluggable `Telemetry` lifecycle from a harness turn.
7
+ *
8
+ * A harness turn is not a `streamText` call — it has no language model, prompt
9
+ * standardization, or sampling settings — but the AI SDK telemetry contract is
10
+ * shaped around `generateText`/`streamText` events, and `@ai-sdk/otel` (the
11
+ * main integration) only produces spans when the full lifecycle fires. So we
12
+ * map the turn onto that contract: turn = operation, each `finish-step` = a
13
+ * step boundary, tool-calls = tool executions, `finish` = operation end. The
14
+ * model-call-only event fields the harness has no value for (sampling params,
15
+ * standardized prompt) are left `undefined` / cast; the fields the integrations
16
+ * actually read (`callId`, `operationId`, `provider`, `modelId`, `messages`,
17
+ * `toolCall`, `usage`, `finishReason`) carry real values.
18
+ *
19
+ * Telemetry is opt-in: the framework only drives it when `settings.telemetry`
20
+ * is set (the dispatcher then also honours globally-registered integrations).
21
+ */
22
+
23
+ type Dispatcher = ReturnType<typeof createTelemetryDispatcher>;
24
+ type EventArg<K extends keyof Dispatcher> = Dispatcher[K] extends
25
+ | ((event: infer E) => unknown)
26
+ | undefined
27
+ ? E
28
+ : never;
29
+
30
+ /**
31
+ * An output content part accumulated over a step — the model's assistant turn.
32
+ * Shaped for the gen_ai output-message conventions `@ai-sdk/otel` reads.
33
+ */
34
+ export type TurnContentPart =
35
+ | { type: 'text'; text: string }
36
+ | { type: 'reasoning'; text: string }
37
+ | { type: 'tool-call'; toolCallId: string; toolName: string; input: unknown };
38
+
39
+ export interface TurnTelemetry {
40
+ /**
41
+ * Begin the operation span. Called on `stream-start`, optionally with the
42
+ * model the runtime resolved to (overriding the session's configured id).
43
+ * Idempotent — the first call wins.
44
+ */
45
+ start(modelId?: string): void;
46
+ /** Open a step span lazily, before the first content of a step. */
47
+ ensureStepOpen(): void;
48
+ /** Close the current step (on a harness `finish-step`). */
49
+ stepFinish(info: {
50
+ finishReason: unknown;
51
+ usage: unknown;
52
+ providerMetadata?: unknown;
53
+ /** The model's output content for this step (text/reasoning/tool-calls). */
54
+ content?: TurnContentPart[];
55
+ }): void;
56
+ /** A tool execution began (on a `tool-call`). */
57
+ toolStart(call: {
58
+ toolCallId: string;
59
+ toolName: string;
60
+ input: unknown;
61
+ }): void;
62
+ /**
63
+ * A tool execution completed (on its `tool-result` or after host execution).
64
+ * Idempotent per `toolCallId` — the first caller wins, so provider-executed
65
+ * and host-executed paths can both call it without double-counting.
66
+ */
67
+ toolEnd(
68
+ toolCallId: string,
69
+ output: { ok: true; output: unknown } | { ok: false; error: unknown },
70
+ ): void;
71
+ /** The turn ended (on a harness `finish`). */
72
+ end(info: { finishReason: unknown; usage: unknown }): void;
73
+ /** The turn failed. */
74
+ error(err: unknown): void;
75
+ }
76
+
77
+ const NOOP: TurnTelemetry = {
78
+ start() {},
79
+ ensureStepOpen() {},
80
+ stepFinish() {},
81
+ toolStart() {},
82
+ toolEnd() {},
83
+ end() {},
84
+ error() {},
85
+ };
86
+
87
+ export function createTurnTelemetry(opts: {
88
+ telemetry: TelemetryOptions | undefined;
89
+ harnessId: string;
90
+ modelId: string | undefined;
91
+ instructions: string | undefined;
92
+ promptText: string;
93
+ runtimeContext: unknown;
94
+ }): TurnTelemetry {
95
+ // Opt-in: with no telemetry settings we do no work and construct no events.
96
+ if (opts.telemetry == null) return NOOP;
97
+
98
+ const dispatcher = createTelemetryDispatcher({ telemetry: opts.telemetry });
99
+
100
+ const callId = generateId();
101
+ const provider = opts.harnessId;
102
+ // The configured session model; `start(modelId)` may override it with the
103
+ // model the runtime actually resolved to.
104
+ let modelId = opts.modelId ?? '';
105
+ const runtimeContext = opts.runtimeContext;
106
+ const inputMessages: ModelMessage[] = [
107
+ { role: 'user', content: opts.promptText },
108
+ ];
109
+
110
+ let started = false;
111
+ let stepOpen = false;
112
+ let stepNumber = 0;
113
+ let ended = false;
114
+ /** Tool calls started in the current turn and not yet ended. */
115
+ const openTools = new Map<
116
+ string,
117
+ { toolCallId: string; toolName: string; input: unknown }
118
+ >();
119
+
120
+ const cast = <K extends keyof Dispatcher>(event: unknown): EventArg<K> =>
121
+ event as EventArg<K>;
122
+
123
+ // onStart — open the operation (root) span. Deferred until `start()` so the
124
+ // runtime-resolved model can be attached to the operation span + trace label.
125
+ const fireStart = (): void => {
126
+ if (started) return;
127
+ started = true;
128
+ dispatcher.onStart?.(
129
+ cast<'onStart'>({
130
+ callId,
131
+ operationId: 'ai.harness',
132
+ provider,
133
+ modelId,
134
+ tools: undefined,
135
+ toolChoice: undefined,
136
+ activeTools: undefined,
137
+ maxRetries: 0,
138
+ timeout: undefined,
139
+ headers: undefined,
140
+ providerOptions: undefined,
141
+ output: undefined,
142
+ toolsContext: undefined,
143
+ runtimeContext,
144
+ instructions: opts.instructions,
145
+ messages: inputMessages,
146
+ }),
147
+ );
148
+ };
149
+
150
+ const start = (overrideModelId?: string): void => {
151
+ if (started) return;
152
+ if (overrideModelId) modelId = overrideModelId;
153
+ fireStart();
154
+ };
155
+
156
+ const ensureStepOpen = (): void => {
157
+ if (!started) fireStart();
158
+ if (stepOpen || ended) return;
159
+ stepOpen = true;
160
+ dispatcher.onStepStart?.(
161
+ cast<'onStepStart'>({
162
+ callId,
163
+ provider,
164
+ modelId,
165
+ stepNumber,
166
+ tools: undefined,
167
+ toolChoice: undefined,
168
+ activeTools: undefined,
169
+ steps: new Array(stepNumber),
170
+ providerOptions: undefined,
171
+ output: undefined,
172
+ runtimeContext,
173
+ messages: inputMessages,
174
+ }),
175
+ );
176
+ // Open the inference (language-model call) span — the gen_ai home for the
177
+ // step's input and (on end) output messages.
178
+ dispatcher.onLanguageModelCallStart?.(
179
+ cast<'onLanguageModelCallStart'>({
180
+ callId,
181
+ provider,
182
+ modelId,
183
+ messages: inputMessages,
184
+ tools: undefined,
185
+ }),
186
+ );
187
+ };
188
+
189
+ /** Close the inference span with the step's output content. */
190
+ const inferenceEnd = (info: {
191
+ finishReason: unknown;
192
+ usage: unknown;
193
+ content: TurnContentPart[];
194
+ }): void => {
195
+ dispatcher.onLanguageModelCallEnd?.(
196
+ cast<'onLanguageModelCallEnd'>({
197
+ callId,
198
+ finishReason: info.finishReason,
199
+ responseId: callId,
200
+ usage: info.usage,
201
+ content: info.content,
202
+ }),
203
+ );
204
+ };
205
+
206
+ const closeOpenTools = (): void => {
207
+ for (const call of openTools.values()) {
208
+ dispatcher.onToolExecutionEnd?.(
209
+ cast<'onToolExecutionEnd'>({
210
+ callId,
211
+ toolExecutionMs: 0,
212
+ messages: [],
213
+ toolCall: {
214
+ type: 'tool-call',
215
+ toolCallId: call.toolCallId,
216
+ toolName: call.toolName,
217
+ input: call.input,
218
+ dynamic: true,
219
+ },
220
+ toolContext: undefined,
221
+ toolOutput: { type: 'error', error: new Error('tool span unclosed') },
222
+ }),
223
+ );
224
+ }
225
+ openTools.clear();
226
+ };
227
+
228
+ return {
229
+ start,
230
+ ensureStepOpen,
231
+
232
+ stepFinish(info) {
233
+ if (!stepOpen) return;
234
+ const content = info.content ?? [];
235
+ closeOpenTools();
236
+ inferenceEnd({
237
+ finishReason: info.finishReason,
238
+ usage: info.usage,
239
+ content,
240
+ });
241
+ dispatcher.onStepEnd?.(
242
+ cast<'onStepEnd'>({
243
+ callId,
244
+ finishReason: info.finishReason,
245
+ usage: info.usage,
246
+ providerMetadata: info.providerMetadata,
247
+ content,
248
+ response: {
249
+ id: callId,
250
+ modelId,
251
+ timestamp: new Date(0),
252
+ messages: [],
253
+ },
254
+ }),
255
+ );
256
+ stepOpen = false;
257
+ stepNumber += 1;
258
+ },
259
+
260
+ toolStart(call) {
261
+ ensureStepOpen();
262
+ openTools.set(call.toolCallId, call);
263
+ dispatcher.onToolExecutionStart?.(
264
+ cast<'onToolExecutionStart'>({
265
+ callId,
266
+ messages: [],
267
+ toolCall: {
268
+ type: 'tool-call',
269
+ toolCallId: call.toolCallId,
270
+ toolName: call.toolName,
271
+ input: call.input,
272
+ dynamic: true,
273
+ },
274
+ toolContext: undefined,
275
+ }),
276
+ );
277
+ },
278
+
279
+ toolEnd(toolCallId, output) {
280
+ const call = openTools.get(toolCallId);
281
+ if (call == null) return;
282
+ openTools.delete(toolCallId);
283
+ dispatcher.onToolExecutionEnd?.(
284
+ cast<'onToolExecutionEnd'>({
285
+ callId,
286
+ toolExecutionMs: 0,
287
+ messages: [],
288
+ toolCall: {
289
+ type: 'tool-call',
290
+ toolCallId: call.toolCallId,
291
+ toolName: call.toolName,
292
+ input: call.input,
293
+ dynamic: true,
294
+ },
295
+ toolContext: undefined,
296
+ toolOutput: output.ok
297
+ ? { type: 'tool-result', output: output.output }
298
+ : { type: 'error', error: output.error },
299
+ }),
300
+ );
301
+ },
302
+
303
+ end(info) {
304
+ if (ended) return;
305
+ if (!started) fireStart();
306
+ if (stepOpen) {
307
+ closeOpenTools();
308
+ inferenceEnd({
309
+ finishReason: info.finishReason,
310
+ usage: info.usage,
311
+ content: [],
312
+ });
313
+ dispatcher.onStepEnd?.(
314
+ cast<'onStepEnd'>({
315
+ callId,
316
+ finishReason: info.finishReason,
317
+ usage: info.usage,
318
+ providerMetadata: undefined,
319
+ content: [],
320
+ response: {
321
+ id: callId,
322
+ modelId,
323
+ timestamp: new Date(0),
324
+ messages: [],
325
+ },
326
+ }),
327
+ );
328
+ stepOpen = false;
329
+ }
330
+ ended = true;
331
+ dispatcher.onEnd?.(
332
+ cast<'onEnd'>({
333
+ callId,
334
+ operationId: 'ai.harness',
335
+ finishReason: info.finishReason,
336
+ usage: info.usage,
337
+ totalUsage: info.usage,
338
+ content: [],
339
+ steps: new Array(stepNumber),
340
+ response: {
341
+ id: callId,
342
+ modelId,
343
+ timestamp: new Date(0),
344
+ messages: [],
345
+ },
346
+ runtimeContext,
347
+ }),
348
+ );
349
+ },
350
+
351
+ error(err) {
352
+ if (ended) return;
353
+ if (!started) fireStart();
354
+ closeOpenTools();
355
+ ended = true;
356
+ dispatcher.onError?.(err);
357
+ },
358
+ };
359
+ }
@@ -0,0 +1,46 @@
1
+ import type { HarnessV1, HarnessV1SandboxProvider } from '../v1';
2
+ import {
3
+ applyBootstrapRecipe,
4
+ hashBootstrap,
5
+ } from './internal/bootstrap-recipe';
6
+
7
+ /**
8
+ * Pre-build a harness's sandbox template without running an agent. Idempotent:
9
+ * if the template already exists (snapshot present, or marker on a non-snapshot
10
+ * provider), this resolves quickly.
11
+ *
12
+ * Use from a CI/deploy script to amortize the first-session cost so production
13
+ * sessions always resume from snapshot. For adapters without a bootstrap
14
+ * recipe (no `getBootstrap`) this is a no-op.
15
+ *
16
+ * The temporary network sandbox session created during pre-warm is stopped
17
+ * before the function resolves; the snapshot/template state persists in the
18
+ * provider's native storage (for Vercel: as the `currentSnapshotId` of the
19
+ * named template sandbox).
20
+ */
21
+ export async function prewarmHarness(options: {
22
+ readonly harness: HarnessV1;
23
+ readonly sandboxProvider: HarnessV1SandboxProvider;
24
+ readonly abortSignal?: AbortSignal;
25
+ }): Promise<void> {
26
+ const recipe = await options.harness.getBootstrap?.({
27
+ abortSignal: options.abortSignal,
28
+ });
29
+ if (recipe == null) return;
30
+
31
+ const identity = await hashBootstrap(recipe);
32
+ const sandboxSession = await options.sandboxProvider.createSession({
33
+ abortSignal: options.abortSignal,
34
+ identity,
35
+ onFirstCreate: (session, opts) =>
36
+ applyBootstrapRecipe(session, recipe, identity, opts),
37
+ });
38
+
39
+ try {
40
+ await applyBootstrapRecipe(sandboxSession.restricted(), recipe, identity, {
41
+ abortSignal: options.abortSignal,
42
+ });
43
+ } finally {
44
+ await Promise.resolve(sandboxSession.stop()).catch(() => {});
45
+ }
46
+ }