@ai-sdk/workflow-harness 1.0.44 → 1.0.46
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 +13 -0
- package/README.md +78 -17
- package/dist/index.d.ts +52 -29
- package/dist/index.js +99 -39
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/harness-workflow-state.ts +18 -16
- package/src/index.ts +11 -1
- package/src/run-harness-agent-slice.ts +24 -584
- package/src/run-harness-agent-step.ts +24 -0
- package/src/run-harness-agent-time-slice.ts +37 -0
- package/src/run-harness-agent.ts +631 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @ai-sdk/workflow-harness
|
|
2
2
|
|
|
3
|
+
## 1.0.46
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @ai-sdk/harness@1.0.46
|
|
8
|
+
|
|
9
|
+
## 1.0.45
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 214ea9f: feat(workflow-harness): add utility functions for agent-step based workflow step definitions
|
|
14
|
+
- @ai-sdk/harness@1.0.45
|
|
15
|
+
|
|
3
16
|
## 1.0.44
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @ai-sdk/workflow-harness
|
|
2
2
|
|
|
3
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).
|
|
4
|
+
using the [Workflow DevKit](https://www.npmjs.com/package/workflow). A turn can
|
|
5
|
+
be divided into time slices or semantic agent steps.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
the durable step return value.
|
|
7
|
+
Time slices let a long agent turn survive a Fluid Compute function recycle
|
|
8
|
+
(~800s). Semantic steps let a workflow persist after each agent step, typically
|
|
9
|
+
by configuring the agent with `stopWhen: isStepCount(1)`. At either boundary the
|
|
10
|
+
agent is frozen non-destructively and a serializable state object is persisted
|
|
11
|
+
as the durable step return value.
|
|
11
12
|
|
|
12
13
|
This package ships plain helpers + a serializable state machine; you own the
|
|
13
14
|
thin `'use workflow'` / `'use step'` wrappers (the Workflow DevKit compiles
|
|
@@ -32,21 +33,21 @@ export const agent = new HarnessAgent({
|
|
|
32
33
|
});
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
`
|
|
36
|
+
`time-slice-step.ts`:
|
|
36
37
|
|
|
37
38
|
```ts
|
|
38
39
|
import {
|
|
39
|
-
|
|
40
|
+
runHarnessAgentTimeSlice,
|
|
40
41
|
type HarnessWorkflowState,
|
|
41
42
|
} from '@ai-sdk/workflow-harness';
|
|
42
43
|
|
|
43
|
-
export async function
|
|
44
|
+
export async function timeSliceStep(
|
|
44
45
|
state: HarnessWorkflowState,
|
|
45
46
|
): Promise<HarnessWorkflowState> {
|
|
46
47
|
'use step';
|
|
47
48
|
|
|
48
49
|
const { agent } = await import('./agent');
|
|
49
|
-
return
|
|
50
|
+
return runHarnessAgentTimeSlice({ agent, state });
|
|
50
51
|
}
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -58,18 +59,78 @@ import {
|
|
|
58
59
|
finalizeHarnessWorkflow,
|
|
59
60
|
type HarnessWorkflowInput,
|
|
60
61
|
} from '@ai-sdk/workflow-harness';
|
|
61
|
-
import {
|
|
62
|
+
import { timeSliceStep } from './time-slice-step';
|
|
62
63
|
|
|
63
|
-
export async function
|
|
64
|
+
export async function timeSliceWorkflow(input: {
|
|
64
65
|
prompt: HarnessWorkflowInput['prompt'];
|
|
65
66
|
sessionId: string;
|
|
66
67
|
}) {
|
|
67
68
|
'use workflow';
|
|
68
69
|
|
|
69
70
|
let state = createHarnessWorkflowState(input);
|
|
70
|
-
|
|
71
|
-
state = await
|
|
72
|
-
}
|
|
71
|
+
do {
|
|
72
|
+
state = await timeSliceStep(state);
|
|
73
|
+
} while (state.status === 'ready_for_next_step');
|
|
74
|
+
return finalizeHarnessWorkflow(state);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a semantic stepped workflow, configure the agent with
|
|
79
|
+
`stopWhen: isStepCount(1)`, call `runHarnessAgentStep()` from the step module,
|
|
80
|
+
and continue while the status is `ready_for_next_step`:
|
|
81
|
+
|
|
82
|
+
`stepped-agent.ts`:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { HarnessAgent } from '@ai-sdk/harness/agent';
|
|
86
|
+
import { claudeCode } from '@ai-sdk/harness-claude-code';
|
|
87
|
+
import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
|
|
88
|
+
import { isStepCount } from 'ai';
|
|
89
|
+
|
|
90
|
+
export const steppedAgent = new HarnessAgent({
|
|
91
|
+
harness: claudeCode,
|
|
92
|
+
sandbox: createVercelSandbox({ runtime: 'node24', ports: [4000] }),
|
|
93
|
+
stopWhen: isStepCount(1),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`stepped-agent-step.ts`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import {
|
|
101
|
+
runHarnessAgentStep,
|
|
102
|
+
type HarnessWorkflowState,
|
|
103
|
+
} from '@ai-sdk/workflow-harness';
|
|
104
|
+
|
|
105
|
+
export async function agentStep(
|
|
106
|
+
state: HarnessWorkflowState,
|
|
107
|
+
): Promise<HarnessWorkflowState> {
|
|
108
|
+
'use step';
|
|
109
|
+
|
|
110
|
+
const { steppedAgent } = await import('./stepped-agent');
|
|
111
|
+
return runHarnessAgentStep({ agent: steppedAgent, state });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`stepped-workflow.ts`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import {
|
|
119
|
+
createHarnessWorkflowState,
|
|
120
|
+
finalizeHarnessWorkflow,
|
|
121
|
+
type HarnessWorkflowInput,
|
|
122
|
+
} from '@ai-sdk/workflow-harness';
|
|
123
|
+
import { agentStep } from './stepped-agent-step';
|
|
124
|
+
|
|
125
|
+
export async function agentWorkflow(
|
|
126
|
+
input: Pick<HarnessWorkflowInput, 'messages' | 'sessionId'>,
|
|
127
|
+
) {
|
|
128
|
+
'use workflow';
|
|
129
|
+
|
|
130
|
+
let state = createHarnessWorkflowState(input);
|
|
131
|
+
do {
|
|
132
|
+
state = await agentStep(state);
|
|
133
|
+
} while (state.status === 'ready_for_next_step');
|
|
73
134
|
return finalizeHarnessWorkflow(state);
|
|
74
135
|
}
|
|
75
136
|
```
|
|
@@ -78,14 +139,14 @@ export async function codingWorkflow(input: {
|
|
|
78
139
|
|
|
79
140
|
```ts
|
|
80
141
|
import { start } from 'workflow/api';
|
|
81
|
-
import {
|
|
142
|
+
import { timeSliceWorkflow } from './workflow';
|
|
82
143
|
|
|
83
144
|
export async function POST(request: Request) {
|
|
84
145
|
const body = (await request.json()) as {
|
|
85
146
|
prompt: string;
|
|
86
147
|
sessionId: string;
|
|
87
148
|
};
|
|
88
|
-
const run = await start(
|
|
149
|
+
const run = await start(timeSliceWorkflow, [body]);
|
|
89
150
|
|
|
90
151
|
return new Response(run.readable);
|
|
91
152
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,17 +15,19 @@ type HarnessWorkflowModelMessage = {
|
|
|
15
15
|
readonly content: any;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
* Where a workflow-driven harness run is in its
|
|
18
|
+
* Where a workflow-driven harness run is in its execution loop.
|
|
19
19
|
*
|
|
20
|
-
* - `
|
|
21
|
-
* - `
|
|
22
|
-
* the cursor
|
|
20
|
+
* - `not_started` — fresh state, no workflow step has run yet.
|
|
21
|
+
* - `ready_for_next_step` — the turn remains unfinished and `continueFrom`
|
|
22
|
+
* carries the cursor for the next workflow step.
|
|
23
23
|
* - `awaiting_tool_approval` — the turn emitted one or more tool approval
|
|
24
24
|
* requests and `continueFrom` carries the suspended turn.
|
|
25
|
-
* - `finished`
|
|
26
|
-
* - `failed`
|
|
25
|
+
* - `finished` — the agent turn completed on its own; `finalResult` is set.
|
|
26
|
+
* - `failed` — the turn errored; `error` is set.
|
|
27
27
|
*/
|
|
28
|
-
type HarnessWorkflowStatus = '
|
|
28
|
+
type HarnessWorkflowStatus = 'not_started' | 'ready_for_next_step' | 'awaiting_tool_approval' | 'finished' | 'failed'
|
|
29
|
+
/** @deprecated Use `ready_for_next_step` instead. */
|
|
30
|
+
| 'timed_out';
|
|
29
31
|
interface HarnessWorkflowUsageSummary {
|
|
30
32
|
readonly inputTokens?: number;
|
|
31
33
|
readonly outputTokens?: number;
|
|
@@ -45,7 +47,7 @@ interface HarnessWorkflowStreamContext {
|
|
|
45
47
|
readonly pendingToolInputs?: Record<string, HarnessWorkflowSerializedChunk>;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
|
-
* Serializable state machine threaded between workflow
|
|
50
|
+
* Serializable state machine threaded between workflow steps. A `'use step'`
|
|
49
51
|
* returns the next value of this object, and the Workflow DevKit persists that
|
|
50
52
|
* return value — so this is the entire durable state of a harness run. Every
|
|
51
53
|
* field must be JSON-serializable.
|
|
@@ -68,13 +70,13 @@ interface HarnessWorkflowState {
|
|
|
68
70
|
* The new user turn for this run — a plain string or a single
|
|
69
71
|
* `UserModelMessage` (the harness's own {@link HarnessV1Prompt}), so
|
|
70
72
|
* structured content survives instead of being flattened to text. Sent once,
|
|
71
|
-
* on the
|
|
73
|
+
* on the execution that starts the turn.
|
|
72
74
|
*/
|
|
73
75
|
readonly prompt: HarnessV1Prompt;
|
|
74
76
|
/**
|
|
75
77
|
* Full AI SDK model messages for continuing a suspended approval turn. When
|
|
76
|
-
* present, the next
|
|
77
|
-
* responses can resume the suspended turn.
|
|
78
|
+
* present, the next execution sends these to `HarnessAgent.stream()` so
|
|
79
|
+
* approval responses can resume the suspended turn.
|
|
78
80
|
*/
|
|
79
81
|
readonly messages?: HarnessWorkflowModelMessage[];
|
|
80
82
|
readonly status: HarnessWorkflowStatus;
|
|
@@ -85,8 +87,8 @@ interface HarnessWorkflowState {
|
|
|
85
87
|
readonly resumeFrom?: HarnessV1ResumeSessionState;
|
|
86
88
|
/**
|
|
87
89
|
* Continuation coordinates for this run's current suspended turn. When
|
|
88
|
-
* present, the next
|
|
89
|
-
*
|
|
90
|
+
* present, the next execution continues the turn rather than sending `prompt`
|
|
91
|
+
* again.
|
|
90
92
|
*/
|
|
91
93
|
readonly continueFrom?: HarnessV1ContinueTurnState;
|
|
92
94
|
readonly streamContext?: HarnessWorkflowStreamContext;
|
|
@@ -125,7 +127,7 @@ interface HarnessWorkflowChunk {
|
|
|
125
127
|
readonly [key: string]: unknown;
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
128
|
-
* The subset of a harness `stream()` / `continueStream()` result the
|
|
130
|
+
* The subset of a harness `stream()` / `continueStream()` result the runner uses.
|
|
129
131
|
* `StreamTextResult` satisfies it structurally.
|
|
130
132
|
*/
|
|
131
133
|
interface HarnessWorkflowStreamResult {
|
|
@@ -134,7 +136,7 @@ interface HarnessWorkflowStreamResult {
|
|
|
134
136
|
readonly totalUsage: PromiseLike<unknown>;
|
|
135
137
|
}
|
|
136
138
|
/**
|
|
137
|
-
* The subset of `HarnessAgent` the
|
|
139
|
+
* The subset of `HarnessAgent` the runner drives. Declared structurally so
|
|
138
140
|
* the engine is decoupled from the concrete agent generics and easy to mock.
|
|
139
141
|
*/
|
|
140
142
|
interface HarnessWorkflowAgent {
|
|
@@ -161,11 +163,10 @@ interface HarnessWorkflowAgent {
|
|
|
161
163
|
session: HarnessAgentSession;
|
|
162
164
|
}): Promise<HarnessWorkflowStreamResult>;
|
|
163
165
|
}
|
|
164
|
-
interface
|
|
166
|
+
interface RunHarnessAgentOptions {
|
|
165
167
|
readonly agent: HarnessWorkflowAgent;
|
|
166
168
|
readonly state: HarnessWorkflowState;
|
|
167
|
-
|
|
168
|
-
readonly sliceTimeoutSeconds?: number;
|
|
169
|
+
readonly timeSliceSeconds?: number;
|
|
169
170
|
/**
|
|
170
171
|
* When the turn finishes, whether to destroy the sandbox. Defaults to `false`:
|
|
171
172
|
* the session is parked or stopped and a fresh resume state is returned in
|
|
@@ -181,20 +182,42 @@ interface RunHarnessAgentSliceOptions {
|
|
|
181
182
|
*/
|
|
182
183
|
readonly writable?: WritableStream<HarnessWorkflowChunk>;
|
|
183
184
|
}
|
|
185
|
+
|
|
186
|
+
type RunHarnessAgentStepOptions = Omit<RunHarnessAgentOptions, 'timeSliceSeconds'>;
|
|
184
187
|
/**
|
|
185
|
-
* Run
|
|
188
|
+
* Run a harness agent until its next semantic step boundary.
|
|
186
189
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
* Configure the agent with a `stopWhen` condition such as `isStepCount(1)`.
|
|
191
|
+
* When that condition completes a result while the underlying turn remains
|
|
192
|
+
* unfinished, the returned state has status `ready_for_next_step` and carries
|
|
193
|
+
* the continuation state for the next workflow step.
|
|
194
|
+
*/
|
|
195
|
+
declare function runHarnessAgentStep(options: RunHarnessAgentStepOptions): Promise<HarnessWorkflowState>;
|
|
196
|
+
|
|
197
|
+
interface RunHarnessAgentTimeSliceOptions extends Omit<RunHarnessAgentOptions, 'timeSliceSeconds'> {
|
|
198
|
+
/**
|
|
199
|
+
* Wall-clock budget for one time slice. Defaults to 750 seconds.
|
|
200
|
+
*/
|
|
201
|
+
readonly timeSliceSeconds?: number;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Run one time-boxed slice of a durable harness agent turn.
|
|
193
205
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
|
|
206
|
+
* When the time slice completes before the turn, the returned state has status
|
|
207
|
+
* `ready_for_next_step` and carries the continuation state for the next slice.
|
|
208
|
+
*/
|
|
209
|
+
declare function runHarnessAgentTimeSlice(options: RunHarnessAgentTimeSliceOptions): Promise<HarnessWorkflowState>;
|
|
210
|
+
|
|
211
|
+
interface RunHarnessAgentSliceOptions extends Omit<RunHarnessAgentTimeSliceOptions, 'timeSliceSeconds'> {
|
|
212
|
+
readonly timeSliceSeconds?: number;
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated Use `timeSliceSeconds` instead.
|
|
215
|
+
*/
|
|
216
|
+
readonly sliceTimeoutSeconds?: number;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* @deprecated Use {@link runHarnessAgentTimeSlice} instead.
|
|
197
220
|
*/
|
|
198
221
|
declare function runHarnessAgentSlice(options: RunHarnessAgentSliceOptions): Promise<HarnessWorkflowState>;
|
|
199
222
|
|
|
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 };
|
|
223
|
+
export { type HarnessWorkflowAgent, type HarnessWorkflowChunk, type HarnessWorkflowFinalResult, type HarnessWorkflowInput, type HarnessWorkflowModelMessage, type HarnessWorkflowState, type HarnessWorkflowStatus, type HarnessWorkflowStreamResult, type HarnessWorkflowUsageSummary, type RunHarnessAgentSliceOptions, type RunHarnessAgentStepOptions, type RunHarnessAgentTimeSliceOptions, createHarnessWorkflowState, finalizeHarnessWorkflow, runHarnessAgentSlice, runHarnessAgentStep, runHarnessAgentTimeSlice };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ function createHarnessWorkflowState(input) {
|
|
|
4
4
|
sessionId: input.sessionId,
|
|
5
5
|
prompt: input.prompt ?? "",
|
|
6
6
|
...input.messages != null ? { messages: input.messages } : {},
|
|
7
|
-
status: "
|
|
7
|
+
status: "not_started",
|
|
8
8
|
...input.resumeFrom != null ? { resumeFrom: input.resumeFrom } : {},
|
|
9
9
|
...input.continueFrom != null ? { continueFrom: input.continueFrom } : {}
|
|
10
10
|
};
|
|
@@ -19,11 +19,9 @@ function finalizeHarnessWorkflow(state) {
|
|
|
19
19
|
return { sessionId: state.sessionId, finishReason: "unknown" };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// src/run-harness-agent
|
|
23
|
-
|
|
24
|
-
async function runHarnessAgentSlice(options) {
|
|
22
|
+
// src/run-harness-agent.ts
|
|
23
|
+
async function runHarnessAgent(options) {
|
|
25
24
|
const { agent, state } = options;
|
|
26
|
-
const sliceTimeoutSeconds = options.sliceTimeoutSeconds ?? DEFAULT_SLICE_TIMEOUT_SECONDS;
|
|
27
25
|
const destroyOnFinish = options.destroyOnFinish ?? false;
|
|
28
26
|
const session = state.continueFrom != null ? await agent.createSession({
|
|
29
27
|
sessionId: state.sessionId,
|
|
@@ -55,12 +53,12 @@ async function runHarnessAgentSlice(options) {
|
|
|
55
53
|
const writable = options.writable ?? await resolveWorkflowWritable();
|
|
56
54
|
const writer = writable.getWriter();
|
|
57
55
|
const streamContext = createMutableStreamContext(state.streamContext);
|
|
58
|
-
const
|
|
56
|
+
const executionPartState = createExecutionPartState();
|
|
59
57
|
let suspendPromise;
|
|
60
|
-
const timer = setTimeout(() => {
|
|
58
|
+
const timer = options.timeSliceSeconds == null ? void 0 : setTimeout(() => {
|
|
61
59
|
suspendPromise = session.suspendTurn();
|
|
62
|
-
},
|
|
63
|
-
timer
|
|
60
|
+
}, options.timeSliceSeconds * 1e3);
|
|
61
|
+
timer?.unref?.();
|
|
64
62
|
let sawError = false;
|
|
65
63
|
let writerClosed = false;
|
|
66
64
|
try {
|
|
@@ -83,11 +81,11 @@ async function runHarnessAgentSlice(options) {
|
|
|
83
81
|
chunk: value,
|
|
84
82
|
writer,
|
|
85
83
|
streamContext,
|
|
86
|
-
|
|
84
|
+
executionPartState
|
|
87
85
|
});
|
|
88
86
|
}
|
|
89
87
|
} finally {
|
|
90
|
-
clearTimeout(timer);
|
|
88
|
+
if (timer != null) clearTimeout(timer);
|
|
91
89
|
reader.releaseLock();
|
|
92
90
|
}
|
|
93
91
|
if (sawError) {
|
|
@@ -105,15 +103,15 @@ async function runHarnessAgentSlice(options) {
|
|
|
105
103
|
}
|
|
106
104
|
if (suspendPromise != null) {
|
|
107
105
|
const continueFrom = await suspendPromise;
|
|
108
|
-
await
|
|
106
|
+
await closeOpenExecutionParts({
|
|
109
107
|
writer,
|
|
110
108
|
streamContext,
|
|
111
|
-
|
|
109
|
+
executionPartState
|
|
112
110
|
});
|
|
113
111
|
return {
|
|
114
112
|
sessionId: state.sessionId,
|
|
115
113
|
prompt: state.prompt,
|
|
116
|
-
status: "
|
|
114
|
+
status: "ready_for_next_step",
|
|
117
115
|
continueFrom,
|
|
118
116
|
...serializeStreamContextField(streamContext)
|
|
119
117
|
};
|
|
@@ -123,16 +121,39 @@ async function runHarnessAgentSlice(options) {
|
|
|
123
121
|
Promise.resolve(result.totalUsage).catch(() => void 0)
|
|
124
122
|
]);
|
|
125
123
|
const normalizedFinishReason = toFinishReasonString(finishReason);
|
|
126
|
-
if (
|
|
124
|
+
if (session.hasUnfinishedTurn()) {
|
|
127
125
|
const continueFrom = await session.suspendTurn();
|
|
128
|
-
|
|
126
|
+
if (!hasPendingHostInput(continueFrom)) {
|
|
127
|
+
await closeOpenExecutionParts({
|
|
128
|
+
writer,
|
|
129
|
+
streamContext,
|
|
130
|
+
executionPartState
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
sessionId: state.sessionId,
|
|
134
|
+
prompt: state.prompt,
|
|
135
|
+
status: "ready_for_next_step",
|
|
136
|
+
continueFrom,
|
|
137
|
+
...serializeStreamContextField(streamContext)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
await writer.write({
|
|
141
|
+
type: "finish",
|
|
142
|
+
finishReason: normalizedFinishReason
|
|
143
|
+
});
|
|
129
144
|
await writer.close();
|
|
130
145
|
writerClosed = true;
|
|
131
146
|
return {
|
|
132
147
|
sessionId: state.sessionId,
|
|
133
148
|
prompt: state.prompt,
|
|
134
149
|
status: "awaiting_tool_approval",
|
|
135
|
-
continueFrom
|
|
150
|
+
continueFrom,
|
|
151
|
+
resumeFrom: toResumeState({ continueFrom }),
|
|
152
|
+
finalResult: {
|
|
153
|
+
sessionId: state.sessionId,
|
|
154
|
+
finishReason: normalizedFinishReason,
|
|
155
|
+
usage: toUsageSummary(usage)
|
|
156
|
+
}
|
|
136
157
|
};
|
|
137
158
|
}
|
|
138
159
|
await writer.write({ type: "finish" });
|
|
@@ -167,7 +188,7 @@ function createMutableStreamContext(context) {
|
|
|
167
188
|
pendingToolInputs: { ...context?.pendingToolInputs ?? {} }
|
|
168
189
|
};
|
|
169
190
|
}
|
|
170
|
-
function
|
|
191
|
+
function createExecutionPartState() {
|
|
171
192
|
return {
|
|
172
193
|
openedTextParts: /* @__PURE__ */ new Set(),
|
|
173
194
|
openedReasoningParts: /* @__PURE__ */ new Set(),
|
|
@@ -180,74 +201,74 @@ async function writeWorkflowChunk(options) {
|
|
|
180
201
|
recordWorkflowChunk(options);
|
|
181
202
|
}
|
|
182
203
|
async function writeRequiredPrelude(options) {
|
|
183
|
-
const { chunk, writer, streamContext,
|
|
204
|
+
const { chunk, writer, streamContext, executionPartState } = options;
|
|
184
205
|
const id = stringProperty({ chunk, key: "id" });
|
|
185
|
-
if ((chunk.type === "text-delta" || chunk.type === "text-end") && id != null && streamContext.activeTextParts[id] != null && !
|
|
206
|
+
if ((chunk.type === "text-delta" || chunk.type === "text-end") && id != null && streamContext.activeTextParts[id] != null && !executionPartState.openedTextParts.has(id)) {
|
|
186
207
|
await writer.write(streamContext.activeTextParts[id]);
|
|
187
|
-
|
|
208
|
+
executionPartState.openedTextParts.add(id);
|
|
188
209
|
}
|
|
189
|
-
if ((chunk.type === "reasoning-delta" || chunk.type === "reasoning-end") && id != null && streamContext.activeReasoningParts[id] != null && !
|
|
210
|
+
if ((chunk.type === "reasoning-delta" || chunk.type === "reasoning-end") && id != null && streamContext.activeReasoningParts[id] != null && !executionPartState.openedReasoningParts.has(id)) {
|
|
190
211
|
await writer.write(streamContext.activeReasoningParts[id]);
|
|
191
|
-
|
|
212
|
+
executionPartState.openedReasoningParts.add(id);
|
|
192
213
|
}
|
|
193
214
|
const toolCallId = stringProperty({ chunk, key: "toolCallId" });
|
|
194
|
-
if (toolCallId != null && needsToolInputPrelude(chunk) && streamContext.pendingToolInputs[toolCallId] != null && !
|
|
215
|
+
if (toolCallId != null && needsToolInputPrelude(chunk) && streamContext.pendingToolInputs[toolCallId] != null && !executionPartState.writtenToolInputs.has(toolCallId)) {
|
|
195
216
|
await writer.write(streamContext.pendingToolInputs[toolCallId]);
|
|
196
|
-
|
|
217
|
+
executionPartState.writtenToolInputs.add(toolCallId);
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
function recordWorkflowChunk(options) {
|
|
200
|
-
const { chunk, streamContext,
|
|
221
|
+
const { chunk, streamContext, executionPartState } = options;
|
|
201
222
|
const id = stringProperty({ chunk, key: "id" });
|
|
202
223
|
const toolCallId = stringProperty({ chunk, key: "toolCallId" });
|
|
203
224
|
if (chunk.type === "text-start" && id != null) {
|
|
204
225
|
streamContext.activeTextParts[id] = cloneChunk(chunk);
|
|
205
|
-
|
|
226
|
+
executionPartState.openedTextParts.add(id);
|
|
206
227
|
return;
|
|
207
228
|
}
|
|
208
229
|
if (chunk.type === "text-end" && id != null) {
|
|
209
230
|
delete streamContext.activeTextParts[id];
|
|
210
|
-
|
|
231
|
+
executionPartState.openedTextParts.delete(id);
|
|
211
232
|
return;
|
|
212
233
|
}
|
|
213
234
|
if (chunk.type === "reasoning-start" && id != null) {
|
|
214
235
|
streamContext.activeReasoningParts[id] = cloneChunk(chunk);
|
|
215
|
-
|
|
236
|
+
executionPartState.openedReasoningParts.add(id);
|
|
216
237
|
return;
|
|
217
238
|
}
|
|
218
239
|
if (chunk.type === "reasoning-end" && id != null) {
|
|
219
240
|
delete streamContext.activeReasoningParts[id];
|
|
220
|
-
|
|
241
|
+
executionPartState.openedReasoningParts.delete(id);
|
|
221
242
|
return;
|
|
222
243
|
}
|
|
223
244
|
if (chunk.type === "tool-input-available" && toolCallId != null) {
|
|
224
245
|
streamContext.pendingToolInputs[toolCallId] = cloneChunk(chunk);
|
|
225
|
-
|
|
246
|
+
executionPartState.writtenToolInputs.add(toolCallId);
|
|
226
247
|
return;
|
|
227
248
|
}
|
|
228
249
|
if (chunk.type === "tool-input-error" && toolCallId != null) {
|
|
229
250
|
delete streamContext.pendingToolInputs[toolCallId];
|
|
230
|
-
|
|
251
|
+
executionPartState.writtenToolInputs.add(toolCallId);
|
|
231
252
|
return;
|
|
232
253
|
}
|
|
233
254
|
if ((chunk.type === "tool-output-error" || chunk.type === "tool-output-denied" || chunk.type === "tool-output-available" && chunk.preliminary !== true) && toolCallId != null) {
|
|
234
255
|
delete streamContext.pendingToolInputs[toolCallId];
|
|
235
256
|
}
|
|
236
257
|
}
|
|
237
|
-
async function
|
|
238
|
-
const { writer, streamContext,
|
|
239
|
-
for (const id of
|
|
258
|
+
async function closeOpenExecutionParts(options) {
|
|
259
|
+
const { writer, streamContext, executionPartState } = options;
|
|
260
|
+
for (const id of executionPartState.openedTextParts) {
|
|
240
261
|
if (streamContext.activeTextParts[id] != null) {
|
|
241
262
|
await writer.write({ type: "text-end", id });
|
|
242
263
|
}
|
|
243
264
|
}
|
|
244
|
-
|
|
245
|
-
for (const id of
|
|
265
|
+
executionPartState.openedTextParts.clear();
|
|
266
|
+
for (const id of executionPartState.openedReasoningParts) {
|
|
246
267
|
if (streamContext.activeReasoningParts[id] != null) {
|
|
247
268
|
await writer.write({ type: "reasoning-end", id });
|
|
248
269
|
}
|
|
249
270
|
}
|
|
250
|
-
|
|
271
|
+
executionPartState.openedReasoningParts.clear();
|
|
251
272
|
}
|
|
252
273
|
function serializeStreamContextField(context) {
|
|
253
274
|
const streamContext = {
|
|
@@ -260,6 +281,19 @@ function serializeStreamContextField(context) {
|
|
|
260
281
|
function needsToolInputPrelude(chunk) {
|
|
261
282
|
return chunk.type === "tool-approval-request" || chunk.type === "tool-output-available" || chunk.type === "tool-output-error" || chunk.type === "tool-output-denied";
|
|
262
283
|
}
|
|
284
|
+
function hasPendingHostInput(state) {
|
|
285
|
+
return (state.pendingToolApprovals?.length ?? 0) > 0 || (state.pendingToolResults?.length ?? 0) > 0;
|
|
286
|
+
}
|
|
287
|
+
function toResumeState(options) {
|
|
288
|
+
const { continueFrom } = options;
|
|
289
|
+
return {
|
|
290
|
+
type: "resume-session",
|
|
291
|
+
harnessId: continueFrom.harnessId,
|
|
292
|
+
specificationVersion: continueFrom.specificationVersion,
|
|
293
|
+
data: continueFrom.data,
|
|
294
|
+
continueFrom
|
|
295
|
+
};
|
|
296
|
+
}
|
|
263
297
|
function stringProperty(options) {
|
|
264
298
|
const value = options.chunk[options.key];
|
|
265
299
|
return typeof value === "string" ? value : void 0;
|
|
@@ -304,9 +338,35 @@ function toUsageSummary(usage) {
|
|
|
304
338
|
...outputTokens != null ? { outputTokens } : {}
|
|
305
339
|
};
|
|
306
340
|
}
|
|
341
|
+
|
|
342
|
+
// src/run-harness-agent-step.ts
|
|
343
|
+
async function runHarnessAgentStep(options) {
|
|
344
|
+
return runHarnessAgent(options);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/run-harness-agent-time-slice.ts
|
|
348
|
+
var DEFAULT_TIME_SLICE_SECONDS = 750;
|
|
349
|
+
async function runHarnessAgentTimeSlice(options) {
|
|
350
|
+
return runHarnessAgent({
|
|
351
|
+
...options,
|
|
352
|
+
timeSliceSeconds: options.timeSliceSeconds ?? DEFAULT_TIME_SLICE_SECONDS
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/run-harness-agent-slice.ts
|
|
357
|
+
async function runHarnessAgentSlice(options) {
|
|
358
|
+
const { sliceTimeoutSeconds, timeSliceSeconds, ...timeSliceOptions } = options;
|
|
359
|
+
const state = await runHarnessAgentTimeSlice({
|
|
360
|
+
...timeSliceOptions,
|
|
361
|
+
timeSliceSeconds: timeSliceSeconds ?? sliceTimeoutSeconds
|
|
362
|
+
});
|
|
363
|
+
return state.status === "ready_for_next_step" ? { ...state, status: "timed_out" } : state;
|
|
364
|
+
}
|
|
307
365
|
export {
|
|
308
366
|
createHarnessWorkflowState,
|
|
309
367
|
finalizeHarnessWorkflow,
|
|
310
|
-
runHarnessAgentSlice
|
|
368
|
+
runHarnessAgentSlice,
|
|
369
|
+
runHarnessAgentStep,
|
|
370
|
+
runHarnessAgentTimeSlice
|
|
311
371
|
};
|
|
312
372
|
//# sourceMappingURL=index.js.map
|