@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +13 -0
  3. package/README.md +142 -0
  4. package/agent/index.ts +47 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1521 -0
  7. package/dist/agent/index.js +2958 -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 +415 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1536 -0
  13. package/dist/index.js +15834 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/index.d.ts +225 -0
  16. package/dist/utils/index.js +12148 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +99 -1
  19. package/src/agent/harness-agent-session.ts +509 -0
  20. package/src/agent/harness-agent-settings.ts +131 -0
  21. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  22. package/src/agent/harness-agent-types.ts +50 -0
  23. package/src/agent/harness-agent.ts +819 -0
  24. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  25. package/src/agent/internal/bridge-port-registry.ts +52 -0
  26. package/src/agent/internal/harness-stream-text-result.ts +720 -0
  27. package/src/agent/internal/lifecycle-state-validation.ts +95 -0
  28. package/src/agent/internal/permission-mode.ts +50 -0
  29. package/src/agent/internal/resolve-observability.ts +128 -0
  30. package/src/agent/internal/run-prompt.ts +813 -0
  31. package/src/agent/internal/strip-work-dir.ts +68 -0
  32. package/src/agent/internal/to-harness-stream.ts +75 -0
  33. package/src/agent/internal/translate-stream-part.ts +221 -0
  34. package/src/agent/internal/turn-telemetry.ts +359 -0
  35. package/src/agent/observability/file-reporter.ts +206 -0
  36. package/src/agent/observability/index.ts +15 -0
  37. package/src/agent/observability/trace-tree-reporter.ts +122 -0
  38. package/src/agent/observability/types.ts +86 -0
  39. package/src/agent/prewarm.ts +47 -0
  40. package/src/bridge/index.ts +702 -0
  41. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  42. package/src/errors/harness-error.ts +22 -0
  43. package/src/index.ts +3 -0
  44. package/src/utils/bridge-ready.ts +277 -0
  45. package/src/utils/classify-disk-log.ts +43 -0
  46. package/src/utils/index.ts +15 -0
  47. package/src/utils/sandbox-channel.ts +453 -0
  48. package/src/v1/harness-v1-bootstrap.ts +46 -0
  49. package/src/v1/harness-v1-bridge-protocol.ts +310 -0
  50. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  51. package/src/v1/harness-v1-call-warning.ts +22 -0
  52. package/src/v1/harness-v1-diagnostic.ts +66 -0
  53. package/src/v1/harness-v1-lifecycle-state.ts +65 -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-sandbox-provider.ts +76 -0
  61. package/src/v1/harness-v1-session.ts +272 -0
  62. package/src/v1/harness-v1-skill.ts +36 -0
  63. package/src/v1/harness-v1-stream-part.ts +363 -0
  64. package/src/v1/harness-v1-tool-spec.ts +31 -0
  65. package/src/v1/harness-v1.ts +83 -0
  66. package/src/v1/index.ts +93 -0
  67. 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
+ }