@ai-sdk/harness 0.0.0 → 1.0.0-beta.14
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 +110 -0
- package/LICENSE +13 -0
- package/README.md +142 -0
- package/agent/index.ts +47 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1521 -0
- package/dist/agent/index.js +2958 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +111 -0
- package/dist/bridge/index.js +415 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1536 -0
- package/dist/index.js +15834 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +225 -0
- package/dist/utils/index.js +12148 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +99 -1
- package/src/agent/harness-agent-session.ts +509 -0
- package/src/agent/harness-agent-settings.ts +131 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +819 -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 +720 -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 +813 -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/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +359 -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/prewarm.ts +47 -0
- package/src/bridge/index.ts +702 -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/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/sandbox-channel.ts +453 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +310 -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 +272 -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-spec.ts +31 -0
- package/src/v1/harness-v1.ts +83 -0
- package/src/v1/index.ts +93 -0
- package/utils/index.ts +1 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HarnessV1StreamPart } from '../../v1';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove the session working-directory prefix from path-bearing fields of a
|
|
5
|
+
* stream event, returning a new event for display to consumers.
|
|
6
|
+
*
|
|
7
|
+
* Harness adapters run the agent in a per-session working directory that is a
|
|
8
|
+
* subdirectory of the sandbox root, and the agent's tools use absolute paths so
|
|
9
|
+
* they resolve against the root regardless of where the runtime process
|
|
10
|
+
* operates. The absolute paths are correct but noisy in a UI, so this strips
|
|
11
|
+
* the prefix for the consumer-facing projection only.
|
|
12
|
+
*
|
|
13
|
+
* Blanket prefix replacement (rather than rewriting known path fields) is used
|
|
14
|
+
* deliberately: `tool-result` results are free-form text — command stdout, grep
|
|
15
|
+
* output — where paths can appear anywhere and field-aware rewriting is
|
|
16
|
+
* impossible. The prefix is long and contains the session id, so it is unique
|
|
17
|
+
* enough that replacing every occurrence is safe.
|
|
18
|
+
*/
|
|
19
|
+
export function stripWorkDir(
|
|
20
|
+
part: HarnessV1StreamPart,
|
|
21
|
+
sessionWorkDir: string,
|
|
22
|
+
): HarnessV1StreamPart {
|
|
23
|
+
if (sessionWorkDir.length === 0) return part;
|
|
24
|
+
|
|
25
|
+
switch (part.type) {
|
|
26
|
+
case 'tool-call':
|
|
27
|
+
return { ...part, input: stripString(part.input, sessionWorkDir) };
|
|
28
|
+
case 'tool-result':
|
|
29
|
+
return {
|
|
30
|
+
...part,
|
|
31
|
+
result: stripDeep(part.result, sessionWorkDir) as Extract<
|
|
32
|
+
HarnessV1StreamPart,
|
|
33
|
+
{ type: 'tool-result' }
|
|
34
|
+
>['result'],
|
|
35
|
+
};
|
|
36
|
+
case 'file-change':
|
|
37
|
+
return { ...part, path: stripString(part.path, sessionWorkDir) };
|
|
38
|
+
default:
|
|
39
|
+
return part;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace occurrences of the working directory in a string. A reference to the
|
|
45
|
+
* directory followed by a separator becomes workspace-relative
|
|
46
|
+
* (`/work/dir/src/a.ts` → `src/a.ts`); a bare reference to the directory itself
|
|
47
|
+
* becomes `.`.
|
|
48
|
+
*/
|
|
49
|
+
function stripString(value: string, workDir: string): string {
|
|
50
|
+
return value.split(`${workDir}/`).join('').split(workDir).join('.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively strip the working directory from every string nested in an
|
|
55
|
+
* arbitrary JSON-like value. Non-string leaves are returned unchanged.
|
|
56
|
+
*/
|
|
57
|
+
function stripDeep(value: unknown, workDir: string): unknown {
|
|
58
|
+
if (typeof value === 'string') return stripString(value, workDir);
|
|
59
|
+
if (Array.isArray(value)) return value.map(item => stripDeep(item, workDir));
|
|
60
|
+
if (value !== null && typeof value === 'object') {
|
|
61
|
+
const out: Record<string, unknown> = {};
|
|
62
|
+
for (const [key, val] of Object.entries(value)) {
|
|
63
|
+
out[key] = stripDeep(val, workDir);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { HarnessV1PromptControl } from '../../v1/harness-v1-prompt-control';
|
|
2
|
+
import type { HarnessV1StreamPart } from '../../v1/harness-v1-stream-part';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bridge an adapter's emit-based event surface into a pull-based
|
|
6
|
+
* `ReadableStream<HarnessV1StreamPart>`.
|
|
7
|
+
*
|
|
8
|
+
* Adapters implement `doPromptTurn` / `doContinueTurn` against an `emit` callback
|
|
9
|
+
* because that is the natural shape when wrapping an SDK that itself produces
|
|
10
|
+
* events. Consumers (notably `HarnessAgent`) prefer a stream because that is
|
|
11
|
+
* the idiomatic AI SDK shape. This helper converts the former into the latter
|
|
12
|
+
* and is agnostic to which turn entry point produced the control surface — the
|
|
13
|
+
* caller supplies an `invoke` thunk that wires `emit` into either method.
|
|
14
|
+
*
|
|
15
|
+
* Lifetime:
|
|
16
|
+
* 1. Calls `invoke(emit)` immediately (which runs `doPromptTurn`/`doContinueTurn`).
|
|
17
|
+
* 2. Every `emit(part)` becomes a stream chunk.
|
|
18
|
+
* 3. When `control.done` resolves, the stream closes — this includes a
|
|
19
|
+
* graceful `doSuspendTurn`, which resolves `done` cleanly after draining.
|
|
20
|
+
* 4. When `control.done` rejects, an `{ type: 'error', error }` part is
|
|
21
|
+
* enqueued and the stream is then closed normally. The rejection is
|
|
22
|
+
* surfaced to consumers as a discriminated-union event rather than as
|
|
23
|
+
* a stream error so iteration code does not need a separate try/catch
|
|
24
|
+
* around the consumer loop.
|
|
25
|
+
* 5. The supplied `abortSignal` (if any) aborts the underlying turn and
|
|
26
|
+
* closes the stream.
|
|
27
|
+
*
|
|
28
|
+
* The returned `control` is the same object the adapter produced and is
|
|
29
|
+
* intended for use by the consumer to submit tool results / approvals /
|
|
30
|
+
* user messages back into the in-flight turn.
|
|
31
|
+
*/
|
|
32
|
+
export async function toHarnessStream(options: {
|
|
33
|
+
invoke: (
|
|
34
|
+
emit: (event: HarnessV1StreamPart) => void,
|
|
35
|
+
) => PromiseLike<HarnessV1PromptControl>;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
stream: ReadableStream<HarnessV1StreamPart>;
|
|
38
|
+
control: HarnessV1PromptControl;
|
|
39
|
+
}> {
|
|
40
|
+
let controller!: ReadableStreamDefaultController<HarnessV1StreamPart>;
|
|
41
|
+
let closed = false;
|
|
42
|
+
|
|
43
|
+
const stream = new ReadableStream<HarnessV1StreamPart>({
|
|
44
|
+
start(c) {
|
|
45
|
+
controller = c;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const safeEnqueue = (part: HarnessV1StreamPart) => {
|
|
50
|
+
if (closed) return;
|
|
51
|
+
controller.enqueue(part);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const safeClose = () => {
|
|
55
|
+
if (closed) return;
|
|
56
|
+
closed = true;
|
|
57
|
+
controller.close();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const control = await options.invoke(safeEnqueue);
|
|
61
|
+
|
|
62
|
+
Promise.resolve(control.done)
|
|
63
|
+
.then(
|
|
64
|
+
() => safeClose(),
|
|
65
|
+
(err: unknown) => {
|
|
66
|
+
safeEnqueue({ type: 'error', error: err });
|
|
67
|
+
safeClose();
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
// Belt-and-suspenders: any throw inside the handlers themselves should
|
|
71
|
+
// not become an unhandled rejection.
|
|
72
|
+
.catch(() => {});
|
|
73
|
+
|
|
74
|
+
return { stream, control };
|
|
75
|
+
}
|
|
@@ -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
|
+
}
|