@ai-sdk/workflow-harness 0.0.0-d3900c59-20260626174016

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 ADDED
@@ -0,0 +1,77 @@
1
+ # @ai-sdk/workflow-harness
2
+
3
+ ## 0.0.0-d3900c59-20260626174016
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [691c7c4]
8
+ - @ai-sdk/harness@0.0.0-d3900c59-20260626174016
9
+
10
+ ## 1.0.3
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [51d10a0]
15
+ - @ai-sdk/harness@1.0.3
16
+
17
+ ## 1.0.2
18
+
19
+ ### Patch Changes
20
+
21
+ - @ai-sdk/harness@1.0.2
22
+
23
+ ## 1.0.1
24
+
25
+ ### Patch Changes
26
+
27
+ - @ai-sdk/harness@1.0.1
28
+
29
+ ## 1.0.0
30
+
31
+ ### Major Changes
32
+
33
+ - 6e33eb6: feat(workflow-harness): introduce workflow utilities for durable harness agent execution
34
+
35
+ ### Patch Changes
36
+
37
+ ## 1.0.0-beta.5
38
+
39
+ ### Patch Changes
40
+
41
+ - @ai-sdk/harness@1.0.0-beta.27
42
+
43
+ ## 1.0.0-beta.4
44
+
45
+ ### Patch Changes
46
+
47
+ - Updated dependencies [a83a367]
48
+ - @ai-sdk/harness@1.0.0-beta.26
49
+
50
+ ## 1.0.0-beta.3
51
+
52
+ ### Patch Changes
53
+
54
+ - @ai-sdk/harness@1.0.0-beta.25
55
+
56
+ ## 1.0.0-beta.2
57
+
58
+ ### Patch Changes
59
+
60
+ - @ai-sdk/harness@1.0.0-beta.24
61
+
62
+ ## 1.0.0-beta.1
63
+
64
+ ### Patch Changes
65
+
66
+ - Updated dependencies [57e0a59]
67
+ - @ai-sdk/harness@1.0.0-beta.23
68
+
69
+ ## 1.0.0-beta.0
70
+
71
+ ### Major Changes
72
+
73
+ - 6e33eb6: feat(workflow-harness): introduce workflow utilities for durable harness agent execution
74
+
75
+ ### Patch Changes
76
+
77
+ - @ai-sdk/harness@1.0.0-beta.22
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Vercel, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @ai-sdk/workflow-harness
2
+
3
+ Run an AI SDK `HarnessAgent` (Claude Code, Codex, Pi) as a **durable workflow**
4
+ using the [Workflow DevKit](https://www.npmjs.com/package/workflow).
5
+
6
+ A long agent turn is sliced into short, time-boxed steps so it survives a Fluid
7
+ Compute function recycle (~800s). Between slices the agent is frozen
8
+ non-destructively — the sandbox keeps running and the next slice reattaches to
9
+ the in-flight turn (`attach`) — and a serializable state object is persisted as
10
+ the durable step return value.
11
+
12
+ This package ships plain helpers + a serializable state machine; you own the
13
+ thin `'use workflow'` / `'use step'` wrappers (the Workflow DevKit compiles
14
+ those directives in your app).
15
+
16
+ Keep the Workflow DevKit entrypoints separate from the agent definition. The
17
+ workflow module should import only workflow-safe code plus step modules. The
18
+ step module should dynamically import the agent inside the `'use step'` body so
19
+ the agent, sandbox provider, and other Node-heavy dependencies stay out of the
20
+ compiled workflow bundle.
21
+
22
+ `agent.ts`:
23
+
24
+ ```ts
25
+ import { HarnessAgent } from '@ai-sdk/harness/agent';
26
+ import { claudeCode } from '@ai-sdk/harness-claude-code';
27
+ import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
28
+
29
+ export const agent = new HarnessAgent({
30
+ harness: claudeCode,
31
+ sandbox: createVercelSandbox({ runtime: 'node24', ports: [4000] }),
32
+ });
33
+ ```
34
+
35
+ `run-slice-step.ts`:
36
+
37
+ ```ts
38
+ import {
39
+ runHarnessAgentSlice,
40
+ type HarnessWorkflowState,
41
+ } from '@ai-sdk/workflow-harness';
42
+
43
+ export async function runSlice(
44
+ state: HarnessWorkflowState,
45
+ ): Promise<HarnessWorkflowState> {
46
+ 'use step';
47
+
48
+ const { agent } = await import('./agent');
49
+ return runHarnessAgentSlice({ agent, state });
50
+ }
51
+ ```
52
+
53
+ `workflow.ts`:
54
+
55
+ ```ts
56
+ import {
57
+ createHarnessWorkflowState,
58
+ finalizeHarnessWorkflow,
59
+ type HarnessWorkflowInput,
60
+ } from '@ai-sdk/workflow-harness';
61
+ import { runSlice } from './run-slice-step';
62
+
63
+ export async function codingWorkflow(input: {
64
+ prompt: HarnessWorkflowInput['prompt'];
65
+ sessionId: string;
66
+ }) {
67
+ 'use workflow';
68
+
69
+ let state = createHarnessWorkflowState(input);
70
+ while (state.status === 'running' || state.status === 'timed_out') {
71
+ state = await runSlice(state);
72
+ }
73
+ return finalizeHarnessWorkflow(state);
74
+ }
75
+ ```
76
+
77
+ `route.ts` (Next.js example):
78
+
79
+ ```ts
80
+ import { start } from 'workflow/api';
81
+ import { codingWorkflow } from './workflow';
82
+
83
+ export async function POST(request: Request) {
84
+ const body = (await request.json()) as {
85
+ prompt: string;
86
+ sessionId: string;
87
+ };
88
+ const run = await start(codingWorkflow, [body]);
89
+
90
+ return new Response(run.readable);
91
+ }
92
+ ```
@@ -0,0 +1,200 @@
1
+ import { HarnessV1Prompt, HarnessV1ResumeSessionState, HarnessV1ContinueTurnState } from '@ai-sdk/harness';
2
+ import { HarnessAgentSession } from '@ai-sdk/harness/agent';
3
+
4
+ type HarnessWorkflowModelMessage = {
5
+ readonly role: 'system';
6
+ readonly content: any;
7
+ } | {
8
+ readonly role: 'user';
9
+ readonly content: any;
10
+ } | {
11
+ readonly role: 'assistant';
12
+ readonly content: any;
13
+ } | {
14
+ readonly role: 'tool';
15
+ readonly content: any;
16
+ };
17
+ /**
18
+ * Where a workflow-driven harness run is in its slice loop.
19
+ *
20
+ * - `running` — fresh state, no slice has run yet.
21
+ * - `timed_out` — a slice hit its wall-clock budget; `continueFrom` carries
22
+ * the cursor to continue the same turn.
23
+ * - `awaiting_tool_approval` — the turn emitted one or more tool approval
24
+ * requests and `continueFrom` carries the suspended turn.
25
+ * - `finished` — the agent turn completed on its own; `finalResult` is set.
26
+ * - `failed` — the turn errored; `error` is set.
27
+ */
28
+ type HarnessWorkflowStatus = 'running' | 'timed_out' | 'awaiting_tool_approval' | 'finished' | 'failed';
29
+ interface HarnessWorkflowUsageSummary {
30
+ readonly inputTokens?: number;
31
+ readonly outputTokens?: number;
32
+ }
33
+ interface HarnessWorkflowFinalResult {
34
+ readonly sessionId: string;
35
+ readonly finishReason: string;
36
+ readonly usage?: HarnessWorkflowUsageSummary;
37
+ }
38
+ interface HarnessWorkflowSerializedChunk {
39
+ readonly type: string;
40
+ readonly [key: string]: unknown;
41
+ }
42
+ interface HarnessWorkflowStreamContext {
43
+ readonly activeTextParts?: Record<string, HarnessWorkflowSerializedChunk>;
44
+ readonly activeReasoningParts?: Record<string, HarnessWorkflowSerializedChunk>;
45
+ readonly pendingToolInputs?: Record<string, HarnessWorkflowSerializedChunk>;
46
+ }
47
+ /**
48
+ * Serializable state machine threaded between workflow slices. A `'use step'`
49
+ * returns the next value of this object, and the Workflow DevKit persists that
50
+ * return value — so this is the entire durable state of a harness run. Every
51
+ * field must be JSON-serializable.
52
+ *
53
+ * Two independent lifecycle states drive the engine:
54
+ *
55
+ * - `resumeFrom` reattaches to a warm session before starting this run's new
56
+ * user turn.
57
+ * - `continueFrom` reattaches to an interrupted turn from this same run and
58
+ * continues it without sending `prompt` again.
59
+ */
60
+ interface HarnessWorkflowState {
61
+ /**
62
+ * Stable harness session id; doubles as the sandbox name across processes.
63
+ * Reuse the chat/conversation id so every user turn resumes the same warm
64
+ * session and the agent retains prior-turn context.
65
+ */
66
+ readonly sessionId: string;
67
+ /**
68
+ * The new user turn for this run — a plain string or a single
69
+ * `UserModelMessage` (the harness's own {@link HarnessV1Prompt}), so
70
+ * structured content survives instead of being flattened to text. Sent once,
71
+ * on the slice that starts the turn.
72
+ */
73
+ readonly prompt: HarnessV1Prompt;
74
+ /**
75
+ * Full AI SDK model messages for continuing a suspended approval turn. When
76
+ * present, the next slice sends these to `HarnessAgent.stream()` so approval
77
+ * responses can resume the interrupted turn.
78
+ */
79
+ readonly messages?: HarnessWorkflowModelMessage[];
80
+ readonly status: HarnessWorkflowStatus;
81
+ /**
82
+ * Resume coordinates for the next user turn. Absent only on the first turn of
83
+ * a brand-new conversation or when the sandbox was destroyed after finish.
84
+ */
85
+ readonly resumeFrom?: HarnessV1ResumeSessionState;
86
+ /**
87
+ * Continuation coordinates for this run's current suspended turn. When
88
+ * present, the next slice must call `continueTurn` rather than sending
89
+ * `prompt` again.
90
+ */
91
+ readonly continueFrom?: HarnessV1ContinueTurnState;
92
+ readonly streamContext?: HarnessWorkflowStreamContext;
93
+ readonly finalResult?: HarnessWorkflowFinalResult;
94
+ readonly error?: string;
95
+ }
96
+ /**
97
+ * Input for one user turn — the argument to {@link createHarnessWorkflowState}
98
+ * and the natural shape for a workflow function's input. `sessionId` is required
99
+ * (and must be caller-supplied, since the workflow runtime forbids
100
+ * non-deterministic id generation inside a step) — reuse the conversation id so
101
+ * the sandbox name is stable across turns. Pass `resumeFrom` (the handle
102
+ * persisted after the previous turn) to resume the warm conversation; omit it
103
+ * only for the first turn of a new conversation.
104
+ */
105
+ interface HarnessWorkflowInput {
106
+ prompt?: HarnessV1Prompt;
107
+ messages?: HarnessWorkflowModelMessage[];
108
+ sessionId: string;
109
+ resumeFrom?: HarnessV1ResumeSessionState;
110
+ continueFrom?: HarnessV1ContinueTurnState;
111
+ }
112
+ /** Initial state for one user turn (see {@link HarnessWorkflowInput}). */
113
+ declare function createHarnessWorkflowState(input: HarnessWorkflowInput): HarnessWorkflowState;
114
+ /**
115
+ * Collapse a terminal state into its result. Throws if the run failed; returns
116
+ * the captured `finalResult` when finished, or a best-effort result otherwise.
117
+ */
118
+ declare function finalizeHarnessWorkflow(state: HarnessWorkflowState): HarnessWorkflowFinalResult;
119
+
120
+ /** The non-string arm of {@link HarnessV1Prompt} — a single `UserModelMessage`. */
121
+ type HarnessV1UserMessage = Exclude<HarnessV1Prompt, string>;
122
+ /** A UI-message-stream chunk. Kept structural so this package need not depend on `ai`. */
123
+ interface HarnessWorkflowChunk {
124
+ readonly type: string;
125
+ readonly [key: string]: unknown;
126
+ }
127
+ /**
128
+ * The subset of a harness `stream()` / `continueStream()` result the slice loop uses.
129
+ * `StreamTextResult` satisfies it structurally.
130
+ */
131
+ interface HarnessWorkflowStreamResult {
132
+ toUIMessageStream(): ReadableStream<HarnessWorkflowChunk>;
133
+ readonly finishReason: PromiseLike<unknown>;
134
+ readonly totalUsage: PromiseLike<unknown>;
135
+ }
136
+ /**
137
+ * The subset of `HarnessAgent` the slice loop drives. Declared structurally so
138
+ * the engine is decoupled from the concrete agent generics and easy to mock.
139
+ */
140
+ interface HarnessWorkflowAgent {
141
+ createSession(options?: {
142
+ sessionId?: string;
143
+ resumeFrom?: HarnessV1ResumeSessionState;
144
+ continueFrom?: HarnessV1ContinueTurnState;
145
+ }): Promise<HarnessAgentSession>;
146
+ stream(options: {
147
+ session: HarnessAgentSession;
148
+ /**
149
+ * The new user turn. A string or an array of user messages — the shape
150
+ * `HarnessAgent.stream` accepts (it collapses an array to its last user
151
+ * entry). The engine passes the run's single {@link HarnessV1Prompt}.
152
+ */
153
+ prompt: string | HarnessV1UserMessage[];
154
+ messages?: undefined;
155
+ } | {
156
+ session: HarnessAgentSession;
157
+ prompt?: undefined;
158
+ messages: HarnessWorkflowModelMessage[];
159
+ }): Promise<HarnessWorkflowStreamResult>;
160
+ continueStream(options: {
161
+ session: HarnessAgentSession;
162
+ }): Promise<HarnessWorkflowStreamResult>;
163
+ }
164
+ interface RunHarnessAgentSliceOptions {
165
+ readonly agent: HarnessWorkflowAgent;
166
+ readonly state: HarnessWorkflowState;
167
+ /** Wall-clock budget for this slice. Defaults to {@link DEFAULT_SLICE_TIMEOUT_SECONDS}. */
168
+ readonly sliceTimeoutSeconds?: number;
169
+ /**
170
+ * When the turn finishes, whether to destroy the sandbox. Defaults to `false`:
171
+ * the session is parked or stopped and a fresh resume state is returned in
172
+ * `resumeFrom`, so the next user turn reattaches to the same conversation
173
+ * (multi-turn chat). Set `true` for a one-shot run that should release the
174
+ * sandbox when the turn completes.
175
+ */
176
+ readonly destroyOnFinish?: boolean;
177
+ /**
178
+ * Where to write the turn's UI-message chunks. Defaults to the workflow's
179
+ * output stream (`getWritable()` from `workflow`). Inject a stream in tests
180
+ * to run the engine without a workflow runtime.
181
+ */
182
+ readonly writable?: WritableStream<HarnessWorkflowChunk>;
183
+ }
184
+ /**
185
+ * Run one durable slice of a harness agent turn.
186
+ *
187
+ * Intended to be the body of a consumer's `'use step'`: it resumes (or starts)
188
+ * the session, streams the turn's chunks to the workflow output, and races the
189
+ * turn against a wall-clock budget. If the budget fires first it freezes the
190
+ * turn with `session.suspendTurn()` (the sandbox keeps running) and returns a
191
+ * `timed_out` state carrying continuation state for the next slice; if the
192
+ * turn finishes first it returns a `finished` state with the result.
193
+ *
194
+ * The returned {@link HarnessWorkflowState} is serializable and is meant to be
195
+ * the step's return value — the Workflow DevKit persists it as the durable
196
+ * checkpoint between slices.
197
+ */
198
+ declare function runHarnessAgentSlice(options: RunHarnessAgentSliceOptions): Promise<HarnessWorkflowState>;
199
+
200
+ export { type HarnessWorkflowAgent, type HarnessWorkflowChunk, type HarnessWorkflowFinalResult, type HarnessWorkflowInput, type HarnessWorkflowModelMessage, type HarnessWorkflowState, type HarnessWorkflowStatus, type HarnessWorkflowStreamResult, type HarnessWorkflowUsageSummary, type RunHarnessAgentSliceOptions, createHarnessWorkflowState, finalizeHarnessWorkflow, runHarnessAgentSlice };
package/dist/index.js ADDED
@@ -0,0 +1,312 @@
1
+ // src/harness-workflow-state.ts
2
+ function createHarnessWorkflowState(input) {
3
+ return {
4
+ sessionId: input.sessionId,
5
+ prompt: input.prompt ?? "",
6
+ ...input.messages != null ? { messages: input.messages } : {},
7
+ status: "running",
8
+ ...input.resumeFrom != null ? { resumeFrom: input.resumeFrom } : {},
9
+ ...input.continueFrom != null ? { continueFrom: input.continueFrom } : {}
10
+ };
11
+ }
12
+ function finalizeHarnessWorkflow(state) {
13
+ if (state.status === "failed") {
14
+ throw new Error(state.error ?? "harness workflow failed");
15
+ }
16
+ if (state.finalResult) {
17
+ return state.finalResult;
18
+ }
19
+ return { sessionId: state.sessionId, finishReason: "unknown" };
20
+ }
21
+
22
+ // src/run-harness-agent-slice.ts
23
+ var DEFAULT_SLICE_TIMEOUT_SECONDS = 750;
24
+ async function runHarnessAgentSlice(options) {
25
+ const { agent, state } = options;
26
+ const sliceTimeoutSeconds = options.sliceTimeoutSeconds ?? DEFAULT_SLICE_TIMEOUT_SECONDS;
27
+ const destroyOnFinish = options.destroyOnFinish ?? false;
28
+ const session = state.continueFrom != null ? await agent.createSession({
29
+ sessionId: state.sessionId,
30
+ continueFrom: state.continueFrom
31
+ }) : state.resumeFrom != null ? await agent.createSession({
32
+ sessionId: state.sessionId,
33
+ resumeFrom: state.resumeFrom
34
+ }) : await agent.createSession({ sessionId: state.sessionId });
35
+ let result;
36
+ try {
37
+ result = state.messages != null ? await agent.stream({
38
+ session,
39
+ messages: state.messages
40
+ }) : state.continueFrom != null ? await agent.continueStream({ session }) : await agent.stream({
41
+ session,
42
+ prompt: typeof state.prompt === "string" ? state.prompt : [state.prompt]
43
+ });
44
+ } catch (err) {
45
+ await destroyQuietly(session);
46
+ return {
47
+ sessionId: state.sessionId,
48
+ prompt: state.prompt,
49
+ status: "failed",
50
+ ...state.resumeFrom != null ? { resumeFrom: state.resumeFrom } : {},
51
+ ...state.continueFrom != null ? { continueFrom: state.continueFrom } : {},
52
+ error: errorMessage(err)
53
+ };
54
+ }
55
+ const writable = options.writable ?? await resolveWorkflowWritable();
56
+ const writer = writable.getWriter();
57
+ const streamContext = createMutableStreamContext(state.streamContext);
58
+ const slicePartState = createSlicePartState();
59
+ let suspendPromise;
60
+ const timer = setTimeout(() => {
61
+ suspendPromise = session.suspendTurn();
62
+ }, sliceTimeoutSeconds * 1e3);
63
+ timer.unref?.();
64
+ let sawError = false;
65
+ let writerClosed = false;
66
+ try {
67
+ const reader = result.toUIMessageStream().getReader();
68
+ try {
69
+ while (true) {
70
+ const { value, done } = await reader.read();
71
+ if (done) break;
72
+ if (value == null) continue;
73
+ if (value.type === "start" && state.continueFrom != null) continue;
74
+ if (value.type === "finish") continue;
75
+ if (value.type === "error") {
76
+ const errorText = value.errorText;
77
+ if (suspendPromise != null && isAbortError(errorText)) {
78
+ continue;
79
+ }
80
+ sawError = true;
81
+ }
82
+ await writeWorkflowChunk({
83
+ chunk: value,
84
+ writer,
85
+ streamContext,
86
+ slicePartState
87
+ });
88
+ }
89
+ } finally {
90
+ clearTimeout(timer);
91
+ reader.releaseLock();
92
+ }
93
+ if (sawError) {
94
+ if (suspendPromise != null) await suspendPromise.catch(() => {
95
+ });
96
+ await destroyQuietly(session);
97
+ return {
98
+ sessionId: state.sessionId,
99
+ prompt: state.prompt,
100
+ status: "failed",
101
+ ...state.resumeFrom != null ? { resumeFrom: state.resumeFrom } : {},
102
+ ...state.continueFrom != null ? { continueFrom: state.continueFrom } : {},
103
+ error: "harness turn emitted an error"
104
+ };
105
+ }
106
+ if (suspendPromise != null) {
107
+ const continueFrom = await suspendPromise;
108
+ await closeOpenSliceParts({
109
+ writer,
110
+ streamContext,
111
+ slicePartState
112
+ });
113
+ return {
114
+ sessionId: state.sessionId,
115
+ prompt: state.prompt,
116
+ status: "timed_out",
117
+ continueFrom,
118
+ ...serializeStreamContextField(streamContext)
119
+ };
120
+ }
121
+ const [finishReason, usage] = await Promise.all([
122
+ Promise.resolve(result.finishReason).catch(() => void 0),
123
+ Promise.resolve(result.totalUsage).catch(() => void 0)
124
+ ]);
125
+ const normalizedFinishReason = toFinishReasonString(finishReason);
126
+ if (normalizedFinishReason === "tool-calls") {
127
+ const continueFrom = await session.suspendTurn();
128
+ await writer.write({ type: "finish", finishReason: "tool-calls" });
129
+ await writer.close();
130
+ writerClosed = true;
131
+ return {
132
+ sessionId: state.sessionId,
133
+ prompt: state.prompt,
134
+ status: "awaiting_tool_approval",
135
+ continueFrom
136
+ };
137
+ }
138
+ await writer.write({ type: "finish" });
139
+ await writer.close();
140
+ writerClosed = true;
141
+ let resumeFrom = state.resumeFrom;
142
+ if (destroyOnFinish) {
143
+ await destroyQuietly(session);
144
+ resumeFrom = void 0;
145
+ } else {
146
+ resumeFrom = await session.detach().catch(() => state.resumeFrom);
147
+ }
148
+ return {
149
+ sessionId: state.sessionId,
150
+ prompt: state.prompt,
151
+ status: "finished",
152
+ ...resumeFrom != null ? { resumeFrom } : {},
153
+ finalResult: {
154
+ sessionId: state.sessionId,
155
+ finishReason: normalizedFinishReason,
156
+ usage: toUsageSummary(usage)
157
+ }
158
+ };
159
+ } finally {
160
+ if (!writerClosed) writer.releaseLock();
161
+ }
162
+ }
163
+ function createMutableStreamContext(context) {
164
+ return {
165
+ activeTextParts: { ...context?.activeTextParts ?? {} },
166
+ activeReasoningParts: { ...context?.activeReasoningParts ?? {} },
167
+ pendingToolInputs: { ...context?.pendingToolInputs ?? {} }
168
+ };
169
+ }
170
+ function createSlicePartState() {
171
+ return {
172
+ openedTextParts: /* @__PURE__ */ new Set(),
173
+ openedReasoningParts: /* @__PURE__ */ new Set(),
174
+ writtenToolInputs: /* @__PURE__ */ new Set()
175
+ };
176
+ }
177
+ async function writeWorkflowChunk(options) {
178
+ await writeRequiredPrelude(options);
179
+ await options.writer.write(options.chunk);
180
+ recordWorkflowChunk(options);
181
+ }
182
+ async function writeRequiredPrelude(options) {
183
+ const { chunk, writer, streamContext, slicePartState } = options;
184
+ const id = stringProperty({ chunk, key: "id" });
185
+ if ((chunk.type === "text-delta" || chunk.type === "text-end") && id != null && streamContext.activeTextParts[id] != null && !slicePartState.openedTextParts.has(id)) {
186
+ await writer.write(streamContext.activeTextParts[id]);
187
+ slicePartState.openedTextParts.add(id);
188
+ }
189
+ if ((chunk.type === "reasoning-delta" || chunk.type === "reasoning-end") && id != null && streamContext.activeReasoningParts[id] != null && !slicePartState.openedReasoningParts.has(id)) {
190
+ await writer.write(streamContext.activeReasoningParts[id]);
191
+ slicePartState.openedReasoningParts.add(id);
192
+ }
193
+ const toolCallId = stringProperty({ chunk, key: "toolCallId" });
194
+ if (toolCallId != null && needsToolInputPrelude(chunk) && streamContext.pendingToolInputs[toolCallId] != null && !slicePartState.writtenToolInputs.has(toolCallId)) {
195
+ await writer.write(streamContext.pendingToolInputs[toolCallId]);
196
+ slicePartState.writtenToolInputs.add(toolCallId);
197
+ }
198
+ }
199
+ function recordWorkflowChunk(options) {
200
+ const { chunk, streamContext, slicePartState } = options;
201
+ const id = stringProperty({ chunk, key: "id" });
202
+ const toolCallId = stringProperty({ chunk, key: "toolCallId" });
203
+ if (chunk.type === "text-start" && id != null) {
204
+ streamContext.activeTextParts[id] = cloneChunk(chunk);
205
+ slicePartState.openedTextParts.add(id);
206
+ return;
207
+ }
208
+ if (chunk.type === "text-end" && id != null) {
209
+ delete streamContext.activeTextParts[id];
210
+ slicePartState.openedTextParts.delete(id);
211
+ return;
212
+ }
213
+ if (chunk.type === "reasoning-start" && id != null) {
214
+ streamContext.activeReasoningParts[id] = cloneChunk(chunk);
215
+ slicePartState.openedReasoningParts.add(id);
216
+ return;
217
+ }
218
+ if (chunk.type === "reasoning-end" && id != null) {
219
+ delete streamContext.activeReasoningParts[id];
220
+ slicePartState.openedReasoningParts.delete(id);
221
+ return;
222
+ }
223
+ if (chunk.type === "tool-input-available" && toolCallId != null) {
224
+ streamContext.pendingToolInputs[toolCallId] = cloneChunk(chunk);
225
+ slicePartState.writtenToolInputs.add(toolCallId);
226
+ return;
227
+ }
228
+ if (chunk.type === "tool-input-error" && toolCallId != null) {
229
+ delete streamContext.pendingToolInputs[toolCallId];
230
+ slicePartState.writtenToolInputs.add(toolCallId);
231
+ return;
232
+ }
233
+ if ((chunk.type === "tool-output-error" || chunk.type === "tool-output-denied" || chunk.type === "tool-output-available" && chunk.preliminary !== true) && toolCallId != null) {
234
+ delete streamContext.pendingToolInputs[toolCallId];
235
+ }
236
+ }
237
+ async function closeOpenSliceParts(options) {
238
+ const { writer, streamContext, slicePartState } = options;
239
+ for (const id of slicePartState.openedTextParts) {
240
+ if (streamContext.activeTextParts[id] != null) {
241
+ await writer.write({ type: "text-end", id });
242
+ }
243
+ }
244
+ slicePartState.openedTextParts.clear();
245
+ for (const id of slicePartState.openedReasoningParts) {
246
+ if (streamContext.activeReasoningParts[id] != null) {
247
+ await writer.write({ type: "reasoning-end", id });
248
+ }
249
+ }
250
+ slicePartState.openedReasoningParts.clear();
251
+ }
252
+ function serializeStreamContextField(context) {
253
+ const streamContext = {
254
+ ...Object.keys(context.activeTextParts).length > 0 ? { activeTextParts: context.activeTextParts } : {},
255
+ ...Object.keys(context.activeReasoningParts).length > 0 ? { activeReasoningParts: context.activeReasoningParts } : {},
256
+ ...Object.keys(context.pendingToolInputs).length > 0 ? { pendingToolInputs: context.pendingToolInputs } : {}
257
+ };
258
+ return Object.keys(streamContext).length > 0 ? { streamContext } : {};
259
+ }
260
+ function needsToolInputPrelude(chunk) {
261
+ return chunk.type === "tool-approval-request" || chunk.type === "tool-output-available" || chunk.type === "tool-output-error" || chunk.type === "tool-output-denied";
262
+ }
263
+ function stringProperty(options) {
264
+ const value = options.chunk[options.key];
265
+ return typeof value === "string" ? value : void 0;
266
+ }
267
+ function cloneChunk(chunk) {
268
+ return { ...chunk };
269
+ }
270
+ async function resolveWorkflowWritable() {
271
+ const { getWritable } = await import("workflow");
272
+ return getWritable();
273
+ }
274
+ async function destroyQuietly(session) {
275
+ await session.destroy().catch(() => {
276
+ });
277
+ }
278
+ function errorMessage(err) {
279
+ return err instanceof Error ? err.message : String(err);
280
+ }
281
+ function isAbortError(value) {
282
+ if (value == null) return false;
283
+ if (typeof value === "object" && value.name === "AbortError") {
284
+ return true;
285
+ }
286
+ const text = typeof value === "string" ? value : value instanceof Error ? value.message : String(value);
287
+ return /\baborted\b|AbortError|operation was aborted/i.test(text);
288
+ }
289
+ function toFinishReasonString(finishReason) {
290
+ if (typeof finishReason === "string") return finishReason;
291
+ if (finishReason != null && typeof finishReason === "object" && typeof finishReason.unified === "string") {
292
+ return finishReason.unified;
293
+ }
294
+ return "stop";
295
+ }
296
+ function toUsageSummary(usage) {
297
+ if (usage == null || typeof usage !== "object") return void 0;
298
+ const u = usage;
299
+ const inputTokens = u.inputTokens?.total;
300
+ const outputTokens = u.outputTokens?.total;
301
+ if (inputTokens == null && outputTokens == null) return void 0;
302
+ return {
303
+ ...inputTokens != null ? { inputTokens } : {},
304
+ ...outputTokens != null ? { outputTokens } : {}
305
+ };
306
+ }
307
+ export {
308
+ createHarnessWorkflowState,
309
+ finalizeHarnessWorkflow,
310
+ runHarnessAgentSlice
311
+ };
312
+ //# sourceMappingURL=index.js.map