@ai-sdk/harness 0.0.0-6b196531-20260710185421
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/CHANGELOG.md +414 -0
- package/LICENSE +13 -0
- package/README.md +176 -0
- package/agent/index.ts +56 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1631 -0
- package/dist/agent/index.js +3491 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +129 -0
- package/dist/bridge/index.js +482 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1587 -0
- package/dist/index.js +517 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +329 -0
- package/dist/utils/index.js +1241 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +100 -0
- package/src/agent/harness-agent-session.ts +518 -0
- package/src/agent/harness-agent-settings.ts +187 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-tool-types.ts +15 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +865 -0
- package/src/agent/internal/bootstrap-recipe.ts +124 -0
- package/src/agent/internal/bridge-port-registry.ts +52 -0
- package/src/agent/internal/harness-stream-text-result.ts +731 -0
- package/src/agent/internal/lifecycle-state-validation.ts +95 -0
- package/src/agent/internal/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/run-prompt.ts +901 -0
- package/src/agent/internal/sandbox-bootstrap.ts +266 -0
- package/src/agent/internal/strip-work-dir.ts +68 -0
- package/src/agent/internal/to-harness-stream.ts +75 -0
- package/src/agent/internal/tool-filtering.ts +114 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +361 -0
- package/src/agent/observability/file-reporter.ts +206 -0
- package/src/agent/observability/index.ts +15 -0
- package/src/agent/observability/trace-tree-reporter.ts +122 -0
- package/src/agent/observability/types.ts +86 -0
- package/src/agent/prepare-harness-sandbox-template.ts +68 -0
- package/src/agent/prepare-sandbox-for-harness.ts +165 -0
- package/src/bridge/index.ts +797 -0
- package/src/errors/harness-capability-unsupported-error.ts +41 -0
- package/src/errors/harness-error.ts +22 -0
- package/src/index.ts +3 -0
- package/src/utils/ai-gateway-auth.ts +15 -0
- package/src/utils/bridge-diagnostics.ts +213 -0
- package/src/utils/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +31 -0
- package/src/utils/sandbox-channel.ts +525 -0
- package/src/utils/sandbox-home-dir.ts +22 -0
- package/src/utils/shell-quote.ts +3 -0
- package/src/utils/write-skills.ts +141 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +342 -0
- package/src/v1/harness-v1-builtin-tool.ts +138 -0
- package/src/v1/harness-v1-call-warning.ts +22 -0
- package/src/v1/harness-v1-diagnostic.ts +66 -0
- package/src/v1/harness-v1-lifecycle-state.ts +65 -0
- package/src/v1/harness-v1-metadata.ts +13 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
- package/src/v1/harness-v1-observability.ts +20 -0
- package/src/v1/harness-v1-permission-mode.ts +11 -0
- package/src/v1/harness-v1-prompt-control.ts +41 -0
- package/src/v1/harness-v1-prompt.ts +11 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +280 -0
- package/src/v1/harness-v1-skill.ts +36 -0
- package/src/v1/harness-v1-stream-part.ts +363 -0
- package/src/v1/harness-v1-tool-filtering.ts +25 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +94 -0
- package/src/v1/index.ts +99 -0
- package/utils/index.ts +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { HarnessV1StreamPart } from '../../v1';
|
|
2
|
+
import type { TextStreamPart } from 'ai';
|
|
3
|
+
import { generateId, type ToolSet } from '@ai-sdk/provider-utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Translate one event from the harness wire format to the AI SDK
|
|
7
|
+
* `TextStreamPart` shape consumed by `streamText` callers.
|
|
8
|
+
*
|
|
9
|
+
* Most variants are close to the identity function — V4 primitives
|
|
10
|
+
* (`LanguageModelV4ToolCall`, `LanguageModelV4ToolResult`,
|
|
11
|
+
* `LanguageModelV4ToolApprovalRequest`, `LanguageModelV4FinishReason`,
|
|
12
|
+
* `LanguageModelV4Usage`) flow through unchanged. The adapters are:
|
|
13
|
+
* - `harnessMetadata` → `providerMetadata`
|
|
14
|
+
* - tool-call events are not translated here — validation against the
|
|
15
|
+
* merged tool set is async and handled by `validateToolCall` in
|
|
16
|
+
* `run-prompt.ts`
|
|
17
|
+
* - the harness `raw` part is forwarded as the AI SDK `raw` part
|
|
18
|
+
*
|
|
19
|
+
* Returns an array of zero or more AI SDK parts. Most harness events project
|
|
20
|
+
* to a single AI SDK part; `file-change` fans out into a synthetic
|
|
21
|
+
* dynamic + provider-executed `tool-call` / `tool-result` pair so the event
|
|
22
|
+
* is observable in `streamText`-style flows without a new stream-part type
|
|
23
|
+
* needing first-class AI SDK support. Events with no consumer-facing AI SDK
|
|
24
|
+
* equivalent (`stream-start`, `finish-step`, `finish` — consumed internally)
|
|
25
|
+
* return an empty array.
|
|
26
|
+
*/
|
|
27
|
+
export function translateStreamPart<TOOLS extends ToolSet>(
|
|
28
|
+
event: HarnessV1StreamPart,
|
|
29
|
+
): ReadonlyArray<TextStreamPart<TOOLS>> {
|
|
30
|
+
switch (event.type) {
|
|
31
|
+
case 'stream-start':
|
|
32
|
+
// The agent emits its own `start` part with normalized warnings;
|
|
33
|
+
// the harness-level start signal is consumed internally.
|
|
34
|
+
return [];
|
|
35
|
+
|
|
36
|
+
case 'text-start':
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
type: 'text-start',
|
|
40
|
+
id: event.id,
|
|
41
|
+
providerMetadata: event.harnessMetadata,
|
|
42
|
+
} as TextStreamPart<TOOLS>,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
case 'text-delta':
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
type: 'text-delta',
|
|
49
|
+
id: event.id,
|
|
50
|
+
text: event.delta,
|
|
51
|
+
providerMetadata: event.harnessMetadata,
|
|
52
|
+
} as TextStreamPart<TOOLS>,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
case 'text-end':
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
type: 'text-end',
|
|
59
|
+
id: event.id,
|
|
60
|
+
providerMetadata: event.harnessMetadata,
|
|
61
|
+
} as TextStreamPart<TOOLS>,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
case 'reasoning-start':
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
type: 'reasoning-start',
|
|
68
|
+
id: event.id,
|
|
69
|
+
providerMetadata: event.harnessMetadata,
|
|
70
|
+
} as TextStreamPart<TOOLS>,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
case 'reasoning-delta':
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
type: 'reasoning-delta',
|
|
77
|
+
id: event.id,
|
|
78
|
+
text: event.delta,
|
|
79
|
+
providerMetadata: event.harnessMetadata,
|
|
80
|
+
} as TextStreamPart<TOOLS>,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
case 'reasoning-end':
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
type: 'reasoning-end',
|
|
87
|
+
id: event.id,
|
|
88
|
+
providerMetadata: event.harnessMetadata,
|
|
89
|
+
} as TextStreamPart<TOOLS>,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
case 'tool-call':
|
|
93
|
+
// Tool-call validation is async (it parses input against the tool's
|
|
94
|
+
// schema) and lives in `run-prompt.ts` where the merged tool set is in
|
|
95
|
+
// scope. The translator returns nothing here — the run-prompt loop
|
|
96
|
+
// handles the emission.
|
|
97
|
+
return [];
|
|
98
|
+
|
|
99
|
+
case 'tool-approval-request':
|
|
100
|
+
return [];
|
|
101
|
+
|
|
102
|
+
case 'tool-result':
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
type: 'tool-result',
|
|
106
|
+
toolCallId: event.toolCallId,
|
|
107
|
+
toolName: event.toolName,
|
|
108
|
+
input: undefined,
|
|
109
|
+
output: event.result,
|
|
110
|
+
...(event.preliminary !== undefined
|
|
111
|
+
? { preliminary: event.preliminary }
|
|
112
|
+
: {}),
|
|
113
|
+
...(event.providerMetadata !== undefined
|
|
114
|
+
? { providerMetadata: event.providerMetadata }
|
|
115
|
+
: {}),
|
|
116
|
+
} as TextStreamPart<TOOLS>,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
case 'file-change': {
|
|
120
|
+
/*
|
|
121
|
+
* `file-change` has no first-class AI SDK stream-part equivalent.
|
|
122
|
+
* Project it as a synthetic dynamic + provider-executed tool-call /
|
|
123
|
+
* tool-result pair under the reserved name `fileChange` so the event
|
|
124
|
+
* is visible to `streamText`-style consumers. `dynamic: true` keeps it
|
|
125
|
+
* out of typed-tool lookups; `providerExecuted: true` signals the
|
|
126
|
+
* runtime already executed it and the host should not dispatch.
|
|
127
|
+
*/
|
|
128
|
+
const toolCallId = `harness-file-change-${generateId()}`;
|
|
129
|
+
const payload = { event: event.event, path: event.path };
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
type: 'tool-call',
|
|
133
|
+
toolCallId,
|
|
134
|
+
toolName: 'fileChange',
|
|
135
|
+
input: payload,
|
|
136
|
+
dynamic: true,
|
|
137
|
+
providerExecuted: true,
|
|
138
|
+
...(event.harnessMetadata !== undefined
|
|
139
|
+
? { providerMetadata: event.harnessMetadata }
|
|
140
|
+
: {}),
|
|
141
|
+
} as TextStreamPart<TOOLS>,
|
|
142
|
+
{
|
|
143
|
+
type: 'tool-result',
|
|
144
|
+
toolCallId,
|
|
145
|
+
toolName: 'fileChange',
|
|
146
|
+
input: payload,
|
|
147
|
+
output: payload,
|
|
148
|
+
dynamic: true,
|
|
149
|
+
providerExecuted: true,
|
|
150
|
+
...(event.harnessMetadata !== undefined
|
|
151
|
+
? { providerMetadata: event.harnessMetadata }
|
|
152
|
+
: {}),
|
|
153
|
+
} as TextStreamPart<TOOLS>,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'compaction': {
|
|
158
|
+
/*
|
|
159
|
+
* Like `file-change`, compaction has no first-class AI SDK stream-part
|
|
160
|
+
* equivalent. Project it as a synthetic dynamic + provider-executed
|
|
161
|
+
* tool-call / tool-result pair under the reserved name `compaction`, so
|
|
162
|
+
* the event is visible to `streamText`-style consumers. Compaction takes
|
|
163
|
+
* no input, so the call input is empty; the metadata rides on the result
|
|
164
|
+
* output. `dynamic: true` keeps it out of typed-tool lookups;
|
|
165
|
+
* `providerExecuted: true` signals the runtime already performed it.
|
|
166
|
+
*/
|
|
167
|
+
const toolCallId = `harness-compaction-${generateId()}`;
|
|
168
|
+
const output = {
|
|
169
|
+
trigger: event.trigger,
|
|
170
|
+
summary: event.summary,
|
|
171
|
+
...(event.tokensBefore !== undefined
|
|
172
|
+
? { tokensBefore: event.tokensBefore }
|
|
173
|
+
: {}),
|
|
174
|
+
...(event.tokensAfter !== undefined
|
|
175
|
+
? { tokensAfter: event.tokensAfter }
|
|
176
|
+
: {}),
|
|
177
|
+
};
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
type: 'tool-call',
|
|
181
|
+
toolCallId,
|
|
182
|
+
toolName: 'compaction',
|
|
183
|
+
input: {},
|
|
184
|
+
dynamic: true,
|
|
185
|
+
providerExecuted: true,
|
|
186
|
+
...(event.harnessMetadata !== undefined
|
|
187
|
+
? { providerMetadata: event.harnessMetadata }
|
|
188
|
+
: {}),
|
|
189
|
+
} as TextStreamPart<TOOLS>,
|
|
190
|
+
{
|
|
191
|
+
type: 'tool-result',
|
|
192
|
+
toolCallId,
|
|
193
|
+
toolName: 'compaction',
|
|
194
|
+
input: {},
|
|
195
|
+
output,
|
|
196
|
+
dynamic: true,
|
|
197
|
+
providerExecuted: true,
|
|
198
|
+
...(event.harnessMetadata !== undefined
|
|
199
|
+
? { providerMetadata: event.harnessMetadata }
|
|
200
|
+
: {}),
|
|
201
|
+
} as TextStreamPart<TOOLS>,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'error':
|
|
206
|
+
return [{ type: 'error', error: event.error } as TextStreamPart<TOOLS>];
|
|
207
|
+
|
|
208
|
+
case 'raw':
|
|
209
|
+
return [
|
|
210
|
+
{ type: 'raw', rawValue: event.rawValue } as TextStreamPart<TOOLS>,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
case 'finish-step':
|
|
214
|
+
case 'finish':
|
|
215
|
+
// finish-step / finish are consumed by the agent's result builder, not
|
|
216
|
+
// forwarded directly. The agent emits AI SDK `finish-step` / `finish`
|
|
217
|
+
// parts itself once it has assembled the surrounding step / response
|
|
218
|
+
// metadata.
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
stepNumber,
|
|
245
|
+
finishReason: info.finishReason,
|
|
246
|
+
usage: info.usage,
|
|
247
|
+
providerMetadata: info.providerMetadata,
|
|
248
|
+
content,
|
|
249
|
+
response: {
|
|
250
|
+
id: callId,
|
|
251
|
+
modelId,
|
|
252
|
+
timestamp: new Date(0),
|
|
253
|
+
messages: [],
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
stepOpen = false;
|
|
258
|
+
stepNumber += 1;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
toolStart(call) {
|
|
262
|
+
ensureStepOpen();
|
|
263
|
+
openTools.set(call.toolCallId, call);
|
|
264
|
+
dispatcher.onToolExecutionStart?.(
|
|
265
|
+
cast<'onToolExecutionStart'>({
|
|
266
|
+
callId,
|
|
267
|
+
messages: [],
|
|
268
|
+
toolCall: {
|
|
269
|
+
type: 'tool-call',
|
|
270
|
+
toolCallId: call.toolCallId,
|
|
271
|
+
toolName: call.toolName,
|
|
272
|
+
input: call.input,
|
|
273
|
+
dynamic: true,
|
|
274
|
+
},
|
|
275
|
+
toolContext: undefined,
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
toolEnd(toolCallId, output) {
|
|
281
|
+
const call = openTools.get(toolCallId);
|
|
282
|
+
if (call == null) return;
|
|
283
|
+
openTools.delete(toolCallId);
|
|
284
|
+
dispatcher.onToolExecutionEnd?.(
|
|
285
|
+
cast<'onToolExecutionEnd'>({
|
|
286
|
+
callId,
|
|
287
|
+
toolExecutionMs: 0,
|
|
288
|
+
messages: [],
|
|
289
|
+
toolCall: {
|
|
290
|
+
type: 'tool-call',
|
|
291
|
+
toolCallId: call.toolCallId,
|
|
292
|
+
toolName: call.toolName,
|
|
293
|
+
input: call.input,
|
|
294
|
+
dynamic: true,
|
|
295
|
+
},
|
|
296
|
+
toolContext: undefined,
|
|
297
|
+
toolOutput: output.ok
|
|
298
|
+
? { type: 'tool-result', output: output.output }
|
|
299
|
+
: { type: 'error', error: output.error },
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
end(info) {
|
|
305
|
+
if (ended) return;
|
|
306
|
+
if (!started) fireStart();
|
|
307
|
+
if (stepOpen) {
|
|
308
|
+
closeOpenTools();
|
|
309
|
+
inferenceEnd({
|
|
310
|
+
finishReason: info.finishReason,
|
|
311
|
+
usage: info.usage,
|
|
312
|
+
content: [],
|
|
313
|
+
});
|
|
314
|
+
dispatcher.onStepEnd?.(
|
|
315
|
+
cast<'onStepEnd'>({
|
|
316
|
+
callId,
|
|
317
|
+
stepNumber,
|
|
318
|
+
finishReason: info.finishReason,
|
|
319
|
+
usage: info.usage,
|
|
320
|
+
providerMetadata: undefined,
|
|
321
|
+
content: [],
|
|
322
|
+
response: {
|
|
323
|
+
id: callId,
|
|
324
|
+
modelId,
|
|
325
|
+
timestamp: new Date(0),
|
|
326
|
+
messages: [],
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
stepOpen = false;
|
|
331
|
+
}
|
|
332
|
+
ended = true;
|
|
333
|
+
dispatcher.onEnd?.(
|
|
334
|
+
cast<'onEnd'>({
|
|
335
|
+
callId,
|
|
336
|
+
operationId: 'ai.harness',
|
|
337
|
+
finishReason: info.finishReason,
|
|
338
|
+
usage: info.usage,
|
|
339
|
+
totalUsage: info.usage,
|
|
340
|
+
content: [],
|
|
341
|
+
steps: new Array(stepNumber),
|
|
342
|
+
response: {
|
|
343
|
+
id: callId,
|
|
344
|
+
modelId,
|
|
345
|
+
timestamp: new Date(0),
|
|
346
|
+
messages: [],
|
|
347
|
+
},
|
|
348
|
+
runtimeContext,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
error(err) {
|
|
354
|
+
if (ended) return;
|
|
355
|
+
if (!started) fireStart();
|
|
356
|
+
closeOpenTools();
|
|
357
|
+
ended = true;
|
|
358
|
+
dispatcher.onError?.(err);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|