@ai-sdk/workflow 2.0.57 → 2.0.59
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 +31 -0
- package/dist/index.d.ts +61 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +681 -299
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/build-model-step-result.ts +329 -0
- package/src/do-generate-step.ts +212 -0
- package/src/do-stream-step.ts +50 -179
- package/src/index.ts +3 -0
- package/src/{stream-text-iterator.ts → model-call-iterator.ts} +117 -297
- package/src/model-call.ts +196 -0
- package/src/providers/mock-function-wrapper.ts +32 -0
- package/src/test/agent-e2e-workflows.ts +60 -1
- package/src/test/approval-workflows.ts +105 -0
- package/src/test/generation-boundary-model.ts +115 -0
- package/src/test/generation-boundary-workflows.ts +106 -0
- package/src/test/model-call-payload-workflow.ts +132 -0
- package/src/to-model-response-messages.ts +28 -0
- package/src/workflow-agent.ts +509 -201
- package/src/workflow-execution-result.ts +55 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JSONObject,
|
|
3
|
+
LanguageModelV4CallOptions,
|
|
4
|
+
LanguageModelV4Content,
|
|
5
|
+
LanguageModelV4Source,
|
|
6
|
+
SharedV4ProviderMetadata,
|
|
7
|
+
} from '@ai-sdk/provider';
|
|
8
|
+
import type {
|
|
9
|
+
Experimental_LanguageModelStreamPart,
|
|
10
|
+
FinishReason,
|
|
11
|
+
LanguageModelUsage,
|
|
12
|
+
StopCondition,
|
|
13
|
+
ToolCallRepairFunction,
|
|
14
|
+
ToolChoice,
|
|
15
|
+
ToolSet,
|
|
16
|
+
} from 'ai';
|
|
17
|
+
import type { ProviderOptions, StreamTextTransform } from './workflow-agent.js';
|
|
18
|
+
|
|
19
|
+
export type ModelCallStreamPart<TTools extends ToolSet = ToolSet> =
|
|
20
|
+
| Experimental_LanguageModelStreamPart<TTools>
|
|
21
|
+
| {
|
|
22
|
+
type: 'tool-approval-request';
|
|
23
|
+
approvalId: string;
|
|
24
|
+
toolCallId: string;
|
|
25
|
+
signature?: string;
|
|
26
|
+
}
|
|
27
|
+
| { type: 'reset-step' };
|
|
28
|
+
|
|
29
|
+
export type ModelStopCondition = StopCondition<NoInfer<ToolSet>, any>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provider-executed tool result captured from a model call.
|
|
33
|
+
*/
|
|
34
|
+
export interface ProviderExecutedToolResult {
|
|
35
|
+
toolCallId: string;
|
|
36
|
+
toolName: string;
|
|
37
|
+
result: unknown;
|
|
38
|
+
isError?: boolean;
|
|
39
|
+
dynamic?: boolean;
|
|
40
|
+
providerMetadata?: SharedV4ProviderMetadata;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Serializable options for a durable model call.
|
|
45
|
+
*/
|
|
46
|
+
export interface ModelCallOptions {
|
|
47
|
+
maxOutputTokens?: number;
|
|
48
|
+
temperature?: number;
|
|
49
|
+
topP?: number;
|
|
50
|
+
topK?: number;
|
|
51
|
+
presencePenalty?: number;
|
|
52
|
+
frequencyPenalty?: number;
|
|
53
|
+
stopSequences?: string[];
|
|
54
|
+
seed?: number;
|
|
55
|
+
maxRetries?: number;
|
|
56
|
+
abortSignal?: AbortSignal;
|
|
57
|
+
timeoutAt?: number;
|
|
58
|
+
headers?: Record<string, string | undefined>;
|
|
59
|
+
reasoning?: LanguageModelV4CallOptions['reasoning'];
|
|
60
|
+
providerOptions?: ProviderOptions;
|
|
61
|
+
toolChoice?: ToolChoice<ToolSet>;
|
|
62
|
+
includeRawChunks?: boolean;
|
|
63
|
+
repairToolCall?: ToolCallRepairFunction<ToolSet>;
|
|
64
|
+
include?: {
|
|
65
|
+
requestBody?: boolean;
|
|
66
|
+
responseBody?: boolean;
|
|
67
|
+
requestMessages?: boolean;
|
|
68
|
+
};
|
|
69
|
+
responseFormat?: LanguageModelV4CallOptions['responseFormat'];
|
|
70
|
+
experimental_transform?:
|
|
71
|
+
| StreamTextTransform<ToolSet>
|
|
72
|
+
| Array<StreamTextTransform<ToolSet>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parsed tool call from the stream (parsed by streamModelCall's transform).
|
|
77
|
+
*/
|
|
78
|
+
export interface ParsedToolCall {
|
|
79
|
+
type: 'tool-call';
|
|
80
|
+
toolCallId: string;
|
|
81
|
+
toolName: string;
|
|
82
|
+
input: unknown;
|
|
83
|
+
providerExecuted?: boolean;
|
|
84
|
+
providerMetadata?: SharedV4ProviderMetadata;
|
|
85
|
+
title?: string;
|
|
86
|
+
toolMetadata?: JSONObject;
|
|
87
|
+
dynamic?: boolean;
|
|
88
|
+
invalid?: boolean;
|
|
89
|
+
error?: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Finish metadata from a model call.
|
|
94
|
+
*/
|
|
95
|
+
export interface ModelCallFinish {
|
|
96
|
+
finishReason: FinishReason;
|
|
97
|
+
rawFinishReason: string | undefined;
|
|
98
|
+
usage: LanguageModelUsage;
|
|
99
|
+
providerMetadata?: Record<string, unknown>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ModelCallRawContentPart =
|
|
103
|
+
| Extract<
|
|
104
|
+
LanguageModelV4Content,
|
|
105
|
+
{
|
|
106
|
+
type:
|
|
107
|
+
| 'reasoning'
|
|
108
|
+
| 'custom'
|
|
109
|
+
| 'reasoning-file'
|
|
110
|
+
| 'tool-approval-request';
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
| {
|
|
114
|
+
type: 'text';
|
|
115
|
+
text: string;
|
|
116
|
+
providerMetadata?: SharedV4ProviderMetadata;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
type: 'reasoning';
|
|
120
|
+
reasoningIndex: number;
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
type: 'file';
|
|
124
|
+
data: string;
|
|
125
|
+
mediaType: string;
|
|
126
|
+
providerMetadata?: SharedV4ProviderMetadata;
|
|
127
|
+
}
|
|
128
|
+
| LanguageModelV4Source
|
|
129
|
+
| {
|
|
130
|
+
type: 'tool-call';
|
|
131
|
+
toolCallIndex: number;
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
type: 'provider-tool-result';
|
|
135
|
+
toolCallId: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compact callback replay data. The start event establishes the tool name for
|
|
140
|
+
* a call, so delta events do not repeat it and available events reuse the
|
|
141
|
+
* parsed input already present in `toolCalls`.
|
|
142
|
+
*/
|
|
143
|
+
export type ToolInputLifecycleEvent =
|
|
144
|
+
| ['start', toolCallId: string, toolName: string]
|
|
145
|
+
| ['delta', toolCallId: string, inputTextDelta: string]
|
|
146
|
+
| ['available', toolCallId: string];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Minimal aggregates needed to reconstruct a `StepResult` outside the step
|
|
150
|
+
* boundary. By returning only these fields (instead of a fully-populated
|
|
151
|
+
* StepResult plus the raw `chunks[]` array), the durable event log doesn't
|
|
152
|
+
* carry StepResult's redundant derived fields — duplicate tool-call lists,
|
|
153
|
+
* `text`, `files`, `sources`, `reasoningText`, or the tool-result arrays
|
|
154
|
+
* populated after execution. It also avoids the per-chunk `chunks[]` snapshot
|
|
155
|
+
* the iterator never reads. The caller reconstructs the full StepResult via
|
|
156
|
+
* `buildModelStepResult`.
|
|
157
|
+
*/
|
|
158
|
+
export interface ModelCallRawResult {
|
|
159
|
+
content: ModelCallRawContentPart[];
|
|
160
|
+
reasoning: Array<{
|
|
161
|
+
text: string;
|
|
162
|
+
providerMetadata?: SharedV4ProviderMetadata;
|
|
163
|
+
}>;
|
|
164
|
+
responseMetadata?: {
|
|
165
|
+
id?: string;
|
|
166
|
+
timestamp?: Date;
|
|
167
|
+
modelId?: string;
|
|
168
|
+
headers?: Record<string, string>;
|
|
169
|
+
body?: unknown;
|
|
170
|
+
};
|
|
171
|
+
/** Present only on non-streaming results; old stream payloads remain valid. */
|
|
172
|
+
generation?: {
|
|
173
|
+
provider: string;
|
|
174
|
+
modelId: string;
|
|
175
|
+
request?: { body?: unknown };
|
|
176
|
+
responseTimeMs: number;
|
|
177
|
+
};
|
|
178
|
+
warnings?: unknown[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type ModelCallResult =
|
|
182
|
+
| { aborted: true }
|
|
183
|
+
| {
|
|
184
|
+
aborted?: false;
|
|
185
|
+
toolCalls: ParsedToolCall[];
|
|
186
|
+
finish: ModelCallFinish | undefined;
|
|
187
|
+
raw: ModelCallRawResult;
|
|
188
|
+
providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
|
|
189
|
+
/**
|
|
190
|
+
* Optional for compatibility with model-step results persisted before
|
|
191
|
+
* tool input lifecycle callback replay was added.
|
|
192
|
+
*/
|
|
193
|
+
toolInputLifecycleEvents?: ToolInputLifecycleEvent[];
|
|
194
|
+
/** Present when the model stream emitted an error part or generation failed. */
|
|
195
|
+
terminalError?: unknown;
|
|
196
|
+
};
|
|
@@ -38,6 +38,38 @@ class SerializableMockLanguageModel extends MockLanguageModelV4 {
|
|
|
38
38
|
super({
|
|
39
39
|
provider: 'workflow-test',
|
|
40
40
|
modelId: 'workflow-test-model',
|
|
41
|
+
doGenerate: async options => {
|
|
42
|
+
const index = Math.min(
|
|
43
|
+
options.prompt.filter(message => message.role === 'assistant').length,
|
|
44
|
+
responses.length - 1,
|
|
45
|
+
);
|
|
46
|
+
const response = responses[index];
|
|
47
|
+
if (response.type === 'error') throw response.error;
|
|
48
|
+
return {
|
|
49
|
+
content:
|
|
50
|
+
response.type === 'text'
|
|
51
|
+
? [{ type: 'text', text: response.text }]
|
|
52
|
+
: [
|
|
53
|
+
{
|
|
54
|
+
type: 'tool-call',
|
|
55
|
+
toolCallId: `call-${index + 1}`,
|
|
56
|
+
toolName: response.toolName,
|
|
57
|
+
input: response.input,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
finishReason: {
|
|
61
|
+
unified: response.type === 'text' ? 'stop' : 'tool-calls',
|
|
62
|
+
raw: undefined,
|
|
63
|
+
},
|
|
64
|
+
usage,
|
|
65
|
+
warnings: [],
|
|
66
|
+
response: {
|
|
67
|
+
id: 'generate-response',
|
|
68
|
+
modelId: 'mock-generate',
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
41
73
|
doStream: async (options: MockStreamOptions) => {
|
|
42
74
|
const responseIndex = Math.min(
|
|
43
75
|
options.prompt.filter(message => message.role === 'assistant').length,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration test workflows for WorkflowAgent using mock providers.
|
|
3
3
|
*/
|
|
4
|
-
import { tool } from 'ai';
|
|
4
|
+
import { tool, type ModelMessage, type LanguageModelUsage } from 'ai';
|
|
5
5
|
import { WorkflowAgent } from '../workflow-agent.js';
|
|
6
6
|
import { mockTextModel, mockSequenceModel } from '../providers/mock.js';
|
|
7
7
|
import { retryingModel } from './retrying-model.js';
|
|
@@ -809,3 +809,62 @@ export async function agentSandboxE2e() {
|
|
|
809
809
|
prepareStepSawStepSandbox,
|
|
810
810
|
};
|
|
811
811
|
}
|
|
812
|
+
|
|
813
|
+
export async function agentGenerateE2e(repair: boolean): Promise<{
|
|
814
|
+
text: string;
|
|
815
|
+
output: string;
|
|
816
|
+
toolOutputs: number[];
|
|
817
|
+
responseMessages: ModelMessage[];
|
|
818
|
+
usage: LanguageModelUsage;
|
|
819
|
+
events: string[];
|
|
820
|
+
}> {
|
|
821
|
+
'use workflow';
|
|
822
|
+
const events: string[] = [];
|
|
823
|
+
const agent = new WorkflowAgent({
|
|
824
|
+
model: mockSequenceModel([
|
|
825
|
+
{
|
|
826
|
+
type: 'tool-call',
|
|
827
|
+
toolName: 'addNumbers',
|
|
828
|
+
input: repair ? '{' : '{"a":3,"b":7}',
|
|
829
|
+
},
|
|
830
|
+
{ type: 'text', text: 'The sum is 10' },
|
|
831
|
+
]),
|
|
832
|
+
tools: {
|
|
833
|
+
addNumbers: tool({
|
|
834
|
+
inputSchema: z.object({ a: z.number(), b: z.number() }),
|
|
835
|
+
execute: addNumbers,
|
|
836
|
+
}),
|
|
837
|
+
},
|
|
838
|
+
onStepEnd: () => {
|
|
839
|
+
events.push('step');
|
|
840
|
+
},
|
|
841
|
+
onEnd: () => {
|
|
842
|
+
events.push('end');
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
const result = await agent.generate({
|
|
846
|
+
prompt: 'Add 3 and 7',
|
|
847
|
+
repairToolCall: repairToolCall as any,
|
|
848
|
+
});
|
|
849
|
+
return {
|
|
850
|
+
text: result.text,
|
|
851
|
+
output: result.output,
|
|
852
|
+
toolOutputs: result.staticToolResults.map(result => result.output),
|
|
853
|
+
responseMessages: result.responseMessages,
|
|
854
|
+
usage: result.usage,
|
|
855
|
+
events,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function agentGenerateUndefinedFailureE2e() {
|
|
860
|
+
'use workflow';
|
|
861
|
+
try {
|
|
862
|
+
await new WorkflowAgent({
|
|
863
|
+
model: mockSequenceModel([{ type: 'error', error: undefined }]),
|
|
864
|
+
maxRetries: 0,
|
|
865
|
+
}).generate({ prompt: 'fail' });
|
|
866
|
+
return { rejected: false, wasUndefined: false };
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return { rejected: true, wasUndefined: error === undefined };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { tool, type ModelMessage } from 'ai';
|
|
2
|
+
import { createHook } from 'workflow';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { mockSequenceModel } from '../providers/mock.js';
|
|
5
|
+
import { WorkflowAgent } from '../workflow-agent.js';
|
|
6
|
+
|
|
7
|
+
const secret = { environmentVariable: 'WORKFLOW_TOOL_APPROVAL_SECRET' };
|
|
8
|
+
|
|
9
|
+
async function performApprovedAction({ value }: { value: string }) {
|
|
10
|
+
'use step';
|
|
11
|
+
return `performed:${value}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function approvalTools() {
|
|
15
|
+
return {
|
|
16
|
+
action: tool({
|
|
17
|
+
inputSchema: z.object({ value: z.string() }),
|
|
18
|
+
needsApproval: true,
|
|
19
|
+
execute: performApprovedAction,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function issueGenerateApproval(): Promise<ModelMessage[]> {
|
|
25
|
+
'use workflow';
|
|
26
|
+
const agent = new WorkflowAgent({
|
|
27
|
+
model: mockSequenceModel([
|
|
28
|
+
{
|
|
29
|
+
type: 'tool-call',
|
|
30
|
+
toolName: 'action',
|
|
31
|
+
input: '{"value":"approved-action"}',
|
|
32
|
+
},
|
|
33
|
+
]),
|
|
34
|
+
tools: approvalTools(),
|
|
35
|
+
experimental_toolApprovalSecret: secret,
|
|
36
|
+
});
|
|
37
|
+
const result = await agent.generate({ prompt: 'Perform the action.' });
|
|
38
|
+
return result.responseMessages;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resumeGenerateApproval(
|
|
42
|
+
messages: ModelMessage[],
|
|
43
|
+
approved: boolean,
|
|
44
|
+
) {
|
|
45
|
+
'use workflow';
|
|
46
|
+
const agent = new WorkflowAgent({
|
|
47
|
+
model: mockSequenceModel([{ type: 'text', text: 'Finished.' }]),
|
|
48
|
+
tools: approvalTools(),
|
|
49
|
+
experimental_toolApprovalSecret: secret,
|
|
50
|
+
});
|
|
51
|
+
const result = await agent.generate({
|
|
52
|
+
messages: [
|
|
53
|
+
{ role: 'user', content: 'Perform the action.' },
|
|
54
|
+
...messages,
|
|
55
|
+
{
|
|
56
|
+
role: 'tool',
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'tool-approval-response',
|
|
60
|
+
approvalId: 'approval-call-1',
|
|
61
|
+
approved,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
return { text: result.text, responseMessages: result.responseMessages } as {
|
|
68
|
+
text: string;
|
|
69
|
+
responseMessages: ModelMessage[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function completedWork() {
|
|
74
|
+
'use step';
|
|
75
|
+
return 'completed before suspension';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function generateWithSuspendingTool() {
|
|
79
|
+
'use workflow';
|
|
80
|
+
const agent = new WorkflowAgent({
|
|
81
|
+
model: mockSequenceModel([
|
|
82
|
+
{ type: 'tool-call', toolName: 'completedWork', input: '{}' },
|
|
83
|
+
{ type: 'tool-call', toolName: 'waitForInput', input: '{}' },
|
|
84
|
+
{ type: 'text', text: 'Resumed.' },
|
|
85
|
+
]),
|
|
86
|
+
tools: {
|
|
87
|
+
completedWork: tool({
|
|
88
|
+
inputSchema: z.object({}),
|
|
89
|
+
execute: completedWork,
|
|
90
|
+
}),
|
|
91
|
+
waitForInput: tool({
|
|
92
|
+
inputSchema: z.object({}),
|
|
93
|
+
execute: async () => {
|
|
94
|
+
using hook = createHook<string>();
|
|
95
|
+
return await hook;
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const result = await agent.generate({ prompt: 'Do work, then wait.' });
|
|
101
|
+
return {
|
|
102
|
+
text: result.text,
|
|
103
|
+
outputs: result.toolResults.map(result => result.output),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
+
import {
|
|
3
|
+
WORKFLOW_DESERIALIZE,
|
|
4
|
+
WORKFLOW_SERIALIZE,
|
|
5
|
+
} from '@ai-sdk/provider-utils';
|
|
6
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
7
|
+
import { getStepMetadata, getWritable } from 'workflow';
|
|
8
|
+
|
|
9
|
+
export type BoundaryScenario = 'retry' | 'deadline' | 'abort' | 'rich' | 'hook';
|
|
10
|
+
|
|
11
|
+
const usage = {
|
|
12
|
+
inputTokens: { total: 3, noCache: 2, cacheRead: 1, cacheWrite: undefined },
|
|
13
|
+
outputTokens: { total: 2, text: 1, reasoning: 1 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class BoundaryModel extends MockLanguageModelV4 {
|
|
17
|
+
static [WORKFLOW_SERIALIZE](model: BoundaryModel) {
|
|
18
|
+
return { scenario: model.scenario };
|
|
19
|
+
}
|
|
20
|
+
static [WORKFLOW_DESERIALIZE]({ scenario }: { scenario: BoundaryScenario }) {
|
|
21
|
+
return new BoundaryModel(scenario);
|
|
22
|
+
}
|
|
23
|
+
constructor(readonly scenario: BoundaryScenario) {
|
|
24
|
+
let calls = 0;
|
|
25
|
+
super({
|
|
26
|
+
provider: 'boundary-test',
|
|
27
|
+
modelId: 'boundary-model',
|
|
28
|
+
doGenerate: async options => {
|
|
29
|
+
calls++;
|
|
30
|
+
const writer = getWritable().getWriter();
|
|
31
|
+
try {
|
|
32
|
+
await writer.write({
|
|
33
|
+
modelAttempt: calls,
|
|
34
|
+
stepAttempt: getStepMetadata().attempt,
|
|
35
|
+
});
|
|
36
|
+
} finally {
|
|
37
|
+
writer.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
if (scenario === 'abort')
|
|
40
|
+
throw new DOMException('Provider aborted.', 'AbortError');
|
|
41
|
+
if (scenario === 'deadline' || (scenario === 'retry' && calls < 3)) {
|
|
42
|
+
throw new APICallError({
|
|
43
|
+
message: 'Retryable fixture failure',
|
|
44
|
+
url: 'https://example.com/model',
|
|
45
|
+
requestBodyValues: {},
|
|
46
|
+
statusCode: 503,
|
|
47
|
+
responseHeaders: {
|
|
48
|
+
'retry-after-ms': scenario === 'deadline' ? '10000' : '0',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
scenario === 'hook' &&
|
|
54
|
+
!options.prompt.some(message => message.role === 'assistant')
|
|
55
|
+
) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'tool-call',
|
|
60
|
+
toolCallId: 'wait-call',
|
|
61
|
+
toolName: 'wait',
|
|
62
|
+
input: '{}',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
66
|
+
usage,
|
|
67
|
+
warnings: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content:
|
|
72
|
+
scenario === 'rich'
|
|
73
|
+
? [
|
|
74
|
+
{
|
|
75
|
+
type: 'reasoning',
|
|
76
|
+
text: 'Reasoning.',
|
|
77
|
+
providerMetadata: { fixture: { reasoning: true } },
|
|
78
|
+
},
|
|
79
|
+
{ type: 'text', text: 'Answer.' },
|
|
80
|
+
{
|
|
81
|
+
type: 'file',
|
|
82
|
+
data: { type: 'data', data: new Uint8Array([104, 105]) },
|
|
83
|
+
mediaType: 'text/plain',
|
|
84
|
+
providerMetadata: { fixture: { file: true } },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'source',
|
|
88
|
+
sourceType: 'url',
|
|
89
|
+
id: 'source',
|
|
90
|
+
url: 'https://example.com/source',
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
: [{ type: 'text', text: 'Recovered.' }],
|
|
94
|
+
finishReason: { unified: 'stop', raw: 'end_turn' },
|
|
95
|
+
usage,
|
|
96
|
+
warnings: [{ type: 'other', message: 'fixture warning' }],
|
|
97
|
+
providerMetadata: { fixture: { calls } },
|
|
98
|
+
request: { body: 'request body' },
|
|
99
|
+
response: {
|
|
100
|
+
id: 'boundary-response',
|
|
101
|
+
timestamp: new Date('2026-01-02T00:00:00Z'),
|
|
102
|
+
modelId: 'response-model',
|
|
103
|
+
headers: { 'x-fixture': 'yes' },
|
|
104
|
+
body: { fixture: 'response' },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Keep construction outside workflow modules for the SWC closure transform.
|
|
113
|
+
export function generationBoundaryModel(scenario: BoundaryScenario) {
|
|
114
|
+
return new BoundaryModel(scenario);
|
|
115
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { tool, type ModelMessage } from 'ai';
|
|
2
|
+
import { createHook, getWritable } from 'workflow';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { WorkflowAgent } from '../workflow-agent.js';
|
|
5
|
+
import {
|
|
6
|
+
generationBoundaryModel,
|
|
7
|
+
type BoundaryScenario,
|
|
8
|
+
} from './generation-boundary-model.js';
|
|
9
|
+
|
|
10
|
+
async function closeTrace() {
|
|
11
|
+
'use step';
|
|
12
|
+
const writer = getWritable().getWriter();
|
|
13
|
+
try {
|
|
14
|
+
await writer.close();
|
|
15
|
+
} finally {
|
|
16
|
+
writer.releaseLock();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function generateAtBoundary(
|
|
21
|
+
scenario: BoundaryScenario,
|
|
22
|
+
timeout?: number,
|
|
23
|
+
) {
|
|
24
|
+
'use workflow';
|
|
25
|
+
const agent = new WorkflowAgent({
|
|
26
|
+
model: generationBoundaryModel(scenario),
|
|
27
|
+
maxRetries: 2,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const result = await agent.generate({
|
|
31
|
+
prompt: 'Test generation.',
|
|
32
|
+
timeout,
|
|
33
|
+
});
|
|
34
|
+
return { text: result.text };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
errorName: (error as Error).name,
|
|
38
|
+
errorMessage: (error as Error).message,
|
|
39
|
+
};
|
|
40
|
+
} finally {
|
|
41
|
+
await closeTrace();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function generateAcrossDeadline(timeout: number) {
|
|
46
|
+
'use workflow';
|
|
47
|
+
const agent = new WorkflowAgent({
|
|
48
|
+
model: generationBoundaryModel('hook'),
|
|
49
|
+
tools: {
|
|
50
|
+
wait: tool({
|
|
51
|
+
inputSchema: z.object({}),
|
|
52
|
+
execute: async () => {
|
|
53
|
+
using hook = createHook<string>();
|
|
54
|
+
return await hook;
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const result = await agent.generate({
|
|
61
|
+
prompt: 'Wait for the user.',
|
|
62
|
+
timeout,
|
|
63
|
+
});
|
|
64
|
+
return { text: result.text };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { errorName: (error as Error).name };
|
|
67
|
+
} finally {
|
|
68
|
+
await closeTrace();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function generateSerializableFields(include: boolean) {
|
|
73
|
+
'use workflow';
|
|
74
|
+
const agent = new WorkflowAgent({ model: generationBoundaryModel('rich') });
|
|
75
|
+
const result = await agent.generate({
|
|
76
|
+
prompt: 'Return rich content.',
|
|
77
|
+
include: {
|
|
78
|
+
requestBody: include,
|
|
79
|
+
responseBody: include,
|
|
80
|
+
requestMessages: include,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
using hook = createHook<string>();
|
|
84
|
+
await hook;
|
|
85
|
+
await closeTrace();
|
|
86
|
+
return {
|
|
87
|
+
text: result.text,
|
|
88
|
+
output: result.output,
|
|
89
|
+
contentTypes: result.content.map(part => part.type),
|
|
90
|
+
file: {
|
|
91
|
+
base64: result.files[0].base64,
|
|
92
|
+
bytes: result.files[0].uint8Array,
|
|
93
|
+
mediaType: result.files[0].mediaType,
|
|
94
|
+
},
|
|
95
|
+
sources: result.sources,
|
|
96
|
+
response: {
|
|
97
|
+
...result.finalStep.response,
|
|
98
|
+
messages: result.finalStep.response.messages as ModelMessage[],
|
|
99
|
+
},
|
|
100
|
+
request: result.finalStep.request,
|
|
101
|
+
usage: result.usage,
|
|
102
|
+
warnings: result.warnings,
|
|
103
|
+
providerMetadata: result.finalStep.providerMetadata,
|
|
104
|
+
responseMessages: result.responseMessages as ModelMessage[],
|
|
105
|
+
};
|
|
106
|
+
}
|