@assemble-dev/sdk 0.0.1
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/dist/index.cjs +3925 -0
- package/dist/index.d.cts +588 -0
- package/dist/index.d.ts +588 -0
- package/dist/index.js +3877 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3877 @@
|
|
|
1
|
+
// src/agents/agent-definition.ts
|
|
2
|
+
function agent(config) {
|
|
3
|
+
return Object.freeze({
|
|
4
|
+
...config,
|
|
5
|
+
maxSteps: config.maxSteps ?? 1
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/agents/agent-runner.ts
|
|
10
|
+
import { AppErrorCode as AppErrorCode3 } from "@assemble-dev/shared-types/errors";
|
|
11
|
+
import { AppError as AppError3 } from "@assemble-dev/shared-utils/errors";
|
|
12
|
+
|
|
13
|
+
// src/agents/agent-messages.ts
|
|
14
|
+
function serializeAgentInputToText(input) {
|
|
15
|
+
return typeof input === "string" ? input : JSON.stringify(input);
|
|
16
|
+
}
|
|
17
|
+
async function buildAgentChatMessages(definition, input, threadMemory) {
|
|
18
|
+
const priorThreadMessages = threadMemory === void 0 ? [] : await threadMemory.listThreadMessages();
|
|
19
|
+
return [
|
|
20
|
+
{ role: "system", content: definition.instructions },
|
|
21
|
+
...priorThreadMessages.map(mapRunMessageToChatMessage),
|
|
22
|
+
{ role: "user", content: serializeAgentInputToText(input) }
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function mapRunMessageToChatMessage(message) {
|
|
26
|
+
if (message.toolName === void 0) {
|
|
27
|
+
return { role: message.role, content: message.content };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
role: message.role,
|
|
31
|
+
content: message.content,
|
|
32
|
+
toolName: message.toolName
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function appendAgentExchangeToThreadMemory(threadMemory, agentName, input, assistantText, now) {
|
|
36
|
+
if (threadMemory === void 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await threadMemory.appendThreadMessage({
|
|
40
|
+
role: "user",
|
|
41
|
+
content: serializeAgentInputToText(input),
|
|
42
|
+
timestamp: now().toISOString()
|
|
43
|
+
});
|
|
44
|
+
await threadMemory.appendThreadMessage({
|
|
45
|
+
role: "assistant",
|
|
46
|
+
content: assistantText,
|
|
47
|
+
agentName,
|
|
48
|
+
timestamp: now().toISOString()
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/agents/agent-output-generation.ts
|
|
53
|
+
import { AppErrorCode as AppErrorCode2 } from "@assemble-dev/shared-types/errors";
|
|
54
|
+
import { AppError as AppError2 } from "@assemble-dev/shared-utils/errors";
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
|
|
57
|
+
// src/agents/agent-model-timeout.ts
|
|
58
|
+
import { AppErrorCode } from "@assemble-dev/shared-types/errors";
|
|
59
|
+
import { AppError } from "@assemble-dev/shared-utils/errors";
|
|
60
|
+
var agentTimeoutErrorCode = "AGENT_TIMEOUT";
|
|
61
|
+
async function raceModelCallAgainstTimeout(modelCall, timeoutMs, agentName) {
|
|
62
|
+
if (timeoutMs === void 0) {
|
|
63
|
+
return await modelCall;
|
|
64
|
+
}
|
|
65
|
+
return await new Promise((resolve, reject) => {
|
|
66
|
+
const timeoutHandle = setTimeout(() => {
|
|
67
|
+
reject(buildAgentTimeoutError(agentName, timeoutMs));
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
void modelCall.then(
|
|
70
|
+
(modelResult) => {
|
|
71
|
+
clearTimeout(timeoutHandle);
|
|
72
|
+
resolve(modelResult);
|
|
73
|
+
},
|
|
74
|
+
(modelError) => {
|
|
75
|
+
clearTimeout(timeoutHandle);
|
|
76
|
+
reject(modelError);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function buildAgentTimeoutError(agentName, timeoutMs) {
|
|
82
|
+
return new AppError({
|
|
83
|
+
code: AppErrorCode.InternalServerError,
|
|
84
|
+
message: `Agent "${agentName}" timed out after ${timeoutMs}ms`,
|
|
85
|
+
statusCode: 504,
|
|
86
|
+
details: {
|
|
87
|
+
errorCode: agentTimeoutErrorCode,
|
|
88
|
+
timeoutMs
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/agents/agent-output-generation.ts
|
|
94
|
+
async function generateAgentOutput(definition, input, threadMemory) {
|
|
95
|
+
const messages = await buildAgentChatMessages(definition, input, threadMemory);
|
|
96
|
+
const outputSchema = definition.outputSchema;
|
|
97
|
+
if (outputSchema === void 0) {
|
|
98
|
+
const completion = await raceModelCallAgainstTimeout(
|
|
99
|
+
definition.model.generateChatCompletion({ messages }),
|
|
100
|
+
definition.timeoutMs,
|
|
101
|
+
definition.name
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
output: completion.content,
|
|
105
|
+
rawText: completion.content,
|
|
106
|
+
tokenUsage: completion.tokenUsage
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return await generateStructuredAgentOutput(definition, outputSchema, messages);
|
|
110
|
+
}
|
|
111
|
+
async function generateStructuredAgentOutput(definition, outputSchema, messages) {
|
|
112
|
+
try {
|
|
113
|
+
return await requestStructuredAgentOutput(definition, outputSchema, messages);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!(error instanceof AppError2) || error.code !== AppErrorCode2.ValidationFailed) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
return await retryStructuredAgentOutput(definition, outputSchema, messages, error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function retryStructuredAgentOutput(definition, outputSchema, messages, validationError) {
|
|
122
|
+
const retryMessages = [
|
|
123
|
+
...messages,
|
|
124
|
+
buildSchemaCorrectionMessage(validationError.message)
|
|
125
|
+
];
|
|
126
|
+
try {
|
|
127
|
+
return await requestStructuredAgentOutput(definition, outputSchema, retryMessages);
|
|
128
|
+
} catch (retryError) {
|
|
129
|
+
if (!(retryError instanceof AppError2) || retryError.code !== AppErrorCode2.ValidationFailed) {
|
|
130
|
+
throw retryError;
|
|
131
|
+
}
|
|
132
|
+
throw new AppError2({
|
|
133
|
+
code: AppErrorCode2.ValidationFailed,
|
|
134
|
+
message: `Agent "${definition.name}" structured output failed schema validation after one retry: ${retryError.message}`,
|
|
135
|
+
statusCode: 422,
|
|
136
|
+
cause: retryError
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function requestStructuredAgentOutput(definition, outputSchema, messages) {
|
|
141
|
+
try {
|
|
142
|
+
const structuredResult = await raceModelCallAgainstTimeout(
|
|
143
|
+
definition.model.generateStructuredOutput({
|
|
144
|
+
messages,
|
|
145
|
+
schema: outputSchema
|
|
146
|
+
}),
|
|
147
|
+
definition.timeoutMs,
|
|
148
|
+
definition.name
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
output: structuredResult.value,
|
|
152
|
+
rawText: JSON.stringify(structuredResult.value),
|
|
153
|
+
tokenUsage: structuredResult.tokenUsage
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof z.ZodError) {
|
|
157
|
+
throw new AppError2({
|
|
158
|
+
code: AppErrorCode2.ValidationFailed,
|
|
159
|
+
message: `Structured output failed schema validation: ${error.message}`,
|
|
160
|
+
statusCode: 422
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function buildSchemaCorrectionMessage(validationErrorMessage) {
|
|
167
|
+
return {
|
|
168
|
+
role: "user",
|
|
169
|
+
content: `The previous response failed schema validation: ${validationErrorMessage}. Respond again with JSON that matches the required schema exactly.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/agents/agent-trace-events.ts
|
|
174
|
+
import { JsonValueSchema } from "@assemble-dev/shared-types/json";
|
|
175
|
+
import { generateTraceEventId } from "@assemble-dev/shared-utils/ids";
|
|
176
|
+
function buildAgentEventBase(eventContext, parentEventId) {
|
|
177
|
+
const runState = eventContext.traceBus.getRunState();
|
|
178
|
+
return {
|
|
179
|
+
id: generateTraceEventId(),
|
|
180
|
+
runId: runState.runId,
|
|
181
|
+
workflowName: runState.workflowName,
|
|
182
|
+
timestamp: eventContext.now().toISOString(),
|
|
183
|
+
parentEventId,
|
|
184
|
+
redacted: false
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function emitAgentStartedEvent(eventContext, input, parentEventId) {
|
|
188
|
+
return eventContext.traceBus.emitTraceEvent({
|
|
189
|
+
...buildAgentEventBase(eventContext, parentEventId),
|
|
190
|
+
type: "agent.started",
|
|
191
|
+
agentName: eventContext.agentName,
|
|
192
|
+
input
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function emitAgentCompletedEvent(eventContext, startedEventId, output, latencyMs) {
|
|
196
|
+
eventContext.traceBus.emitTraceEvent({
|
|
197
|
+
...buildAgentEventBase(eventContext, startedEventId),
|
|
198
|
+
type: "agent.completed",
|
|
199
|
+
agentName: eventContext.agentName,
|
|
200
|
+
output: JsonValueSchema.parse(output),
|
|
201
|
+
latencyMs
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function emitAgentFailedEvent(eventContext, parentEventId, error, latencyMs) {
|
|
205
|
+
eventContext.traceBus.emitTraceEvent({
|
|
206
|
+
...buildAgentEventBase(eventContext, parentEventId),
|
|
207
|
+
type: "agent.failed",
|
|
208
|
+
agentName: eventContext.agentName,
|
|
209
|
+
error: {
|
|
210
|
+
message: error.message,
|
|
211
|
+
code: resolveAgentTraceErrorCode(error)
|
|
212
|
+
},
|
|
213
|
+
latencyMs
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function resolveAgentTraceErrorCode(error) {
|
|
217
|
+
const detailsErrorCode = error.details?.errorCode;
|
|
218
|
+
return typeof detailsErrorCode === "string" ? detailsErrorCode : error.code;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/agents/agent-runner.ts
|
|
222
|
+
async function runAgent(runInput) {
|
|
223
|
+
const now = runInput.now ?? (() => /* @__PURE__ */ new Date());
|
|
224
|
+
const eventContext = {
|
|
225
|
+
traceBus: runInput.traceBus,
|
|
226
|
+
agentName: runInput.definition.name,
|
|
227
|
+
now
|
|
228
|
+
};
|
|
229
|
+
const startedAtMs = now().getTime();
|
|
230
|
+
const validatedInput = validateAgentInput(runInput, eventContext, startedAtMs);
|
|
231
|
+
const startedEvent = emitAgentStartedEvent(
|
|
232
|
+
eventContext,
|
|
233
|
+
validatedInput,
|
|
234
|
+
runInput.parentEventId ?? null
|
|
235
|
+
);
|
|
236
|
+
invokeAgentStartHook(runInput.definition, validatedInput);
|
|
237
|
+
try {
|
|
238
|
+
return await completeAgentRun(
|
|
239
|
+
runInput,
|
|
240
|
+
eventContext,
|
|
241
|
+
validatedInput,
|
|
242
|
+
startedEvent.id,
|
|
243
|
+
startedAtMs
|
|
244
|
+
);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const appError = mapCaughtErrorToAppError(
|
|
247
|
+
error instanceof Error ? error : new Error(String(error))
|
|
248
|
+
);
|
|
249
|
+
emitAgentFailedEvent(
|
|
250
|
+
eventContext,
|
|
251
|
+
startedEvent.id,
|
|
252
|
+
appError,
|
|
253
|
+
now().getTime() - startedAtMs
|
|
254
|
+
);
|
|
255
|
+
invokeAgentErrorHook(runInput.definition, appError);
|
|
256
|
+
throw appError;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function completeAgentRun(runInput, eventContext, validatedInput, startedEventId, startedAtMs) {
|
|
260
|
+
const generation = await generateAgentOutput(
|
|
261
|
+
runInput.definition,
|
|
262
|
+
validatedInput,
|
|
263
|
+
runInput.threadMemory
|
|
264
|
+
);
|
|
265
|
+
await appendAgentExchangeToThreadMemory(
|
|
266
|
+
runInput.threadMemory,
|
|
267
|
+
runInput.definition.name,
|
|
268
|
+
validatedInput,
|
|
269
|
+
generation.rawText,
|
|
270
|
+
eventContext.now
|
|
271
|
+
);
|
|
272
|
+
const latencyMs = eventContext.now().getTime() - startedAtMs;
|
|
273
|
+
emitAgentCompletedEvent(eventContext, startedEventId, generation.output, latencyMs);
|
|
274
|
+
invokeAgentCompleteHook(runInput.definition, generation.output);
|
|
275
|
+
return {
|
|
276
|
+
output: generation.output,
|
|
277
|
+
rawText: generation.rawText,
|
|
278
|
+
tokenUsage: generation.tokenUsage,
|
|
279
|
+
latencyMs,
|
|
280
|
+
agentCallEventId: startedEventId
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function validateAgentInput(runInput, eventContext, startedAtMs) {
|
|
284
|
+
const inputSchema = runInput.definition.inputSchema;
|
|
285
|
+
if (inputSchema === void 0) {
|
|
286
|
+
return runInput.input;
|
|
287
|
+
}
|
|
288
|
+
const parsedInput = inputSchema.safeParse(runInput.input);
|
|
289
|
+
if (parsedInput.success) {
|
|
290
|
+
return parsedInput.data;
|
|
291
|
+
}
|
|
292
|
+
const validationError = new AppError3({
|
|
293
|
+
code: AppErrorCode3.ValidationFailed,
|
|
294
|
+
message: `Agent "${runInput.definition.name}" input failed schema validation: ${parsedInput.error.message}`,
|
|
295
|
+
statusCode: 422
|
|
296
|
+
});
|
|
297
|
+
emitAgentFailedEvent(
|
|
298
|
+
eventContext,
|
|
299
|
+
runInput.parentEventId ?? null,
|
|
300
|
+
validationError,
|
|
301
|
+
eventContext.now().getTime() - startedAtMs
|
|
302
|
+
);
|
|
303
|
+
invokeAgentErrorHook(runInput.definition, validationError);
|
|
304
|
+
throw validationError;
|
|
305
|
+
}
|
|
306
|
+
function mapCaughtErrorToAppError(error) {
|
|
307
|
+
if (error instanceof AppError3) {
|
|
308
|
+
return error;
|
|
309
|
+
}
|
|
310
|
+
return new AppError3({
|
|
311
|
+
code: AppErrorCode3.InternalServerError,
|
|
312
|
+
message: error.message,
|
|
313
|
+
statusCode: 500,
|
|
314
|
+
cause: error
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function invokeAgentStartHook(definition, input) {
|
|
318
|
+
invokeAgentHookSafely(
|
|
319
|
+
() => definition.onAgentStart?.({ agentName: definition.name, input })
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
function invokeAgentCompleteHook(definition, output) {
|
|
323
|
+
invokeAgentHookSafely(
|
|
324
|
+
() => definition.onAgentComplete?.({ agentName: definition.name, output })
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function invokeAgentErrorHook(definition, error) {
|
|
328
|
+
invokeAgentHookSafely(
|
|
329
|
+
() => definition.onAgentError?.({ agentName: definition.name, error })
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function invokeAgentHookSafely(invokeHook) {
|
|
333
|
+
try {
|
|
334
|
+
invokeHook();
|
|
335
|
+
} catch {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/tools/tool-definition.ts
|
|
341
|
+
import { z as z2 } from "zod";
|
|
342
|
+
var ToolRetryConfigSchema = z2.object({
|
|
343
|
+
maxAttempts: z2.number().int().min(1)
|
|
344
|
+
});
|
|
345
|
+
var ToolDefinitionOptionsSchema = z2.object({
|
|
346
|
+
name: z2.string().min(1),
|
|
347
|
+
description: z2.string().min(1),
|
|
348
|
+
allowedAgentNames: z2.array(z2.string().min(1)).optional(),
|
|
349
|
+
requiresApproval: z2.boolean().optional(),
|
|
350
|
+
retry: ToolRetryConfigSchema.optional(),
|
|
351
|
+
timeoutMs: z2.number().int().positive().optional(),
|
|
352
|
+
sensitiveFieldNames: z2.array(z2.string().min(1)).optional(),
|
|
353
|
+
dryRun: z2.boolean().optional()
|
|
354
|
+
});
|
|
355
|
+
var defaultToolRetryConfig = Object.freeze({
|
|
356
|
+
maxAttempts: 1
|
|
357
|
+
});
|
|
358
|
+
function tool(config) {
|
|
359
|
+
const parsedOptions = ToolDefinitionOptionsSchema.parse({
|
|
360
|
+
name: config.name,
|
|
361
|
+
description: config.description,
|
|
362
|
+
allowedAgentNames: config.allowedAgentNames,
|
|
363
|
+
requiresApproval: config.requiresApproval,
|
|
364
|
+
retry: config.retry,
|
|
365
|
+
timeoutMs: config.timeoutMs,
|
|
366
|
+
sensitiveFieldNames: config.sensitiveFieldNames,
|
|
367
|
+
dryRun: config.dryRun
|
|
368
|
+
});
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
name: parsedOptions.name,
|
|
371
|
+
description: parsedOptions.description,
|
|
372
|
+
inputSchema: config.inputSchema,
|
|
373
|
+
outputSchema: config.outputSchema ?? null,
|
|
374
|
+
execute: config.execute,
|
|
375
|
+
allowedAgentNames: parsedOptions.allowedAgentNames ?? null,
|
|
376
|
+
requiresApproval: parsedOptions.requiresApproval ?? false,
|
|
377
|
+
retry: parsedOptions.retry ?? defaultToolRetryConfig,
|
|
378
|
+
timeoutMs: parsedOptions.timeoutMs ?? null,
|
|
379
|
+
sensitiveFieldNames: parsedOptions.sensitiveFieldNames ?? [],
|
|
380
|
+
dryRun: parsedOptions.dryRun ?? false
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/tools/tool-broker.ts
|
|
385
|
+
import { AppErrorCode as AppErrorCode4 } from "@assemble-dev/shared-types/errors";
|
|
386
|
+
import { AppError as AppError4 } from "@assemble-dev/shared-utils/errors";
|
|
387
|
+
import { generateApprovalId, generateTraceEventId as generateTraceEventId3 } from "@assemble-dev/shared-utils";
|
|
388
|
+
import {
|
|
389
|
+
resolveRedactionMode
|
|
390
|
+
} from "@assemble-dev/shared-utils/redaction";
|
|
391
|
+
|
|
392
|
+
// src/policies/policy-engine.ts
|
|
393
|
+
import {
|
|
394
|
+
PolicyDecisionSchema
|
|
395
|
+
} from "@assemble-dev/shared-types/runs";
|
|
396
|
+
|
|
397
|
+
// src/policies/tool-policy.ts
|
|
398
|
+
var toolPolicyEffectNames = [
|
|
399
|
+
"block",
|
|
400
|
+
"requireApproval",
|
|
401
|
+
"redact",
|
|
402
|
+
"logOnly"
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
// src/policies/policy-engine.ts
|
|
406
|
+
var defaultPolicyName = "default-policy";
|
|
407
|
+
var policyActionPrecedence = [
|
|
408
|
+
"block",
|
|
409
|
+
"requireApproval",
|
|
410
|
+
"redact",
|
|
411
|
+
"logOnly",
|
|
412
|
+
"allow"
|
|
413
|
+
];
|
|
414
|
+
function evaluateToolPolicies(input) {
|
|
415
|
+
const applicablePolicies = input.policies.filter(
|
|
416
|
+
(policy2) => doesPolicyApplyToTool(policy2, input.context.toolName)
|
|
417
|
+
);
|
|
418
|
+
const [firstPolicy, ...remainingPolicies] = applicablePolicies;
|
|
419
|
+
if (firstPolicy === void 0) {
|
|
420
|
+
return buildDefaultAllowEvaluation(input.context, input.timestamp);
|
|
421
|
+
}
|
|
422
|
+
const firstDecision = evaluateSingleToolPolicy(
|
|
423
|
+
firstPolicy,
|
|
424
|
+
input.context,
|
|
425
|
+
input.timestamp
|
|
426
|
+
);
|
|
427
|
+
const remainingDecisions = remainingPolicies.map(
|
|
428
|
+
(policy2) => evaluateSingleToolPolicy(policy2, input.context, input.timestamp)
|
|
429
|
+
);
|
|
430
|
+
const strongestDecision = resolveStrongestPolicyDecision(
|
|
431
|
+
firstDecision,
|
|
432
|
+
remainingDecisions
|
|
433
|
+
);
|
|
434
|
+
return {
|
|
435
|
+
action: strongestDecision.action,
|
|
436
|
+
reason: strongestDecision.reason,
|
|
437
|
+
policyName: strongestDecision.policyName,
|
|
438
|
+
decisions: [firstDecision, ...remainingDecisions]
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function doesPolicyApplyToTool(policy2, toolName) {
|
|
442
|
+
if (policy2.appliesToToolNames === void 0) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
return policy2.appliesToToolNames.includes(toolName);
|
|
446
|
+
}
|
|
447
|
+
function evaluateSingleToolPolicy(policy2, context, timestamp) {
|
|
448
|
+
for (const effectName of toolPolicyEffectNames) {
|
|
449
|
+
const triggeredOutcome = executeToolPolicyRule(policy2, effectName, context);
|
|
450
|
+
if (triggeredOutcome !== null) {
|
|
451
|
+
return buildPolicyDecision({
|
|
452
|
+
policyName: policy2.name,
|
|
453
|
+
action: triggeredOutcome.action,
|
|
454
|
+
reason: triggeredOutcome.reason,
|
|
455
|
+
context,
|
|
456
|
+
timestamp
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return buildPolicyDecision({
|
|
461
|
+
policyName: policy2.name,
|
|
462
|
+
action: "allow",
|
|
463
|
+
reason: "no rules matched",
|
|
464
|
+
context,
|
|
465
|
+
timestamp
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function executeToolPolicyRule(policy2, effectName, context) {
|
|
469
|
+
const evaluateRule = policy2[effectName];
|
|
470
|
+
if (evaluateRule === void 0) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const ruleResult = evaluateRule(context);
|
|
475
|
+
if (ruleResult === false) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (ruleResult === true) {
|
|
479
|
+
return {
|
|
480
|
+
action: effectName,
|
|
481
|
+
reason: buildGeneratedEffectReason(effectName, policy2.name)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
action: effectName,
|
|
486
|
+
reason: ruleResult
|
|
487
|
+
};
|
|
488
|
+
} catch (caughtError) {
|
|
489
|
+
const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
|
490
|
+
return {
|
|
491
|
+
action: "block",
|
|
492
|
+
reason: `blocked by policy ${policy2.name}: ${effectName} rule threw: ${errorMessage}`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function buildGeneratedEffectReason(effectName, policyName) {
|
|
497
|
+
const reasonsByEffectName = {
|
|
498
|
+
block: `blocked by policy ${policyName}`,
|
|
499
|
+
requireApproval: `approval required by policy ${policyName}`,
|
|
500
|
+
redact: `redacted by policy ${policyName}`,
|
|
501
|
+
logOnly: `logged by policy ${policyName}`
|
|
502
|
+
};
|
|
503
|
+
return reasonsByEffectName[effectName];
|
|
504
|
+
}
|
|
505
|
+
function buildPolicyDecision(input) {
|
|
506
|
+
return PolicyDecisionSchema.parse({
|
|
507
|
+
policyName: input.policyName,
|
|
508
|
+
action: input.action,
|
|
509
|
+
agentName: input.context.agentName,
|
|
510
|
+
toolName: input.context.toolName,
|
|
511
|
+
reason: input.reason,
|
|
512
|
+
timestamp: input.timestamp
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function buildDefaultAllowEvaluation(context, timestamp) {
|
|
516
|
+
const defaultDecision = buildPolicyDecision({
|
|
517
|
+
policyName: defaultPolicyName,
|
|
518
|
+
action: "allow",
|
|
519
|
+
reason: `no policies matched tool ${context.toolName}`,
|
|
520
|
+
context,
|
|
521
|
+
timestamp
|
|
522
|
+
});
|
|
523
|
+
return {
|
|
524
|
+
action: defaultDecision.action,
|
|
525
|
+
reason: defaultDecision.reason,
|
|
526
|
+
policyName: defaultDecision.policyName,
|
|
527
|
+
decisions: [defaultDecision]
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function resolveStrongestPolicyDecision(firstDecision, remainingDecisions) {
|
|
531
|
+
let strongestDecision = firstDecision;
|
|
532
|
+
for (const decision of remainingDecisions) {
|
|
533
|
+
const decisionRank = policyActionPrecedence.indexOf(decision.action);
|
|
534
|
+
const strongestRank = policyActionPrecedence.indexOf(
|
|
535
|
+
strongestDecision.action
|
|
536
|
+
);
|
|
537
|
+
if (decisionRank < strongestRank) {
|
|
538
|
+
strongestDecision = decision;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return strongestDecision;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/policies/policy-evaluated-event.ts
|
|
545
|
+
import {
|
|
546
|
+
PolicyEvaluatedEventSchema
|
|
547
|
+
} from "@assemble-dev/shared-types/trace-events";
|
|
548
|
+
function buildPolicyEvaluatedEvent(input) {
|
|
549
|
+
return PolicyEvaluatedEventSchema.parse({
|
|
550
|
+
type: "policy.evaluated",
|
|
551
|
+
id: input.id,
|
|
552
|
+
runId: input.runId,
|
|
553
|
+
workflowName: input.workflowName,
|
|
554
|
+
timestamp: input.timestamp,
|
|
555
|
+
parentEventId: input.parentEventId,
|
|
556
|
+
redacted: input.redacted,
|
|
557
|
+
policyName: input.policyName,
|
|
558
|
+
action: input.action,
|
|
559
|
+
reason: input.reason,
|
|
560
|
+
toolName: input.toolName,
|
|
561
|
+
agentName: input.agentName
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function buildPolicyEvaluatedEvents(input) {
|
|
565
|
+
return input.decisionEvents.map(
|
|
566
|
+
(decisionEvent) => buildPolicyEvaluatedEvent({
|
|
567
|
+
id: decisionEvent.id,
|
|
568
|
+
runId: input.runId,
|
|
569
|
+
workflowName: input.workflowName,
|
|
570
|
+
timestamp: input.timestamp,
|
|
571
|
+
parentEventId: input.parentEventId,
|
|
572
|
+
redacted: input.redacted,
|
|
573
|
+
policyName: decisionEvent.decision.policyName,
|
|
574
|
+
action: decisionEvent.decision.action,
|
|
575
|
+
reason: decisionEvent.decision.reason,
|
|
576
|
+
toolName: input.toolName,
|
|
577
|
+
agentName: input.agentName
|
|
578
|
+
})
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/tools/tool-execution.ts
|
|
583
|
+
function buildToolValidationFailureMessage(target, toolName, issues) {
|
|
584
|
+
const issueSummary = issues.map((issue) => issue.message).join("; ");
|
|
585
|
+
return `${target} validation failed for tool ${toolName}: ${issueSummary}`;
|
|
586
|
+
}
|
|
587
|
+
async function runToolExecutionAttempts(toolDefinition, input) {
|
|
588
|
+
let lastError = {
|
|
589
|
+
code: "EXECUTION_FAILED",
|
|
590
|
+
message: `tool ${toolDefinition.name} failed before executing`
|
|
591
|
+
};
|
|
592
|
+
for (let attempt = 1; attempt <= toolDefinition.retry.maxAttempts; attempt += 1) {
|
|
593
|
+
try {
|
|
594
|
+
return { output: await executeToolAttemptWithTimeout(toolDefinition, input) };
|
|
595
|
+
} catch (caughtError) {
|
|
596
|
+
lastError = {
|
|
597
|
+
code: "EXECUTION_FAILED",
|
|
598
|
+
message: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { error: lastError };
|
|
603
|
+
}
|
|
604
|
+
function validateToolExecutionOutput(toolDefinition, output) {
|
|
605
|
+
if (toolDefinition.outputSchema === null) {
|
|
606
|
+
return { output };
|
|
607
|
+
}
|
|
608
|
+
const parsedOutput = toolDefinition.outputSchema.safeParse(output);
|
|
609
|
+
if (parsedOutput.success) {
|
|
610
|
+
return { output: parsedOutput.data };
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
error: {
|
|
614
|
+
code: "VALIDATION_FAILED",
|
|
615
|
+
message: buildToolValidationFailureMessage(
|
|
616
|
+
"output",
|
|
617
|
+
toolDefinition.name,
|
|
618
|
+
parsedOutput.error.issues
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
async function executeToolAttemptWithTimeout(toolDefinition, input) {
|
|
624
|
+
const timeoutMs = toolDefinition.timeoutMs;
|
|
625
|
+
if (timeoutMs === null) {
|
|
626
|
+
return await toolDefinition.execute(input);
|
|
627
|
+
}
|
|
628
|
+
return await new Promise((resolveAttempt, rejectAttempt) => {
|
|
629
|
+
const timeoutHandle = setTimeout(() => {
|
|
630
|
+
rejectAttempt(
|
|
631
|
+
new Error(
|
|
632
|
+
`tool ${toolDefinition.name} timed out after ${timeoutMs}ms`
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}, timeoutMs);
|
|
636
|
+
const settleWithOutput = (output) => {
|
|
637
|
+
clearTimeout(timeoutHandle);
|
|
638
|
+
resolveAttempt(output);
|
|
639
|
+
};
|
|
640
|
+
const settleWithError = (executionError) => {
|
|
641
|
+
clearTimeout(timeoutHandle);
|
|
642
|
+
rejectAttempt(executionError);
|
|
643
|
+
};
|
|
644
|
+
void toolDefinition.execute(input).then(settleWithOutput, settleWithError);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/tools/tool-trace-events.ts
|
|
649
|
+
import { generateTraceEventId as generateTraceEventId2 } from "@assemble-dev/shared-utils";
|
|
650
|
+
import {
|
|
651
|
+
redactTracePayload
|
|
652
|
+
} from "@assemble-dev/shared-utils/redaction";
|
|
653
|
+
function buildToolEventBase(context, parentEventId, redacted) {
|
|
654
|
+
return {
|
|
655
|
+
id: generateTraceEventId2(),
|
|
656
|
+
runId: context.runId,
|
|
657
|
+
workflowName: context.workflowName,
|
|
658
|
+
timestamp: context.now().toISOString(),
|
|
659
|
+
parentEventId,
|
|
660
|
+
redacted
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function emitToolRequestedEvent(context, input, mode) {
|
|
664
|
+
const redaction = redactTracePayload(input, {
|
|
665
|
+
mode,
|
|
666
|
+
sensitiveFieldNames: context.sensitiveFieldNames
|
|
667
|
+
});
|
|
668
|
+
const emittedEvent = context.traceBus.emitTraceEvent({
|
|
669
|
+
...buildToolEventBase(context, context.parentEventId, redaction.redacted),
|
|
670
|
+
type: "tool.requested",
|
|
671
|
+
toolName: context.toolName,
|
|
672
|
+
agentName: context.agentName,
|
|
673
|
+
input: redaction.payload
|
|
674
|
+
});
|
|
675
|
+
return emittedEvent.id;
|
|
676
|
+
}
|
|
677
|
+
function emitToolBlockedEvent(context, requestEventId, policyName, reason) {
|
|
678
|
+
context.traceBus.emitTraceEvent({
|
|
679
|
+
...buildToolEventBase(context, requestEventId, false),
|
|
680
|
+
type: "tool.blocked",
|
|
681
|
+
toolName: context.toolName,
|
|
682
|
+
agentName: context.agentName,
|
|
683
|
+
policyName,
|
|
684
|
+
reason
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function emitToolCompletedEvent(context, requestEventId, output, latencyMs) {
|
|
688
|
+
const redaction = redactTracePayload(output, {
|
|
689
|
+
mode: context.redactionMode,
|
|
690
|
+
sensitiveFieldNames: context.sensitiveFieldNames
|
|
691
|
+
});
|
|
692
|
+
context.traceBus.emitTraceEvent({
|
|
693
|
+
...buildToolEventBase(context, requestEventId, redaction.redacted),
|
|
694
|
+
type: "tool.completed",
|
|
695
|
+
toolName: context.toolName,
|
|
696
|
+
agentName: context.agentName,
|
|
697
|
+
output: redaction.payload,
|
|
698
|
+
latencyMs
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function emitToolFailedEvent(context, requestEventId, error, latencyMs) {
|
|
702
|
+
const redaction = redactTracePayload(error.message, {
|
|
703
|
+
mode: context.redactionMode,
|
|
704
|
+
sensitiveFieldNames: context.sensitiveFieldNames
|
|
705
|
+
});
|
|
706
|
+
const message = typeof redaction.payload === "string" ? redaction.payload : JSON.stringify(redaction.payload);
|
|
707
|
+
context.traceBus.emitTraceEvent({
|
|
708
|
+
...buildToolEventBase(context, requestEventId, redaction.redacted),
|
|
709
|
+
type: "tool.failed",
|
|
710
|
+
toolName: context.toolName,
|
|
711
|
+
agentName: context.agentName,
|
|
712
|
+
error: { message, code: error.code },
|
|
713
|
+
latencyMs
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function emitToolApprovedEvent(context, toolRequestEventId, approvalId) {
|
|
717
|
+
const emittedEvent = context.traceBus.emitTraceEvent({
|
|
718
|
+
...buildToolEventBase(context, toolRequestEventId, false),
|
|
719
|
+
type: "tool.approved",
|
|
720
|
+
toolName: context.toolName,
|
|
721
|
+
agentName: context.agentName,
|
|
722
|
+
approvalId
|
|
723
|
+
});
|
|
724
|
+
return emittedEvent.id;
|
|
725
|
+
}
|
|
726
|
+
function emitApprovalRequiredEvent(context, toolRequestEventId, approvalId, reason) {
|
|
727
|
+
context.traceBus.emitTraceEvent({
|
|
728
|
+
...buildToolEventBase(context, toolRequestEventId, false),
|
|
729
|
+
type: "approval.required",
|
|
730
|
+
approvalId,
|
|
731
|
+
toolName: context.toolName,
|
|
732
|
+
agentName: context.agentName,
|
|
733
|
+
reason
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/tools/tool-execution-outcome.ts
|
|
738
|
+
async function executeToolAndEmitOutcome(context, toolDefinition, input, requestEventId, mockExecution) {
|
|
739
|
+
const startedAtMs = context.now().getTime();
|
|
740
|
+
if (toolDefinition.dryRun || mockExecution !== void 0) {
|
|
741
|
+
return completeToolCallWithoutExecution(
|
|
742
|
+
context,
|
|
743
|
+
toolDefinition,
|
|
744
|
+
input,
|
|
745
|
+
requestEventId,
|
|
746
|
+
mockExecution,
|
|
747
|
+
startedAtMs
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
const executionResult = await runToolExecutionAttempts(toolDefinition, input);
|
|
751
|
+
if ("error" in executionResult) {
|
|
752
|
+
return failToolCallAfterRequest(
|
|
753
|
+
context,
|
|
754
|
+
requestEventId,
|
|
755
|
+
executionResult.error,
|
|
756
|
+
calculateElapsedMs(context, startedAtMs)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const validatedOutput = validateToolExecutionOutput(
|
|
760
|
+
toolDefinition,
|
|
761
|
+
executionResult.output
|
|
762
|
+
);
|
|
763
|
+
if ("error" in validatedOutput) {
|
|
764
|
+
return failToolCallAfterRequest(
|
|
765
|
+
context,
|
|
766
|
+
requestEventId,
|
|
767
|
+
validatedOutput.error,
|
|
768
|
+
calculateElapsedMs(context, startedAtMs)
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const latencyMs = calculateElapsedMs(context, startedAtMs);
|
|
772
|
+
emitToolCompletedEvent(context, requestEventId, validatedOutput.output, latencyMs);
|
|
773
|
+
return {
|
|
774
|
+
status: "completed",
|
|
775
|
+
output: validatedOutput.output,
|
|
776
|
+
toolRequestEventId: requestEventId,
|
|
777
|
+
latencyMs
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function completeToolCallWithoutExecution(context, toolDefinition, input, requestEventId, mockExecution, startedAtMs) {
|
|
781
|
+
const output = mockExecution !== void 0 ? mockExecution(input) : { dryRun: true, toolName: toolDefinition.name };
|
|
782
|
+
const latencyMs = calculateElapsedMs(context, startedAtMs);
|
|
783
|
+
emitToolCompletedEvent(context, requestEventId, output, latencyMs);
|
|
784
|
+
return {
|
|
785
|
+
status: "completed",
|
|
786
|
+
output,
|
|
787
|
+
toolRequestEventId: requestEventId,
|
|
788
|
+
latencyMs
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function failToolCallAfterRequest(context, requestEventId, error, latencyMs) {
|
|
792
|
+
emitToolFailedEvent(context, requestEventId, error, latencyMs);
|
|
793
|
+
return { status: "failed", error };
|
|
794
|
+
}
|
|
795
|
+
function calculateElapsedMs(context, startedAtMs) {
|
|
796
|
+
return Math.max(0, context.now().getTime() - startedAtMs);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/tools/tool-broker.ts
|
|
800
|
+
var agentAllowlistPolicyName = "agent-allowlist";
|
|
801
|
+
var staticApprovalReason = "tool requires approval";
|
|
802
|
+
async function executeToolCall(callInput) {
|
|
803
|
+
const context = buildToolBrokerContext(callInput);
|
|
804
|
+
const parsedInput = callInput.tool.inputSchema.safeParse(callInput.input);
|
|
805
|
+
if (!parsedInput.success) {
|
|
806
|
+
return failToolCallForInvalidInput(
|
|
807
|
+
context,
|
|
808
|
+
callInput.input,
|
|
809
|
+
parsedInput.error.issues
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (callInput.grantedApproval !== void 0) {
|
|
813
|
+
return await executeGrantedApprovalToolCall(
|
|
814
|
+
context,
|
|
815
|
+
callInput,
|
|
816
|
+
parsedInput.data,
|
|
817
|
+
callInput.grantedApproval
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (!isAgentAllowedToUseTool(callInput.tool, callInput.agentName)) {
|
|
821
|
+
return blockToolCallForDisallowedAgent(context, parsedInput.data);
|
|
822
|
+
}
|
|
823
|
+
const evaluation = evaluatePoliciesForToolCall(
|
|
824
|
+
context,
|
|
825
|
+
callInput.policies,
|
|
826
|
+
parsedInput.data
|
|
827
|
+
);
|
|
828
|
+
if (evaluation !== null && evaluation.action === "block") {
|
|
829
|
+
return blockToolCallByPolicy(context, parsedInput.data, evaluation);
|
|
830
|
+
}
|
|
831
|
+
const requestEventId = emitToolRequestedEvent(
|
|
832
|
+
context,
|
|
833
|
+
parsedInput.data,
|
|
834
|
+
resolveInputRedactionMode(context, evaluation)
|
|
835
|
+
);
|
|
836
|
+
const approvalReason = resolveApprovalReason(callInput.tool, evaluation);
|
|
837
|
+
if (approvalReason !== null) {
|
|
838
|
+
return requireApprovalForToolCall(context, requestEventId, approvalReason);
|
|
839
|
+
}
|
|
840
|
+
return await executeToolAndEmitOutcome(
|
|
841
|
+
context,
|
|
842
|
+
callInput.tool,
|
|
843
|
+
parsedInput.data,
|
|
844
|
+
requestEventId,
|
|
845
|
+
callInput.mockExecution
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
function buildToolBrokerContext(callInput) {
|
|
849
|
+
const runState = callInput.traceBus.getRunState();
|
|
850
|
+
return {
|
|
851
|
+
toolName: callInput.tool.name,
|
|
852
|
+
agentName: callInput.agentName,
|
|
853
|
+
runId: runState.runId,
|
|
854
|
+
workflowName: runState.workflowName,
|
|
855
|
+
parentEventId: callInput.parentEventId ?? null,
|
|
856
|
+
traceBus: callInput.traceBus,
|
|
857
|
+
now: callInput.now ?? (() => /* @__PURE__ */ new Date()),
|
|
858
|
+
redactionMode: resolveRedactionMode(callInput.redactionMode),
|
|
859
|
+
sensitiveFieldNames: callInput.tool.sensitiveFieldNames
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function isAgentAllowedToUseTool(toolDefinition, agentName) {
|
|
863
|
+
if (toolDefinition.allowedAgentNames === null) {
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
return toolDefinition.allowedAgentNames.includes(agentName);
|
|
867
|
+
}
|
|
868
|
+
function resolveInputRedactionMode(context, evaluation) {
|
|
869
|
+
if (evaluation !== null && evaluation.action === "redact") {
|
|
870
|
+
return "redacted";
|
|
871
|
+
}
|
|
872
|
+
return context.redactionMode;
|
|
873
|
+
}
|
|
874
|
+
function resolveApprovalReason(toolDefinition, evaluation) {
|
|
875
|
+
if (evaluation !== null && evaluation.action === "requireApproval") {
|
|
876
|
+
return evaluation.reason;
|
|
877
|
+
}
|
|
878
|
+
if (toolDefinition.requiresApproval) {
|
|
879
|
+
return staticApprovalReason;
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
function evaluatePoliciesForToolCall(context, policies, input) {
|
|
884
|
+
if (policies === void 0) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const timestamp = context.now().toISOString();
|
|
888
|
+
const evaluation = evaluateToolPolicies({
|
|
889
|
+
policies,
|
|
890
|
+
context: {
|
|
891
|
+
toolName: context.toolName,
|
|
892
|
+
agentName: context.agentName,
|
|
893
|
+
input
|
|
894
|
+
},
|
|
895
|
+
timestamp
|
|
896
|
+
});
|
|
897
|
+
const policyEvents = buildPolicyEvaluatedEvents({
|
|
898
|
+
runId: context.runId,
|
|
899
|
+
workflowName: context.workflowName,
|
|
900
|
+
timestamp,
|
|
901
|
+
parentEventId: context.parentEventId,
|
|
902
|
+
redacted: false,
|
|
903
|
+
toolName: context.toolName,
|
|
904
|
+
agentName: context.agentName,
|
|
905
|
+
decisionEvents: evaluation.decisions.map((decision) => ({
|
|
906
|
+
id: generateTraceEventId3(),
|
|
907
|
+
decision
|
|
908
|
+
}))
|
|
909
|
+
});
|
|
910
|
+
for (const policyEvent of policyEvents) {
|
|
911
|
+
context.traceBus.emitTraceEvent(policyEvent);
|
|
912
|
+
}
|
|
913
|
+
return evaluation;
|
|
914
|
+
}
|
|
915
|
+
function failToolCallForInvalidInput(context, rawInput, issues) {
|
|
916
|
+
const requestEventId = emitToolRequestedEvent(
|
|
917
|
+
context,
|
|
918
|
+
rawInput,
|
|
919
|
+
context.redactionMode
|
|
920
|
+
);
|
|
921
|
+
const error = {
|
|
922
|
+
code: "VALIDATION_FAILED",
|
|
923
|
+
message: buildToolValidationFailureMessage(
|
|
924
|
+
"input",
|
|
925
|
+
context.toolName,
|
|
926
|
+
issues
|
|
927
|
+
)
|
|
928
|
+
};
|
|
929
|
+
emitToolFailedEvent(context, requestEventId, error, 0);
|
|
930
|
+
return { status: "failed", error };
|
|
931
|
+
}
|
|
932
|
+
function blockToolCallForDisallowedAgent(context, input) {
|
|
933
|
+
const reason = `agent ${context.agentName} is not allowed to use tool ${context.toolName}`;
|
|
934
|
+
const requestEventId = emitToolRequestedEvent(
|
|
935
|
+
context,
|
|
936
|
+
input,
|
|
937
|
+
context.redactionMode
|
|
938
|
+
);
|
|
939
|
+
emitToolBlockedEvent(context, requestEventId, agentAllowlistPolicyName, reason);
|
|
940
|
+
return { status: "blocked", reason };
|
|
941
|
+
}
|
|
942
|
+
function blockToolCallByPolicy(context, input, evaluation) {
|
|
943
|
+
const requestEventId = emitToolRequestedEvent(
|
|
944
|
+
context,
|
|
945
|
+
input,
|
|
946
|
+
context.redactionMode
|
|
947
|
+
);
|
|
948
|
+
emitToolBlockedEvent(
|
|
949
|
+
context,
|
|
950
|
+
requestEventId,
|
|
951
|
+
evaluation.policyName,
|
|
952
|
+
evaluation.reason
|
|
953
|
+
);
|
|
954
|
+
return { status: "blocked", reason: evaluation.reason };
|
|
955
|
+
}
|
|
956
|
+
function requireApprovalForToolCall(context, toolRequestEventId, reason) {
|
|
957
|
+
const approvalId = generateApprovalId();
|
|
958
|
+
emitApprovalRequiredEvent(context, toolRequestEventId, approvalId, reason);
|
|
959
|
+
return { status: "awaiting_approval", approvalId, toolRequestEventId };
|
|
960
|
+
}
|
|
961
|
+
function ensureGrantedApprovalIsKnown(context, grantedApproval) {
|
|
962
|
+
const runApproval = context.traceBus.getRunState().approvals.find((approval) => approval.id === grantedApproval.approvalId);
|
|
963
|
+
if (runApproval === void 0 || runApproval.status === "rejected") {
|
|
964
|
+
throw new AppError4({
|
|
965
|
+
code: AppErrorCode4.Forbidden,
|
|
966
|
+
message: `granted approval "${grantedApproval.approvalId}" does not match a grantable approval for run "${context.runId}"; the tool call cannot use this grant`,
|
|
967
|
+
statusCode: 403,
|
|
968
|
+
details: {
|
|
969
|
+
approvalId: grantedApproval.approvalId,
|
|
970
|
+
toolName: context.toolName,
|
|
971
|
+
agentName: context.agentName,
|
|
972
|
+
runId: context.runId
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function executeGrantedApprovalToolCall(context, callInput, input, grantedApproval) {
|
|
978
|
+
ensureGrantedApprovalIsKnown(context, grantedApproval);
|
|
979
|
+
if (!isAgentAllowedToUseTool(callInput.tool, callInput.agentName)) {
|
|
980
|
+
return blockToolCallForDisallowedAgent(context, input);
|
|
981
|
+
}
|
|
982
|
+
const evaluation = evaluatePoliciesForToolCall(
|
|
983
|
+
context,
|
|
984
|
+
callInput.policies,
|
|
985
|
+
input
|
|
986
|
+
);
|
|
987
|
+
if (evaluation !== null && evaluation.action === "block") {
|
|
988
|
+
return blockToolCallByPolicy(context, input, evaluation);
|
|
989
|
+
}
|
|
990
|
+
const approvedEventId = emitToolApprovedEvent(
|
|
991
|
+
context,
|
|
992
|
+
grantedApproval.toolRequestEventId,
|
|
993
|
+
grantedApproval.approvalId
|
|
994
|
+
);
|
|
995
|
+
const requestEventId = grantedApproval.toolRequestEventId ?? approvedEventId;
|
|
996
|
+
return await executeToolAndEmitOutcome(
|
|
997
|
+
context,
|
|
998
|
+
callInput.tool,
|
|
999
|
+
input,
|
|
1000
|
+
requestEventId,
|
|
1001
|
+
callInput.mockExecution
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/teams/team-definition.ts
|
|
1006
|
+
import { AppErrorCode as AppErrorCode5 } from "@assemble-dev/shared-types/errors";
|
|
1007
|
+
import { AppError as AppError5 } from "@assemble-dev/shared-utils/errors";
|
|
1008
|
+
function team(config) {
|
|
1009
|
+
ensureTeamNameIsPresent(config.name);
|
|
1010
|
+
ensureTeamHasAgents(config.name, config.agents);
|
|
1011
|
+
ensureTeamAgentNamesAreUnique(config.name, config.agents);
|
|
1012
|
+
return Object.freeze({
|
|
1013
|
+
name: config.name,
|
|
1014
|
+
description: config.description ?? "",
|
|
1015
|
+
agents: Object.freeze([...config.agents]),
|
|
1016
|
+
agentNames: Object.freeze(config.agents.map((member) => member.name))
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function ensureTeamNameIsPresent(name) {
|
|
1020
|
+
if (name.trim() !== "") {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
throw new AppError5({
|
|
1024
|
+
code: AppErrorCode5.BadRequest,
|
|
1025
|
+
message: "team name must not be empty",
|
|
1026
|
+
statusCode: 400
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
function ensureTeamHasAgents(teamName, agents) {
|
|
1030
|
+
if (agents.length > 0) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
throw new AppError5({
|
|
1034
|
+
code: AppErrorCode5.BadRequest,
|
|
1035
|
+
message: `team "${teamName}" must include at least one agent`,
|
|
1036
|
+
statusCode: 400
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
function ensureTeamAgentNamesAreUnique(teamName, agents) {
|
|
1040
|
+
const seenAgentNames = /* @__PURE__ */ new Set();
|
|
1041
|
+
for (const member of agents) {
|
|
1042
|
+
if (seenAgentNames.has(member.name)) {
|
|
1043
|
+
throw new AppError5({
|
|
1044
|
+
code: AppErrorCode5.BadRequest,
|
|
1045
|
+
message: `team "${teamName}" contains duplicate agent name "${member.name}"`,
|
|
1046
|
+
statusCode: 400
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
seenAgentNames.add(member.name);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/teams/team-presets.ts
|
|
1054
|
+
function buildPipelineTeamWorkflowConfig(input) {
|
|
1055
|
+
return {
|
|
1056
|
+
name: input.name,
|
|
1057
|
+
inputSchema: input.inputSchema,
|
|
1058
|
+
outputSchema: input.outputSchema,
|
|
1059
|
+
agents: [...input.team.agents],
|
|
1060
|
+
topology: { kind: "pipeline" }
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function buildSupervisorTeamWorkflowConfig(input) {
|
|
1064
|
+
return {
|
|
1065
|
+
name: input.name,
|
|
1066
|
+
inputSchema: input.inputSchema,
|
|
1067
|
+
outputSchema: input.outputSchema,
|
|
1068
|
+
agents: [...input.team.agents],
|
|
1069
|
+
topology: {
|
|
1070
|
+
kind: "supervisor",
|
|
1071
|
+
supervisorName: input.supervisorName,
|
|
1072
|
+
route: input.route,
|
|
1073
|
+
maxSteps: input.maxSteps
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function buildReviewPanelWorkflowConfig(input) {
|
|
1078
|
+
return {
|
|
1079
|
+
name: input.name,
|
|
1080
|
+
inputSchema: input.inputSchema,
|
|
1081
|
+
agents: [...input.team.agents],
|
|
1082
|
+
topology: { kind: "parallel" }
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/policies/policy-definition.ts
|
|
1087
|
+
import { AppErrorCode as AppErrorCode6 } from "@assemble-dev/shared-types/errors";
|
|
1088
|
+
import { AppError as AppError6 } from "@assemble-dev/shared-utils/errors";
|
|
1089
|
+
function policy(config) {
|
|
1090
|
+
ensurePolicyNameIsPresent(config.name);
|
|
1091
|
+
ensurePolicyDefinesAtLeastOneRule(config);
|
|
1092
|
+
return Object.freeze({ ...config });
|
|
1093
|
+
}
|
|
1094
|
+
function ensurePolicyNameIsPresent(name) {
|
|
1095
|
+
if (name.trim() !== "") {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
throw new AppError6({
|
|
1099
|
+
code: AppErrorCode6.BadRequest,
|
|
1100
|
+
message: "policy name must not be empty",
|
|
1101
|
+
statusCode: 400
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function ensurePolicyDefinesAtLeastOneRule(config) {
|
|
1105
|
+
const hasRule = toolPolicyEffectNames.some(
|
|
1106
|
+
(effectName) => config[effectName] !== void 0
|
|
1107
|
+
);
|
|
1108
|
+
if (hasRule) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
throw new AppError6({
|
|
1112
|
+
code: AppErrorCode6.BadRequest,
|
|
1113
|
+
message: `policy "${config.name}" must define at least one rule (${toolPolicyEffectNames.join(", ")})`,
|
|
1114
|
+
statusCode: 400
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/workflows/workflow-runtime.ts
|
|
1119
|
+
import { AppErrorCode as AppErrorCode12 } from "@assemble-dev/shared-types/errors";
|
|
1120
|
+
import { generateRunId } from "@assemble-dev/shared-utils/ids";
|
|
1121
|
+
import { AppError as AppError12 } from "@assemble-dev/shared-utils/errors";
|
|
1122
|
+
|
|
1123
|
+
// src/memory/artifact-store.ts
|
|
1124
|
+
import { JsonObjectSchema } from "@assemble-dev/shared-types/json";
|
|
1125
|
+
import { RunArtifactSchema } from "@assemble-dev/shared-types/runs";
|
|
1126
|
+
function buildRunArtifactsMemoryKey(runId) {
|
|
1127
|
+
return `run:${runId}:artifacts`;
|
|
1128
|
+
}
|
|
1129
|
+
function mapRunArtifactToJsonValue(artifact) {
|
|
1130
|
+
const artifactValue = {
|
|
1131
|
+
key: artifact.key,
|
|
1132
|
+
value: artifact.value,
|
|
1133
|
+
createdAt: artifact.createdAt
|
|
1134
|
+
};
|
|
1135
|
+
if (artifact.producedByAgentName !== void 0) {
|
|
1136
|
+
artifactValue.producedByAgentName = artifact.producedByAgentName;
|
|
1137
|
+
}
|
|
1138
|
+
return artifactValue;
|
|
1139
|
+
}
|
|
1140
|
+
async function loadRunArtifactsRecord(provider, memoryKey) {
|
|
1141
|
+
const storedArtifacts = await provider.getMemoryValue(memoryKey);
|
|
1142
|
+
if (storedArtifacts === null) {
|
|
1143
|
+
return {};
|
|
1144
|
+
}
|
|
1145
|
+
return JsonObjectSchema.parse(storedArtifacts);
|
|
1146
|
+
}
|
|
1147
|
+
function createArtifactStore(options) {
|
|
1148
|
+
const memoryKey = buildRunArtifactsMemoryKey(options.runId);
|
|
1149
|
+
return {
|
|
1150
|
+
async saveRunArtifact(artifact) {
|
|
1151
|
+
const artifactsRecord = await loadRunArtifactsRecord(
|
|
1152
|
+
options.provider,
|
|
1153
|
+
memoryKey
|
|
1154
|
+
);
|
|
1155
|
+
artifactsRecord[artifact.key] = mapRunArtifactToJsonValue(artifact);
|
|
1156
|
+
await options.provider.setMemoryValue(memoryKey, artifactsRecord);
|
|
1157
|
+
},
|
|
1158
|
+
async getRunArtifact(key) {
|
|
1159
|
+
const artifactsRecord = await loadRunArtifactsRecord(
|
|
1160
|
+
options.provider,
|
|
1161
|
+
memoryKey
|
|
1162
|
+
);
|
|
1163
|
+
const storedArtifact = artifactsRecord[key];
|
|
1164
|
+
if (storedArtifact === void 0) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
return RunArtifactSchema.parse(storedArtifact);
|
|
1168
|
+
},
|
|
1169
|
+
async listRunArtifacts() {
|
|
1170
|
+
const artifactsRecord = await loadRunArtifactsRecord(
|
|
1171
|
+
options.provider,
|
|
1172
|
+
memoryKey
|
|
1173
|
+
);
|
|
1174
|
+
return Object.values(artifactsRecord).map(
|
|
1175
|
+
(storedArtifact) => RunArtifactSchema.parse(storedArtifact)
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/memory/memory-provider.ts
|
|
1182
|
+
function buildAppendedMemoryValue(existingValue, appendedValue) {
|
|
1183
|
+
if (existingValue === void 0) {
|
|
1184
|
+
return [appendedValue];
|
|
1185
|
+
}
|
|
1186
|
+
if (Array.isArray(existingValue)) {
|
|
1187
|
+
return [...existingValue, appendedValue];
|
|
1188
|
+
}
|
|
1189
|
+
return [existingValue, appendedValue];
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/memory/in-memory-provider.ts
|
|
1193
|
+
function cloneJsonValue(value) {
|
|
1194
|
+
return structuredClone(value);
|
|
1195
|
+
}
|
|
1196
|
+
function createInMemoryMemoryProvider() {
|
|
1197
|
+
const memoryEntries = /* @__PURE__ */ new Map();
|
|
1198
|
+
return {
|
|
1199
|
+
async getMemoryValue(key) {
|
|
1200
|
+
const storedValue = memoryEntries.get(key);
|
|
1201
|
+
if (storedValue === void 0) {
|
|
1202
|
+
return await Promise.resolve(null);
|
|
1203
|
+
}
|
|
1204
|
+
return await Promise.resolve(cloneJsonValue(storedValue));
|
|
1205
|
+
},
|
|
1206
|
+
async setMemoryValue(key, value) {
|
|
1207
|
+
memoryEntries.set(key, cloneJsonValue(value));
|
|
1208
|
+
await Promise.resolve();
|
|
1209
|
+
},
|
|
1210
|
+
async appendMemoryValue(key, value) {
|
|
1211
|
+
memoryEntries.set(
|
|
1212
|
+
key,
|
|
1213
|
+
buildAppendedMemoryValue(memoryEntries.get(key), cloneJsonValue(value))
|
|
1214
|
+
);
|
|
1215
|
+
await Promise.resolve();
|
|
1216
|
+
},
|
|
1217
|
+
async listMemoryKeys() {
|
|
1218
|
+
return await Promise.resolve([...memoryEntries.keys()]);
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/memory/memory-config.ts
|
|
1224
|
+
function resolveWorkflowMemoryConfig(config) {
|
|
1225
|
+
if (config !== void 0) {
|
|
1226
|
+
return { provider: config.provider };
|
|
1227
|
+
}
|
|
1228
|
+
return { provider: createInMemoryMemoryProvider() };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/memory/thread-memory.ts
|
|
1232
|
+
import { RunMessageSchema } from "@assemble-dev/shared-types/runs";
|
|
1233
|
+
function buildThreadMessagesMemoryKey(threadKey) {
|
|
1234
|
+
return `thread:${threadKey}:messages`;
|
|
1235
|
+
}
|
|
1236
|
+
function mapRunMessageToJsonValue(message) {
|
|
1237
|
+
const messageValue = {
|
|
1238
|
+
role: message.role,
|
|
1239
|
+
content: message.content,
|
|
1240
|
+
timestamp: message.timestamp
|
|
1241
|
+
};
|
|
1242
|
+
if (message.agentName !== void 0) {
|
|
1243
|
+
messageValue.agentName = message.agentName;
|
|
1244
|
+
}
|
|
1245
|
+
if (message.toolName !== void 0) {
|
|
1246
|
+
messageValue.toolName = message.toolName;
|
|
1247
|
+
}
|
|
1248
|
+
return messageValue;
|
|
1249
|
+
}
|
|
1250
|
+
function createThreadMemory(options) {
|
|
1251
|
+
const memoryKey = buildThreadMessagesMemoryKey(options.threadKey);
|
|
1252
|
+
return {
|
|
1253
|
+
async appendThreadMessage(message) {
|
|
1254
|
+
await options.provider.appendMemoryValue(
|
|
1255
|
+
memoryKey,
|
|
1256
|
+
mapRunMessageToJsonValue(message)
|
|
1257
|
+
);
|
|
1258
|
+
},
|
|
1259
|
+
async listThreadMessages() {
|
|
1260
|
+
const storedMessages = await options.provider.getMemoryValue(memoryKey);
|
|
1261
|
+
if (storedMessages === null) {
|
|
1262
|
+
return [];
|
|
1263
|
+
}
|
|
1264
|
+
return RunMessageSchema.array().parse(storedMessages);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/tracing/trace-bus.ts
|
|
1270
|
+
import {
|
|
1271
|
+
TraceEventSchema
|
|
1272
|
+
} from "@assemble-dev/shared-types/trace-events";
|
|
1273
|
+
|
|
1274
|
+
// src/tracing/run-state-error-mapper.ts
|
|
1275
|
+
function mapEventError(error, timestamp, agentName, toolName) {
|
|
1276
|
+
return {
|
|
1277
|
+
message: error.message,
|
|
1278
|
+
code: error.code,
|
|
1279
|
+
agentName,
|
|
1280
|
+
toolName,
|
|
1281
|
+
timestamp
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/tracing/run-state-agent-tool-reducers.ts
|
|
1286
|
+
function applyAgentStarted(state, event) {
|
|
1287
|
+
const agentCall = {
|
|
1288
|
+
id: event.id,
|
|
1289
|
+
agentName: event.agentName,
|
|
1290
|
+
status: "running",
|
|
1291
|
+
input: event.input,
|
|
1292
|
+
output: null,
|
|
1293
|
+
error: null,
|
|
1294
|
+
startedAt: event.timestamp,
|
|
1295
|
+
completedAt: null,
|
|
1296
|
+
latencyMs: null
|
|
1297
|
+
};
|
|
1298
|
+
return { ...state, agentCalls: [...state.agentCalls, agentCall] };
|
|
1299
|
+
}
|
|
1300
|
+
function applyAgentCompleted(state, event) {
|
|
1301
|
+
return updateAgentCall(state, event.parentEventId, event.agentName, {
|
|
1302
|
+
status: "completed",
|
|
1303
|
+
output: event.output,
|
|
1304
|
+
completedAt: event.timestamp,
|
|
1305
|
+
latencyMs: event.latencyMs
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
function applyAgentFailed(state, event) {
|
|
1309
|
+
const failedState = updateAgentCall(
|
|
1310
|
+
state,
|
|
1311
|
+
event.parentEventId,
|
|
1312
|
+
event.agentName,
|
|
1313
|
+
{
|
|
1314
|
+
status: "failed",
|
|
1315
|
+
error: mapEventError(event.error, event.timestamp, event.agentName),
|
|
1316
|
+
completedAt: event.timestamp,
|
|
1317
|
+
latencyMs: event.latencyMs
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
return {
|
|
1321
|
+
...failedState,
|
|
1322
|
+
errors: [
|
|
1323
|
+
...failedState.errors,
|
|
1324
|
+
mapEventError(event.error, event.timestamp, event.agentName)
|
|
1325
|
+
]
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function applyToolRequested(state, event) {
|
|
1329
|
+
const toolCall = {
|
|
1330
|
+
id: event.id,
|
|
1331
|
+
toolName: event.toolName,
|
|
1332
|
+
agentName: event.agentName,
|
|
1333
|
+
status: "requested",
|
|
1334
|
+
input: event.input,
|
|
1335
|
+
output: null,
|
|
1336
|
+
error: null,
|
|
1337
|
+
requestedAt: event.timestamp,
|
|
1338
|
+
completedAt: null,
|
|
1339
|
+
latencyMs: null
|
|
1340
|
+
};
|
|
1341
|
+
return { ...state, toolCalls: [...state.toolCalls, toolCall] };
|
|
1342
|
+
}
|
|
1343
|
+
function applyToolBlocked(state, event) {
|
|
1344
|
+
return applyToolStatus(state, event.parentEventId, event.toolName, {
|
|
1345
|
+
status: "blocked",
|
|
1346
|
+
completedAt: event.timestamp
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
function applyToolCompleted(state, event) {
|
|
1350
|
+
return applyToolStatus(state, event.parentEventId, event.toolName, {
|
|
1351
|
+
status: "completed",
|
|
1352
|
+
output: event.output,
|
|
1353
|
+
completedAt: event.timestamp,
|
|
1354
|
+
latencyMs: event.latencyMs
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
function applyToolFailed(state, event) {
|
|
1358
|
+
return applyToolStatus(state, event.parentEventId, event.toolName, {
|
|
1359
|
+
status: "failed",
|
|
1360
|
+
error: mapEventError(event.error, event.timestamp, void 0, event.toolName),
|
|
1361
|
+
completedAt: event.timestamp,
|
|
1362
|
+
latencyMs: event.latencyMs
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
function updateAgentCall(state, parentEventId, agentName, update) {
|
|
1366
|
+
const targetIndex = findCallIndexByParentThenPredicate(
|
|
1367
|
+
state.agentCalls,
|
|
1368
|
+
parentEventId,
|
|
1369
|
+
(call) => call.agentName === agentName && call.status === "running"
|
|
1370
|
+
);
|
|
1371
|
+
if (targetIndex === -1) {
|
|
1372
|
+
return state;
|
|
1373
|
+
}
|
|
1374
|
+
const agentCalls = state.agentCalls.map(
|
|
1375
|
+
(call, index) => index === targetIndex ? { ...call, ...update } : call
|
|
1376
|
+
);
|
|
1377
|
+
return { ...state, agentCalls };
|
|
1378
|
+
}
|
|
1379
|
+
function applyToolStatus(state, parentEventId, toolName, update) {
|
|
1380
|
+
const targetIndex = findCallIndexByParentThenPredicate(
|
|
1381
|
+
state.toolCalls,
|
|
1382
|
+
parentEventId,
|
|
1383
|
+
(call) => call.toolName === toolName && (call.status === "requested" || call.status === "approved")
|
|
1384
|
+
);
|
|
1385
|
+
if (targetIndex === -1) {
|
|
1386
|
+
return state;
|
|
1387
|
+
}
|
|
1388
|
+
const toolCalls = state.toolCalls.map(
|
|
1389
|
+
(call, index) => index === targetIndex ? { ...call, ...update } : call
|
|
1390
|
+
);
|
|
1391
|
+
return { ...state, toolCalls };
|
|
1392
|
+
}
|
|
1393
|
+
function findCallIndexByParentThenPredicate(calls, parentEventId, fallbackPredicate) {
|
|
1394
|
+
if (parentEventId !== null) {
|
|
1395
|
+
const parentIndex = findLastIndex(
|
|
1396
|
+
calls,
|
|
1397
|
+
(call) => call.id === parentEventId
|
|
1398
|
+
);
|
|
1399
|
+
if (parentIndex !== -1) {
|
|
1400
|
+
return parentIndex;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return findLastIndex(calls, fallbackPredicate);
|
|
1404
|
+
}
|
|
1405
|
+
function findLastIndex(items, predicate) {
|
|
1406
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
1407
|
+
const item = items[index];
|
|
1408
|
+
if (item !== void 0 && predicate(item)) {
|
|
1409
|
+
return index;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return -1;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/tracing/run-state-approval-routing-reducers.ts
|
|
1416
|
+
function applyPolicyEvaluated(state, event) {
|
|
1417
|
+
return {
|
|
1418
|
+
...state,
|
|
1419
|
+
policyDecisions: [
|
|
1420
|
+
...state.policyDecisions,
|
|
1421
|
+
{
|
|
1422
|
+
policyName: event.policyName,
|
|
1423
|
+
action: event.action,
|
|
1424
|
+
agentName: event.agentName,
|
|
1425
|
+
toolName: event.toolName,
|
|
1426
|
+
reason: event.reason,
|
|
1427
|
+
timestamp: event.timestamp
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function applyRouteSelected(state, event) {
|
|
1433
|
+
return {
|
|
1434
|
+
...state,
|
|
1435
|
+
routingDecisions: [
|
|
1436
|
+
...state.routingDecisions,
|
|
1437
|
+
{
|
|
1438
|
+
supervisorName: event.supervisorName,
|
|
1439
|
+
selectedAgentName: event.selectedAgentName,
|
|
1440
|
+
candidateAgentNames: event.candidateAgentNames,
|
|
1441
|
+
reason: event.reason,
|
|
1442
|
+
timestamp: event.timestamp
|
|
1443
|
+
}
|
|
1444
|
+
]
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function applyApprovalRequired(state, event) {
|
|
1448
|
+
return {
|
|
1449
|
+
...state,
|
|
1450
|
+
status: "awaiting_approval",
|
|
1451
|
+
approvals: [
|
|
1452
|
+
...state.approvals,
|
|
1453
|
+
{
|
|
1454
|
+
id: event.approvalId,
|
|
1455
|
+
status: "pending",
|
|
1456
|
+
toolCallId: event.parentEventId,
|
|
1457
|
+
toolName: event.toolName,
|
|
1458
|
+
agentName: event.agentName,
|
|
1459
|
+
reason: event.reason,
|
|
1460
|
+
requestedAt: event.timestamp,
|
|
1461
|
+
decidedAt: null,
|
|
1462
|
+
decidedBy: null
|
|
1463
|
+
}
|
|
1464
|
+
]
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function applyApprovalDecision(state, event, status, reason) {
|
|
1468
|
+
const approvals = state.approvals.map(
|
|
1469
|
+
(approval) => approval.id === event.approvalId ? {
|
|
1470
|
+
...approval,
|
|
1471
|
+
status,
|
|
1472
|
+
reason: reason ?? approval.reason,
|
|
1473
|
+
decidedAt: event.timestamp,
|
|
1474
|
+
decidedBy: event.decidedBy
|
|
1475
|
+
} : approval
|
|
1476
|
+
);
|
|
1477
|
+
const hasPendingApprovals = approvals.some(
|
|
1478
|
+
(approval) => approval.status === "pending"
|
|
1479
|
+
);
|
|
1480
|
+
return {
|
|
1481
|
+
...state,
|
|
1482
|
+
approvals,
|
|
1483
|
+
status: hasPendingApprovals ? "awaiting_approval" : "running"
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/tracing/run-state-reducer.ts
|
|
1488
|
+
function createInitialRunState(input) {
|
|
1489
|
+
return {
|
|
1490
|
+
runId: input.runId,
|
|
1491
|
+
workflowName: input.workflowName,
|
|
1492
|
+
status: "pending",
|
|
1493
|
+
input: input.input,
|
|
1494
|
+
messages: [],
|
|
1495
|
+
agentCalls: [],
|
|
1496
|
+
toolCalls: [],
|
|
1497
|
+
policyDecisions: [],
|
|
1498
|
+
routingDecisions: [],
|
|
1499
|
+
approvals: [],
|
|
1500
|
+
artifacts: [],
|
|
1501
|
+
errors: [],
|
|
1502
|
+
metrics: {
|
|
1503
|
+
totalLatencyMs: null,
|
|
1504
|
+
tokenUsage: null,
|
|
1505
|
+
agentCallCount: 0,
|
|
1506
|
+
toolCallCount: 0,
|
|
1507
|
+
policyDecisionCount: 0,
|
|
1508
|
+
approvalCount: 0
|
|
1509
|
+
},
|
|
1510
|
+
finalOutput: null,
|
|
1511
|
+
createdAt: input.createdAt,
|
|
1512
|
+
startedAt: null,
|
|
1513
|
+
completedAt: null
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function applyTraceEventToRunState(state, event) {
|
|
1517
|
+
const nextState = reduceTraceEvent(state, event);
|
|
1518
|
+
return {
|
|
1519
|
+
...nextState,
|
|
1520
|
+
metrics: {
|
|
1521
|
+
...nextState.metrics,
|
|
1522
|
+
agentCallCount: nextState.agentCalls.length,
|
|
1523
|
+
toolCallCount: nextState.toolCalls.length,
|
|
1524
|
+
policyDecisionCount: nextState.policyDecisions.length,
|
|
1525
|
+
approvalCount: nextState.approvals.length
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function reduceTraceEvent(state, event) {
|
|
1530
|
+
switch (event.type) {
|
|
1531
|
+
case "run.started":
|
|
1532
|
+
return applyRunStarted(state, event);
|
|
1533
|
+
case "run.completed":
|
|
1534
|
+
return applyRunCompleted(state, event);
|
|
1535
|
+
case "run.failed":
|
|
1536
|
+
return applyRunFailed(state, event);
|
|
1537
|
+
case "agent.started":
|
|
1538
|
+
return applyAgentStarted(state, event);
|
|
1539
|
+
case "agent.completed":
|
|
1540
|
+
return applyAgentCompleted(state, event);
|
|
1541
|
+
case "agent.failed":
|
|
1542
|
+
return applyAgentFailed(state, event);
|
|
1543
|
+
case "tool.requested":
|
|
1544
|
+
return applyToolRequested(state, event);
|
|
1545
|
+
case "tool.approved":
|
|
1546
|
+
return applyToolStatus(state, event.parentEventId, event.toolName, {
|
|
1547
|
+
status: "approved"
|
|
1548
|
+
});
|
|
1549
|
+
case "tool.blocked":
|
|
1550
|
+
return applyToolBlocked(state, event);
|
|
1551
|
+
case "tool.completed":
|
|
1552
|
+
return applyToolCompleted(state, event);
|
|
1553
|
+
case "tool.failed":
|
|
1554
|
+
return applyToolFailed(state, event);
|
|
1555
|
+
case "policy.evaluated":
|
|
1556
|
+
return applyPolicyEvaluated(state, event);
|
|
1557
|
+
case "route.selected":
|
|
1558
|
+
return applyRouteSelected(state, event);
|
|
1559
|
+
case "approval.required":
|
|
1560
|
+
return applyApprovalRequired(state, event);
|
|
1561
|
+
case "approval.approved":
|
|
1562
|
+
return applyApprovalDecision(state, event, "approved", null);
|
|
1563
|
+
case "approval.rejected":
|
|
1564
|
+
return applyApprovalDecision(state, event, "rejected", event.reason);
|
|
1565
|
+
case "eval.started":
|
|
1566
|
+
case "eval.completed":
|
|
1567
|
+
case "replay.started":
|
|
1568
|
+
case "replay.completed":
|
|
1569
|
+
return state;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function applyRunStarted(state, event) {
|
|
1573
|
+
return {
|
|
1574
|
+
...state,
|
|
1575
|
+
status: "running",
|
|
1576
|
+
input: event.input,
|
|
1577
|
+
startedAt: event.timestamp
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function applyRunCompleted(state, event) {
|
|
1581
|
+
return {
|
|
1582
|
+
...state,
|
|
1583
|
+
status: "completed",
|
|
1584
|
+
finalOutput: event.output,
|
|
1585
|
+
completedAt: event.timestamp,
|
|
1586
|
+
metrics: { ...state.metrics, totalLatencyMs: event.latencyMs }
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function applyRunFailed(state, event) {
|
|
1590
|
+
return {
|
|
1591
|
+
...state,
|
|
1592
|
+
status: "failed",
|
|
1593
|
+
completedAt: event.timestamp,
|
|
1594
|
+
errors: [...state.errors, mapEventError(event.error, event.timestamp)],
|
|
1595
|
+
metrics: { ...state.metrics, totalLatencyMs: event.latencyMs }
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/tracing/trace-bus.ts
|
|
1600
|
+
function buildSinkFailure(sinkName, operation, thrownError) {
|
|
1601
|
+
return { sinkName, operation, error: thrownError };
|
|
1602
|
+
}
|
|
1603
|
+
function reportSinkFailure(onSinkError, failure) {
|
|
1604
|
+
if (onSinkError !== void 0) {
|
|
1605
|
+
onSinkError(failure);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function writeTraceEventToSinkSafely(sink, event, onSinkError) {
|
|
1609
|
+
try {
|
|
1610
|
+
Promise.resolve(sink.writeTraceEvent(event)).catch(
|
|
1611
|
+
(thrownError) => {
|
|
1612
|
+
reportSinkFailure(
|
|
1613
|
+
onSinkError,
|
|
1614
|
+
buildSinkFailure(sink.name, "write", thrownError)
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
);
|
|
1618
|
+
} catch (thrownError) {
|
|
1619
|
+
reportSinkFailure(
|
|
1620
|
+
onSinkError,
|
|
1621
|
+
buildSinkFailure(
|
|
1622
|
+
sink.name,
|
|
1623
|
+
"write",
|
|
1624
|
+
thrownError instanceof Error ? thrownError : new Error(String(thrownError))
|
|
1625
|
+
)
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async function flushSinkSafely(sink, onSinkError) {
|
|
1630
|
+
if (sink.flush === void 0) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
try {
|
|
1634
|
+
await sink.flush();
|
|
1635
|
+
} catch (thrownError) {
|
|
1636
|
+
reportSinkFailure(
|
|
1637
|
+
onSinkError,
|
|
1638
|
+
buildSinkFailure(
|
|
1639
|
+
sink.name,
|
|
1640
|
+
"flush",
|
|
1641
|
+
thrownError instanceof Error ? thrownError : new Error(String(thrownError))
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function createTraceBus(busInput) {
|
|
1647
|
+
const now = busInput.now ?? (() => /* @__PURE__ */ new Date());
|
|
1648
|
+
const events = [];
|
|
1649
|
+
let runState = busInput.initialRunState ?? createInitialRunState({
|
|
1650
|
+
runId: busInput.runId,
|
|
1651
|
+
workflowName: busInput.workflowName,
|
|
1652
|
+
input: busInput.input,
|
|
1653
|
+
createdAt: now().toISOString()
|
|
1654
|
+
});
|
|
1655
|
+
function emitTraceEvent(event) {
|
|
1656
|
+
const validatedEvent = TraceEventSchema.parse(event);
|
|
1657
|
+
events.push(validatedEvent);
|
|
1658
|
+
runState = applyTraceEventToRunState(runState, validatedEvent);
|
|
1659
|
+
for (const sink of busInput.sinks) {
|
|
1660
|
+
writeTraceEventToSinkSafely(sink, validatedEvent, busInput.onSinkError);
|
|
1661
|
+
}
|
|
1662
|
+
return validatedEvent;
|
|
1663
|
+
}
|
|
1664
|
+
async function flushSinks() {
|
|
1665
|
+
for (const sink of busInput.sinks) {
|
|
1666
|
+
await flushSinkSafely(sink, busInput.onSinkError);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function closeSinks() {
|
|
1670
|
+
for (const sink of busInput.sinks) {
|
|
1671
|
+
if (sink.close !== void 0) {
|
|
1672
|
+
sink.close();
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
emitTraceEvent,
|
|
1678
|
+
getRunState: () => runState,
|
|
1679
|
+
listTraceEvents: () => [...events],
|
|
1680
|
+
flushSinks,
|
|
1681
|
+
closeSinks
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/workflows/workflow-approval.ts
|
|
1686
|
+
import { AppErrorCode as AppErrorCode9 } from "@assemble-dev/shared-types/errors";
|
|
1687
|
+
import { JsonValueSchema as JsonValueSchema2 } from "@assemble-dev/shared-types/json";
|
|
1688
|
+
import { AppError as AppError9 } from "@assemble-dev/shared-utils/errors";
|
|
1689
|
+
import { z as z3 } from "zod";
|
|
1690
|
+
|
|
1691
|
+
// src/workflows/run-state-store.ts
|
|
1692
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1693
|
+
import { join } from "path";
|
|
1694
|
+
import { RunStateSchema } from "@assemble-dev/shared-types/runs";
|
|
1695
|
+
var defaultRunStateDirectory = join(".assemble", "runs");
|
|
1696
|
+
function resolveRunStateFilePath(runId, directory = defaultRunStateDirectory) {
|
|
1697
|
+
return join(directory, `${runId}.json`);
|
|
1698
|
+
}
|
|
1699
|
+
function saveRunStateFile(input) {
|
|
1700
|
+
const directory = input.directory ?? defaultRunStateDirectory;
|
|
1701
|
+
const filePath = resolveRunStateFilePath(input.runState.runId, directory);
|
|
1702
|
+
const validatedRunState = RunStateSchema.parse(input.runState);
|
|
1703
|
+
mkdirSync(directory, { recursive: true });
|
|
1704
|
+
writeFileSync(filePath, JSON.stringify(validatedRunState, null, 2), "utf8");
|
|
1705
|
+
return filePath;
|
|
1706
|
+
}
|
|
1707
|
+
function loadRunStateFile(input) {
|
|
1708
|
+
const filePath = resolveRunStateFilePath(input.runId, input.directory);
|
|
1709
|
+
const rawContents = readFileSync(filePath, "utf8");
|
|
1710
|
+
return RunStateSchema.parse(JSON.parse(rawContents));
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/workflows/workflow-runtime-context.ts
|
|
1714
|
+
async function persistWorkflowRunState(context) {
|
|
1715
|
+
if (!context.persistRunState) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
saveRunStateFile({
|
|
1719
|
+
runState: await buildWorkflowRunState(context),
|
|
1720
|
+
directory: context.runStateDirectory
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
async function buildWorkflowRunState(context) {
|
|
1724
|
+
return {
|
|
1725
|
+
...context.traceBus.getRunState(),
|
|
1726
|
+
artifacts: await context.artifactStore.listRunArtifacts()
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function buildWorkflowEventContext(context) {
|
|
1730
|
+
return {
|
|
1731
|
+
traceBus: context.traceBus,
|
|
1732
|
+
runId: context.runId,
|
|
1733
|
+
workflowName: context.definition.name,
|
|
1734
|
+
now: context.now
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function calculateWorkflowElapsedMs(context) {
|
|
1738
|
+
return Math.max(0, context.now().getTime() - context.startedAtMs);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/workflows/workflow-step-execution.ts
|
|
1742
|
+
import { AppErrorCode as AppErrorCode7 } from "@assemble-dev/shared-types/errors";
|
|
1743
|
+
import { AppError as AppError7 } from "@assemble-dev/shared-utils/errors";
|
|
1744
|
+
|
|
1745
|
+
// src/workflows/workflow-trace-events.ts
|
|
1746
|
+
import { generateTraceEventId as generateTraceEventId4 } from "@assemble-dev/shared-utils/ids";
|
|
1747
|
+
function buildWorkflowEventBase(eventContext, parentEventId) {
|
|
1748
|
+
return {
|
|
1749
|
+
id: generateTraceEventId4(),
|
|
1750
|
+
runId: eventContext.runId,
|
|
1751
|
+
workflowName: eventContext.workflowName,
|
|
1752
|
+
timestamp: eventContext.now().toISOString(),
|
|
1753
|
+
parentEventId,
|
|
1754
|
+
redacted: false
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function emitRunStartedEvent(eventContext, input) {
|
|
1758
|
+
return eventContext.traceBus.emitTraceEvent({
|
|
1759
|
+
...buildWorkflowEventBase(eventContext, null),
|
|
1760
|
+
type: "run.started",
|
|
1761
|
+
input
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
function emitRunCompletedEvent(eventContext, runStartedEventId, output, latencyMs) {
|
|
1765
|
+
eventContext.traceBus.emitTraceEvent({
|
|
1766
|
+
...buildWorkflowEventBase(eventContext, runStartedEventId),
|
|
1767
|
+
type: "run.completed",
|
|
1768
|
+
output,
|
|
1769
|
+
latencyMs
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
function emitRouteSelectedEvent(eventContext, runStartedEventId, routeSelection) {
|
|
1773
|
+
eventContext.traceBus.emitTraceEvent({
|
|
1774
|
+
...buildWorkflowEventBase(eventContext, runStartedEventId),
|
|
1775
|
+
type: "route.selected",
|
|
1776
|
+
supervisorName: routeSelection.supervisorName,
|
|
1777
|
+
selectedAgentName: routeSelection.selectedAgentName,
|
|
1778
|
+
candidateAgentNames: routeSelection.candidateAgentNames,
|
|
1779
|
+
reason: routeSelection.reason
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
function emitApprovalApprovedEvent(eventContext, decision) {
|
|
1783
|
+
eventContext.traceBus.emitTraceEvent({
|
|
1784
|
+
...buildWorkflowEventBase(eventContext, decision.parentEventId),
|
|
1785
|
+
type: "approval.approved",
|
|
1786
|
+
approvalId: decision.approvalId,
|
|
1787
|
+
decidedBy: decision.decidedBy
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
function emitApprovalRejectedEvent(eventContext, decision, reason) {
|
|
1791
|
+
eventContext.traceBus.emitTraceEvent({
|
|
1792
|
+
...buildWorkflowEventBase(eventContext, decision.parentEventId),
|
|
1793
|
+
type: "approval.rejected",
|
|
1794
|
+
approvalId: decision.approvalId,
|
|
1795
|
+
decidedBy: decision.decidedBy,
|
|
1796
|
+
reason
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
function emitRunFailedEvent(eventContext, runStartedEventId, error, latencyMs) {
|
|
1800
|
+
eventContext.traceBus.emitTraceEvent({
|
|
1801
|
+
...buildWorkflowEventBase(eventContext, runStartedEventId),
|
|
1802
|
+
type: "run.failed",
|
|
1803
|
+
error,
|
|
1804
|
+
latencyMs
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/workflows/workflow-step-execution.ts
|
|
1809
|
+
async function executeWorkflowStep(context, stepIndex, stepAgent, stepInput) {
|
|
1810
|
+
try {
|
|
1811
|
+
const agentResult = await runAgent({
|
|
1812
|
+
definition: stepAgent,
|
|
1813
|
+
input: stepInput,
|
|
1814
|
+
traceBus: context.traceBus,
|
|
1815
|
+
parentEventId: context.runStartedEventId,
|
|
1816
|
+
threadMemory: context.threadMemory,
|
|
1817
|
+
now: context.now
|
|
1818
|
+
});
|
|
1819
|
+
await saveWorkflowStepArtifact(
|
|
1820
|
+
context,
|
|
1821
|
+
stepIndex,
|
|
1822
|
+
stepAgent.name,
|
|
1823
|
+
agentResult.output
|
|
1824
|
+
);
|
|
1825
|
+
return agentResult.output;
|
|
1826
|
+
} catch (thrownError) {
|
|
1827
|
+
const stepError = mapCaughtWorkflowErrorToAppError(
|
|
1828
|
+
thrownError instanceof Error ? thrownError : new Error(String(thrownError))
|
|
1829
|
+
);
|
|
1830
|
+
return await resolveWorkflowStepFailure(
|
|
1831
|
+
context,
|
|
1832
|
+
stepIndex,
|
|
1833
|
+
stepAgent.name,
|
|
1834
|
+
stepError,
|
|
1835
|
+
stepInput
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
async function resolveWorkflowStepFailure(context, stepIndex, agentName, stepError, previousStepOutput) {
|
|
1840
|
+
if (context.definition.continueOnAgentFailure) {
|
|
1841
|
+
return previousStepOutput;
|
|
1842
|
+
}
|
|
1843
|
+
const failureError = new AppError7({
|
|
1844
|
+
code: stepError.code,
|
|
1845
|
+
message: `step ${stepIndex} (${agentName}) failed: ${stepError.message}`,
|
|
1846
|
+
statusCode: stepError.statusCode,
|
|
1847
|
+
details: { stepIndex, agentName, reason: stepError.message },
|
|
1848
|
+
cause: stepError
|
|
1849
|
+
});
|
|
1850
|
+
await failWorkflowRun(context, failureError);
|
|
1851
|
+
throw failureError;
|
|
1852
|
+
}
|
|
1853
|
+
function shouldStopWorkflowRun(context, stepIndex) {
|
|
1854
|
+
if (context.definition.stopCondition === null) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
return context.definition.stopCondition({
|
|
1858
|
+
state: context.traceBus.getRunState(),
|
|
1859
|
+
stepIndex
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
async function saveWorkflowStepArtifact(context, stepIndex, agentName, stepOutput) {
|
|
1863
|
+
await context.artifactStore.saveRunArtifact({
|
|
1864
|
+
key: `step-${stepIndex}:${agentName}`,
|
|
1865
|
+
value: stepOutput,
|
|
1866
|
+
producedByAgentName: agentName,
|
|
1867
|
+
createdAt: context.now().toISOString()
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
async function failWorkflowRun(context, failureError) {
|
|
1871
|
+
emitRunFailedEvent(
|
|
1872
|
+
buildWorkflowEventContext(context),
|
|
1873
|
+
context.runStartedEventId,
|
|
1874
|
+
{ message: failureError.message, code: failureError.code },
|
|
1875
|
+
calculateWorkflowElapsedMs(context)
|
|
1876
|
+
);
|
|
1877
|
+
await persistWorkflowRunState(context);
|
|
1878
|
+
await context.traceBus.flushSinks();
|
|
1879
|
+
}
|
|
1880
|
+
function mapCaughtWorkflowErrorToAppError(thrownError) {
|
|
1881
|
+
if (thrownError instanceof AppError7) {
|
|
1882
|
+
return thrownError;
|
|
1883
|
+
}
|
|
1884
|
+
return new AppError7({
|
|
1885
|
+
code: AppErrorCode7.InternalServerError,
|
|
1886
|
+
message: thrownError.message,
|
|
1887
|
+
statusCode: 500,
|
|
1888
|
+
cause: thrownError
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// src/workflows/workflow-tool-execution.ts
|
|
1893
|
+
import { AppErrorCode as AppErrorCode8 } from "@assemble-dev/shared-types/errors";
|
|
1894
|
+
import { AppError as AppError8 } from "@assemble-dev/shared-utils/errors";
|
|
1895
|
+
async function executeWorkflowTool(toolInput) {
|
|
1896
|
+
const runtimeContext = toolInput.runtimeContext;
|
|
1897
|
+
const workflowTool = findWorkflowToolByName(
|
|
1898
|
+
runtimeContext.definition,
|
|
1899
|
+
toolInput.toolName
|
|
1900
|
+
);
|
|
1901
|
+
const workflowPolicies = runtimeContext.definition.policies;
|
|
1902
|
+
const outcome = await executeToolCall({
|
|
1903
|
+
tool: workflowTool,
|
|
1904
|
+
agentName: toolInput.agentName,
|
|
1905
|
+
input: toolInput.input,
|
|
1906
|
+
policies: workflowPolicies.length > 0 ? [...workflowPolicies] : void 0,
|
|
1907
|
+
traceBus: runtimeContext.traceBus,
|
|
1908
|
+
parentEventId: runtimeContext.parentEventId ?? void 0,
|
|
1909
|
+
now: runtimeContext.now,
|
|
1910
|
+
grantedApproval: toolInput.grantedApproval
|
|
1911
|
+
});
|
|
1912
|
+
if (toolInput.throwOnUnsuccessfulOutcome === false) {
|
|
1913
|
+
return outcome;
|
|
1914
|
+
}
|
|
1915
|
+
return ensureWorkflowToolOutcomeSucceeded(toolInput, outcome);
|
|
1916
|
+
}
|
|
1917
|
+
function findWorkflowToolByName(definition, toolName) {
|
|
1918
|
+
const workflowTool = definition.tools.find(
|
|
1919
|
+
(candidateTool) => candidateTool.name === toolName
|
|
1920
|
+
);
|
|
1921
|
+
if (workflowTool !== void 0) {
|
|
1922
|
+
return workflowTool;
|
|
1923
|
+
}
|
|
1924
|
+
const registeredToolNames = definition.tools.map((registeredTool) => registeredTool.name).join(", ");
|
|
1925
|
+
throw new AppError8({
|
|
1926
|
+
code: AppErrorCode8.NotFound,
|
|
1927
|
+
message: `tool "${toolName}" is not registered on workflow "${definition.name}"; registered tools: [${registeredToolNames}]. Add the tool to the workflow definition before calling it`,
|
|
1928
|
+
statusCode: 404,
|
|
1929
|
+
details: { toolName, workflowName: definition.name }
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
function ensureWorkflowToolOutcomeSucceeded(toolInput, outcome) {
|
|
1933
|
+
const stepIndex = toolInput.runtimeContext.stepIndex;
|
|
1934
|
+
if (outcome.status === "blocked") {
|
|
1935
|
+
throw new AppError8({
|
|
1936
|
+
code: AppErrorCode8.Forbidden,
|
|
1937
|
+
message: `tool "${toolInput.toolName}" was blocked at step ${stepIndex} for agent "${toolInput.agentName}": ${outcome.reason}`,
|
|
1938
|
+
statusCode: 403,
|
|
1939
|
+
details: { toolName: toolInput.toolName, stepIndex, reason: outcome.reason }
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
if (outcome.status === "failed") {
|
|
1943
|
+
throw new AppError8({
|
|
1944
|
+
code: mapToolCallErrorCodeToAppErrorCode(outcome.error.code),
|
|
1945
|
+
message: `tool "${toolInput.toolName}" failed at step ${stepIndex} for agent "${toolInput.agentName}": ${outcome.error.message}`,
|
|
1946
|
+
statusCode: outcome.error.code === "VALIDATION_FAILED" ? 422 : 500,
|
|
1947
|
+
details: {
|
|
1948
|
+
toolName: toolInput.toolName,
|
|
1949
|
+
stepIndex,
|
|
1950
|
+
reason: outcome.error.message
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
return outcome;
|
|
1955
|
+
}
|
|
1956
|
+
function mapToolCallErrorCodeToAppErrorCode(toolCallErrorCode) {
|
|
1957
|
+
return toolCallErrorCode === "VALIDATION_FAILED" ? AppErrorCode8.ValidationFailed : AppErrorCode8.InternalServerError;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/workflows/workflow-approval.ts
|
|
1961
|
+
var pendingApprovalArtifactKey = "workflow:pending-approval";
|
|
1962
|
+
var PendingWorkflowApprovalSchema = z3.object({
|
|
1963
|
+
stepIndex: z3.number().int().nonnegative(),
|
|
1964
|
+
agentName: z3.string().min(1),
|
|
1965
|
+
toolName: z3.string().min(1),
|
|
1966
|
+
toolInput: JsonValueSchema2,
|
|
1967
|
+
approvalId: z3.string().min(1),
|
|
1968
|
+
toolRequestEventId: z3.string().nullable(),
|
|
1969
|
+
agentOutput: JsonValueSchema2,
|
|
1970
|
+
runStartedEventId: z3.string().min(1)
|
|
1971
|
+
});
|
|
1972
|
+
function buildBoundToolArtifactKey(stepIndex, agentName, toolName) {
|
|
1973
|
+
return `step-${stepIndex}:${agentName}:tool:${toolName}`;
|
|
1974
|
+
}
|
|
1975
|
+
function buildWorkflowStepContext(context, stepIndex) {
|
|
1976
|
+
return {
|
|
1977
|
+
definition: context.definition,
|
|
1978
|
+
traceBus: context.traceBus,
|
|
1979
|
+
stepIndex,
|
|
1980
|
+
parentEventId: context.runStartedEventId,
|
|
1981
|
+
artifactStore: context.artifactStore,
|
|
1982
|
+
threadMemory: context.threadMemory,
|
|
1983
|
+
now: context.now
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
async function applyAgentToolBindingAfterStep(context, stepIndex, agentName, agentOutput) {
|
|
1987
|
+
const binding = findAgentToolBinding(context, agentName);
|
|
1988
|
+
if (binding === null) {
|
|
1989
|
+
return { status: "continue" };
|
|
1990
|
+
}
|
|
1991
|
+
const toolInput = binding.buildToolInput({
|
|
1992
|
+
state: await buildWorkflowRunState(context),
|
|
1993
|
+
stepIndex,
|
|
1994
|
+
agentOutput
|
|
1995
|
+
});
|
|
1996
|
+
const outcome = await executeBoundToolOrFailRun(
|
|
1997
|
+
context,
|
|
1998
|
+
stepIndex,
|
|
1999
|
+
binding,
|
|
2000
|
+
toolInput
|
|
2001
|
+
);
|
|
2002
|
+
if (outcome.status === "awaiting_approval") {
|
|
2003
|
+
return await pauseWorkflowRunForApproval(context, {
|
|
2004
|
+
stepIndex,
|
|
2005
|
+
agentName,
|
|
2006
|
+
toolName: binding.toolName,
|
|
2007
|
+
toolInput,
|
|
2008
|
+
approvalId: outcome.approvalId,
|
|
2009
|
+
toolRequestEventId: outcome.toolRequestEventId,
|
|
2010
|
+
agentOutput,
|
|
2011
|
+
runStartedEventId: context.runStartedEventId
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
if (outcome.status === "completed") {
|
|
2015
|
+
await saveBoundToolArtifact(
|
|
2016
|
+
context,
|
|
2017
|
+
stepIndex,
|
|
2018
|
+
agentName,
|
|
2019
|
+
binding.toolName,
|
|
2020
|
+
outcome.output
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
return { status: "continue" };
|
|
2024
|
+
}
|
|
2025
|
+
async function executeApprovedWorkflowTool(context, pendingApproval) {
|
|
2026
|
+
const outcome = await executeBoundToolOrFailRun(
|
|
2027
|
+
context,
|
|
2028
|
+
pendingApproval.stepIndex,
|
|
2029
|
+
{
|
|
2030
|
+
agentName: pendingApproval.agentName,
|
|
2031
|
+
toolName: pendingApproval.toolName,
|
|
2032
|
+
buildToolInput: () => pendingApproval.toolInput
|
|
2033
|
+
},
|
|
2034
|
+
pendingApproval.toolInput,
|
|
2035
|
+
{
|
|
2036
|
+
approvalId: pendingApproval.approvalId,
|
|
2037
|
+
toolRequestEventId: pendingApproval.toolRequestEventId
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
if (outcome.status === "completed") {
|
|
2041
|
+
await saveBoundToolArtifact(
|
|
2042
|
+
context,
|
|
2043
|
+
pendingApproval.stepIndex,
|
|
2044
|
+
pendingApproval.agentName,
|
|
2045
|
+
pendingApproval.toolName,
|
|
2046
|
+
outcome.output
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async function executeBoundToolOrFailRun(context, stepIndex, binding, toolInput, grantedApproval) {
|
|
2051
|
+
try {
|
|
2052
|
+
const outcome = await executeWorkflowTool({
|
|
2053
|
+
runtimeContext: buildWorkflowStepContext(context, stepIndex),
|
|
2054
|
+
toolName: binding.toolName,
|
|
2055
|
+
agentName: binding.agentName,
|
|
2056
|
+
input: toolInput,
|
|
2057
|
+
grantedApproval
|
|
2058
|
+
});
|
|
2059
|
+
if (outcome.status === "completed") {
|
|
2060
|
+
return { status: "completed", output: outcome.output };
|
|
2061
|
+
}
|
|
2062
|
+
if (outcome.status === "awaiting_approval") {
|
|
2063
|
+
return outcome;
|
|
2064
|
+
}
|
|
2065
|
+
throw new AppError9({
|
|
2066
|
+
code: AppErrorCode9.InternalServerError,
|
|
2067
|
+
message: `tool "${binding.toolName}" returned unexpected outcome "${outcome.status}" at step ${stepIndex} for agent "${binding.agentName}"`,
|
|
2068
|
+
statusCode: 500,
|
|
2069
|
+
details: { toolName: binding.toolName, stepIndex }
|
|
2070
|
+
});
|
|
2071
|
+
} catch (thrownError) {
|
|
2072
|
+
const bindingError = thrownError instanceof AppError9 ? thrownError : new AppError9({
|
|
2073
|
+
code: AppErrorCode9.InternalServerError,
|
|
2074
|
+
message: `tool "${binding.toolName}" threw at step ${stepIndex} for agent "${binding.agentName}": ${String(thrownError)}`,
|
|
2075
|
+
statusCode: 500,
|
|
2076
|
+
details: { toolName: binding.toolName, stepIndex }
|
|
2077
|
+
});
|
|
2078
|
+
await failWorkflowRun(context, bindingError);
|
|
2079
|
+
throw bindingError;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
async function pauseWorkflowRunForApproval(context, pendingApproval) {
|
|
2083
|
+
await context.artifactStore.saveRunArtifact({
|
|
2084
|
+
key: pendingApprovalArtifactKey,
|
|
2085
|
+
value: PendingWorkflowApprovalSchema.parse(pendingApproval),
|
|
2086
|
+
producedByAgentName: pendingApproval.agentName,
|
|
2087
|
+
createdAt: context.now().toISOString()
|
|
2088
|
+
});
|
|
2089
|
+
await persistWorkflowRunState(context);
|
|
2090
|
+
await context.traceBus.flushSinks();
|
|
2091
|
+
return { status: "paused", approvalId: pendingApproval.approvalId };
|
|
2092
|
+
}
|
|
2093
|
+
async function saveBoundToolArtifact(context, stepIndex, agentName, toolName, toolOutput) {
|
|
2094
|
+
await context.artifactStore.saveRunArtifact({
|
|
2095
|
+
key: buildBoundToolArtifactKey(stepIndex, agentName, toolName),
|
|
2096
|
+
value: toolOutput,
|
|
2097
|
+
producedByAgentName: agentName,
|
|
2098
|
+
createdAt: context.now().toISOString()
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
function findAgentToolBinding(context, agentName) {
|
|
2102
|
+
const binding = context.definition.agentToolBindings.find(
|
|
2103
|
+
(candidateBinding) => candidateBinding.agentName === agentName
|
|
2104
|
+
);
|
|
2105
|
+
return binding ?? null;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/workflows/workflow-parallel-execution.ts
|
|
2109
|
+
import { AppErrorCode as AppErrorCode10 } from "@assemble-dev/shared-types/errors";
|
|
2110
|
+
import { AppError as AppError10 } from "@assemble-dev/shared-utils/errors";
|
|
2111
|
+
|
|
2112
|
+
// src/workflows/workflow-topology.ts
|
|
2113
|
+
import { z as z4 } from "zod";
|
|
2114
|
+
var finishWorkflow = "__finish__";
|
|
2115
|
+
var defaultSupervisorMaxSteps = 10;
|
|
2116
|
+
var defaultParallelMergedArtifactKey = "parallel:merged";
|
|
2117
|
+
var PipelineTopologySchema = z4.object({
|
|
2118
|
+
kind: z4.literal("pipeline")
|
|
2119
|
+
});
|
|
2120
|
+
var SupervisorTopologySchema = z4.object({
|
|
2121
|
+
kind: z4.literal("supervisor"),
|
|
2122
|
+
supervisorName: z4.string().min(1),
|
|
2123
|
+
route: z4.custom(
|
|
2124
|
+
(value) => typeof value === "function"
|
|
2125
|
+
),
|
|
2126
|
+
maxSteps: z4.number().int().min(1).optional(),
|
|
2127
|
+
stopWhen: z4.custom((value) => typeof value === "function").optional()
|
|
2128
|
+
});
|
|
2129
|
+
var ParallelTopologySchema = z4.object({
|
|
2130
|
+
kind: z4.literal("parallel"),
|
|
2131
|
+
agentNames: z4.array(z4.string().min(1)).min(1).optional(),
|
|
2132
|
+
mergedArtifactKey: z4.string().min(1).optional()
|
|
2133
|
+
});
|
|
2134
|
+
var WorkflowTopologySchema = z4.discriminatedUnion("kind", [
|
|
2135
|
+
PipelineTopologySchema,
|
|
2136
|
+
SupervisorTopologySchema,
|
|
2137
|
+
ParallelTopologySchema
|
|
2138
|
+
]);
|
|
2139
|
+
var defaultWorkflowTopology = Object.freeze({
|
|
2140
|
+
kind: "pipeline"
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
// src/workflows/workflow-parallel-execution.ts
|
|
2144
|
+
function buildParallelThreadKey(runId, agentName) {
|
|
2145
|
+
return `${runId}:${agentName}`;
|
|
2146
|
+
}
|
|
2147
|
+
async function executeParallelSteps(context, topology, start) {
|
|
2148
|
+
const fanOutAgents = await resolveParallelFanOutAgents(context, topology);
|
|
2149
|
+
const branchContext = {
|
|
2150
|
+
...context,
|
|
2151
|
+
artifactStore: buildSerializedArtifactStore(context.artifactStore)
|
|
2152
|
+
};
|
|
2153
|
+
const branchPromises = fanOutAgents.map(
|
|
2154
|
+
async (fanOutAgent, fanOutIndex) => await executeParallelBranch(
|
|
2155
|
+
branchContext,
|
|
2156
|
+
fanOutIndex,
|
|
2157
|
+
fanOutAgent,
|
|
2158
|
+
start.input
|
|
2159
|
+
)
|
|
2160
|
+
);
|
|
2161
|
+
const branchResults = await collectSettledParallelBranchResults(
|
|
2162
|
+
branchPromises
|
|
2163
|
+
);
|
|
2164
|
+
await ensureAllParallelBranchesSucceeded(context, branchResults);
|
|
2165
|
+
const mergedOutput = mergeParallelBranchOutputs(branchResults);
|
|
2166
|
+
await saveMergedParallelArtifact(context, topology, mergedOutput);
|
|
2167
|
+
await persistWorkflowRunState(context);
|
|
2168
|
+
return { status: "completed", output: mergedOutput };
|
|
2169
|
+
}
|
|
2170
|
+
function buildSerializedArtifactStore(artifactStore) {
|
|
2171
|
+
let pendingWrite = Promise.resolve();
|
|
2172
|
+
return {
|
|
2173
|
+
async saveRunArtifact(artifact) {
|
|
2174
|
+
pendingWrite = pendingWrite.then(
|
|
2175
|
+
async () => await artifactStore.saveRunArtifact(artifact)
|
|
2176
|
+
);
|
|
2177
|
+
return await pendingWrite;
|
|
2178
|
+
},
|
|
2179
|
+
async getRunArtifact(key) {
|
|
2180
|
+
return await artifactStore.getRunArtifact(key);
|
|
2181
|
+
},
|
|
2182
|
+
async listRunArtifacts() {
|
|
2183
|
+
return await artifactStore.listRunArtifacts();
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
async function collectSettledParallelBranchResults(branchPromises) {
|
|
2188
|
+
const branchResults = [];
|
|
2189
|
+
for (const branchPromise of branchPromises) {
|
|
2190
|
+
branchResults.push(await branchPromise);
|
|
2191
|
+
}
|
|
2192
|
+
return branchResults;
|
|
2193
|
+
}
|
|
2194
|
+
async function resolveParallelFanOutAgents(context, topology) {
|
|
2195
|
+
if (topology.agentNames === void 0) {
|
|
2196
|
+
return [...context.definition.agents];
|
|
2197
|
+
}
|
|
2198
|
+
const fanOutAgents = [];
|
|
2199
|
+
for (const agentName of topology.agentNames) {
|
|
2200
|
+
const fanOutAgent = context.definition.agents.find(
|
|
2201
|
+
(agentDefinition) => agentDefinition.name === agentName
|
|
2202
|
+
);
|
|
2203
|
+
if (fanOutAgent === void 0) {
|
|
2204
|
+
return await failParallelRunForUnknownAgent(context, agentName);
|
|
2205
|
+
}
|
|
2206
|
+
fanOutAgents.push(fanOutAgent);
|
|
2207
|
+
}
|
|
2208
|
+
return fanOutAgents;
|
|
2209
|
+
}
|
|
2210
|
+
async function failParallelRunForUnknownAgent(context, agentName) {
|
|
2211
|
+
const validAgentNames = context.definition.agents.map((agentDefinition) => agentDefinition.name).join(", ");
|
|
2212
|
+
const unknownAgentError = new AppError10({
|
|
2213
|
+
code: AppErrorCode10.BadRequest,
|
|
2214
|
+
message: `workflow "${context.definition.name}" parallel topology selected unknown agent "${agentName}"; valid agent names: ${validAgentNames}`,
|
|
2215
|
+
statusCode: 400,
|
|
2216
|
+
details: {
|
|
2217
|
+
workflowName: context.definition.name,
|
|
2218
|
+
agentName,
|
|
2219
|
+
validAgentNames
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
await failWorkflowRun(context, unknownAgentError);
|
|
2223
|
+
throw unknownAgentError;
|
|
2224
|
+
}
|
|
2225
|
+
async function executeParallelBranch(context, fanOutIndex, fanOutAgent, workflowInput) {
|
|
2226
|
+
try {
|
|
2227
|
+
const agentResult = await runAgent({
|
|
2228
|
+
definition: fanOutAgent,
|
|
2229
|
+
input: workflowInput,
|
|
2230
|
+
traceBus: context.traceBus,
|
|
2231
|
+
parentEventId: context.runStartedEventId,
|
|
2232
|
+
threadMemory: createThreadMemory({
|
|
2233
|
+
provider: context.memoryProvider,
|
|
2234
|
+
threadKey: buildParallelThreadKey(context.runId, fanOutAgent.name)
|
|
2235
|
+
}),
|
|
2236
|
+
now: context.now
|
|
2237
|
+
});
|
|
2238
|
+
await saveParallelBranchArtifact(
|
|
2239
|
+
context,
|
|
2240
|
+
fanOutIndex,
|
|
2241
|
+
fanOutAgent.name,
|
|
2242
|
+
agentResult.output
|
|
2243
|
+
);
|
|
2244
|
+
await applyParallelAgentToolBinding(
|
|
2245
|
+
context,
|
|
2246
|
+
fanOutIndex,
|
|
2247
|
+
fanOutAgent.name,
|
|
2248
|
+
agentResult.output
|
|
2249
|
+
);
|
|
2250
|
+
return {
|
|
2251
|
+
status: "completed",
|
|
2252
|
+
agentName: fanOutAgent.name,
|
|
2253
|
+
output: agentResult.output
|
|
2254
|
+
};
|
|
2255
|
+
} catch (thrownError) {
|
|
2256
|
+
const branchError = thrownError instanceof Error ? thrownError : new Error(String(thrownError));
|
|
2257
|
+
return {
|
|
2258
|
+
status: "failed",
|
|
2259
|
+
agentName: fanOutAgent.name,
|
|
2260
|
+
error: mapParallelBranchErrorToAppError(branchError)
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
async function applyParallelAgentToolBinding(context, fanOutIndex, agentName, agentOutput) {
|
|
2265
|
+
const bindingOutcome = await applyAgentToolBindingAfterStep(
|
|
2266
|
+
context,
|
|
2267
|
+
fanOutIndex,
|
|
2268
|
+
agentName,
|
|
2269
|
+
agentOutput
|
|
2270
|
+
);
|
|
2271
|
+
if (bindingOutcome.status !== "paused") {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
throw new AppError10({
|
|
2275
|
+
code: AppErrorCode10.Conflict,
|
|
2276
|
+
message: `workflow "${context.definition.name}" agent "${agentName}" has a tool binding that requires approval, which is not supported inside parallel fan-out; run this agent in a pipeline or supervisor topology, or remove the approval requirement from the bound tool`,
|
|
2277
|
+
statusCode: 409,
|
|
2278
|
+
details: {
|
|
2279
|
+
workflowName: context.definition.name,
|
|
2280
|
+
agentName,
|
|
2281
|
+
fanOutIndex,
|
|
2282
|
+
approvalId: bindingOutcome.approvalId
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
async function ensureAllParallelBranchesSucceeded(context, branchResults) {
|
|
2287
|
+
const failures = branchResults.filter(
|
|
2288
|
+
(branchResult) => branchResult.status === "failed"
|
|
2289
|
+
);
|
|
2290
|
+
const firstFailure = failures[0];
|
|
2291
|
+
if (firstFailure === void 0) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
const failedAgentNames = failures.map((failure) => failure.agentName).join(", ");
|
|
2295
|
+
const failureReasons = failures.map((failure) => `${failure.agentName}: ${failure.error.message}`).join("; ");
|
|
2296
|
+
const parallelError = new AppError10({
|
|
2297
|
+
code: firstFailure.error.code,
|
|
2298
|
+
message: `workflow "${context.definition.name}" parallel fan-out failed for agent(s) [${failedAgentNames}]: ${failureReasons}`,
|
|
2299
|
+
statusCode: firstFailure.error.statusCode,
|
|
2300
|
+
details: {
|
|
2301
|
+
workflowName: context.definition.name,
|
|
2302
|
+
failedAgentNames,
|
|
2303
|
+
failedAgentCount: failures.length
|
|
2304
|
+
},
|
|
2305
|
+
cause: firstFailure.error
|
|
2306
|
+
});
|
|
2307
|
+
await failWorkflowRun(context, parallelError);
|
|
2308
|
+
throw parallelError;
|
|
2309
|
+
}
|
|
2310
|
+
function mergeParallelBranchOutputs(branchResults) {
|
|
2311
|
+
const mergedOutput = {};
|
|
2312
|
+
for (const branchResult of branchResults) {
|
|
2313
|
+
if (branchResult.status === "completed") {
|
|
2314
|
+
mergedOutput[branchResult.agentName] = branchResult.output;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return mergedOutput;
|
|
2318
|
+
}
|
|
2319
|
+
async function saveParallelBranchArtifact(context, fanOutIndex, agentName, branchOutput) {
|
|
2320
|
+
await context.artifactStore.saveRunArtifact({
|
|
2321
|
+
key: `step-${fanOutIndex}:${agentName}`,
|
|
2322
|
+
value: branchOutput,
|
|
2323
|
+
producedByAgentName: agentName,
|
|
2324
|
+
createdAt: context.now().toISOString()
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
async function saveMergedParallelArtifact(context, topology, mergedOutput) {
|
|
2328
|
+
await context.artifactStore.saveRunArtifact({
|
|
2329
|
+
key: topology.mergedArtifactKey ?? defaultParallelMergedArtifactKey,
|
|
2330
|
+
value: mergedOutput,
|
|
2331
|
+
createdAt: context.now().toISOString()
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
function mapParallelBranchErrorToAppError(branchError) {
|
|
2335
|
+
if (branchError instanceof AppError10) {
|
|
2336
|
+
return branchError;
|
|
2337
|
+
}
|
|
2338
|
+
return new AppError10({
|
|
2339
|
+
code: AppErrorCode10.InternalServerError,
|
|
2340
|
+
message: branchError.message,
|
|
2341
|
+
statusCode: 500,
|
|
2342
|
+
cause: branchError
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/workflows/workflow-supervisor-execution.ts
|
|
2347
|
+
import { AppErrorCode as AppErrorCode11 } from "@assemble-dev/shared-types/errors";
|
|
2348
|
+
import { AppError as AppError11 } from "@assemble-dev/shared-utils/errors";
|
|
2349
|
+
async function executeSupervisorSteps(context, topology, start) {
|
|
2350
|
+
let loopState = {
|
|
2351
|
+
stepIndex: start.startStepIndex,
|
|
2352
|
+
lastAgentName: start.lastAgentName,
|
|
2353
|
+
lastOutput: start.lastAgentName === null ? null : start.input,
|
|
2354
|
+
currentOutput: start.input
|
|
2355
|
+
};
|
|
2356
|
+
for (; ; ) {
|
|
2357
|
+
const routeContext = buildWorkflowRouteContext(context, loopState);
|
|
2358
|
+
if (topology.stopWhen !== void 0 && topology.stopWhen(routeContext)) {
|
|
2359
|
+
return { status: "completed", output: loopState.currentOutput };
|
|
2360
|
+
}
|
|
2361
|
+
const routeDecision = await resolveSupervisorRouteDecision(
|
|
2362
|
+
context,
|
|
2363
|
+
topology,
|
|
2364
|
+
routeContext
|
|
2365
|
+
);
|
|
2366
|
+
if (routeDecision.agentName === finishWorkflow) {
|
|
2367
|
+
return { status: "completed", output: loopState.currentOutput };
|
|
2368
|
+
}
|
|
2369
|
+
const stepOutput = await executeSupervisorStep(
|
|
2370
|
+
context,
|
|
2371
|
+
topology,
|
|
2372
|
+
loopState,
|
|
2373
|
+
routeDecision
|
|
2374
|
+
);
|
|
2375
|
+
const bindingOutcome = await applyAgentToolBindingAfterStep(
|
|
2376
|
+
context,
|
|
2377
|
+
loopState.stepIndex,
|
|
2378
|
+
routeDecision.agentName,
|
|
2379
|
+
stepOutput
|
|
2380
|
+
);
|
|
2381
|
+
if (bindingOutcome.status === "paused") {
|
|
2382
|
+
return bindingOutcome;
|
|
2383
|
+
}
|
|
2384
|
+
if (shouldStopWorkflowRun(context, loopState.stepIndex)) {
|
|
2385
|
+
return { status: "completed", output: stepOutput };
|
|
2386
|
+
}
|
|
2387
|
+
loopState = {
|
|
2388
|
+
stepIndex: loopState.stepIndex + 1,
|
|
2389
|
+
lastAgentName: routeDecision.agentName,
|
|
2390
|
+
lastOutput: stepOutput,
|
|
2391
|
+
currentOutput: stepOutput
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async function executeSupervisorStep(context, topology, loopState, routeDecision) {
|
|
2396
|
+
const selectedAgent = await resolveRoutedWorkflowAgent(
|
|
2397
|
+
context,
|
|
2398
|
+
topology,
|
|
2399
|
+
routeDecision,
|
|
2400
|
+
loopState.stepIndex
|
|
2401
|
+
);
|
|
2402
|
+
await ensureSupervisorStepBudget(context, topology, loopState.stepIndex);
|
|
2403
|
+
emitRouteSelectedEvent(
|
|
2404
|
+
buildWorkflowEventContext(context),
|
|
2405
|
+
context.runStartedEventId,
|
|
2406
|
+
{
|
|
2407
|
+
supervisorName: topology.supervisorName,
|
|
2408
|
+
selectedAgentName: selectedAgent.name,
|
|
2409
|
+
candidateAgentNames: listWorkflowAgentNames(context),
|
|
2410
|
+
reason: routeDecision.reason
|
|
2411
|
+
}
|
|
2412
|
+
);
|
|
2413
|
+
const stepOutput = await executeWorkflowStep(
|
|
2414
|
+
context,
|
|
2415
|
+
loopState.stepIndex,
|
|
2416
|
+
selectedAgent,
|
|
2417
|
+
loopState.currentOutput
|
|
2418
|
+
);
|
|
2419
|
+
await persistWorkflowRunState(context);
|
|
2420
|
+
return stepOutput;
|
|
2421
|
+
}
|
|
2422
|
+
async function resolveSupervisorRouteDecision(context, topology, routeContext) {
|
|
2423
|
+
try {
|
|
2424
|
+
const routeTarget = await topology.route(routeContext);
|
|
2425
|
+
return normalizeWorkflowRouteTarget(routeTarget);
|
|
2426
|
+
} catch (thrownError) {
|
|
2427
|
+
const causeError = thrownError instanceof Error ? thrownError : new Error(String(thrownError));
|
|
2428
|
+
return await failSupervisorRun(
|
|
2429
|
+
context,
|
|
2430
|
+
new AppError11({
|
|
2431
|
+
code: AppErrorCode11.InternalServerError,
|
|
2432
|
+
message: `workflow "${context.definition.name}" supervisor "${topology.supervisorName}" route function threw at step ${routeContext.stepIndex}: ${causeError.message}; fix the route function to return an agent name or finishWorkflow`,
|
|
2433
|
+
statusCode: 500,
|
|
2434
|
+
details: {
|
|
2435
|
+
workflowName: context.definition.name,
|
|
2436
|
+
supervisorName: topology.supervisorName,
|
|
2437
|
+
stepIndex: routeContext.stepIndex,
|
|
2438
|
+
reason: causeError.message
|
|
2439
|
+
},
|
|
2440
|
+
cause: causeError
|
|
2441
|
+
})
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function resolveRoutedWorkflowAgent(context, topology, routeDecision, stepIndex) {
|
|
2446
|
+
const selectedAgent = context.definition.agents.find(
|
|
2447
|
+
(agentDefinition) => agentDefinition.name === routeDecision.agentName
|
|
2448
|
+
);
|
|
2449
|
+
if (selectedAgent !== void 0) {
|
|
2450
|
+
return selectedAgent;
|
|
2451
|
+
}
|
|
2452
|
+
const validAgentNames = listWorkflowAgentNames(context).join(", ");
|
|
2453
|
+
return await failSupervisorRun(
|
|
2454
|
+
context,
|
|
2455
|
+
new AppError11({
|
|
2456
|
+
code: AppErrorCode11.BadRequest,
|
|
2457
|
+
message: `workflow "${context.definition.name}" supervisor "${topology.supervisorName}" routed to unknown agent "${routeDecision.agentName}" at step ${stepIndex}; valid agent names: ${validAgentNames}`,
|
|
2458
|
+
statusCode: 400,
|
|
2459
|
+
details: {
|
|
2460
|
+
workflowName: context.definition.name,
|
|
2461
|
+
supervisorName: topology.supervisorName,
|
|
2462
|
+
selectedAgentName: routeDecision.agentName,
|
|
2463
|
+
validAgentNames,
|
|
2464
|
+
stepIndex
|
|
2465
|
+
}
|
|
2466
|
+
})
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
async function ensureSupervisorStepBudget(context, topology, stepIndex) {
|
|
2470
|
+
const maxSteps = topology.maxSteps ?? defaultSupervisorMaxSteps;
|
|
2471
|
+
if (stepIndex < maxSteps) {
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
await failSupervisorRun(
|
|
2475
|
+
context,
|
|
2476
|
+
new AppError11({
|
|
2477
|
+
code: AppErrorCode11.Conflict,
|
|
2478
|
+
message: `workflow "${context.definition.name}" supervisor "${topology.supervisorName}" exceeded ${maxSteps} steps; return finishWorkflow from the route function, add a stopWhen condition, or increase maxSteps`,
|
|
2479
|
+
statusCode: 409,
|
|
2480
|
+
details: {
|
|
2481
|
+
workflowName: context.definition.name,
|
|
2482
|
+
supervisorName: topology.supervisorName,
|
|
2483
|
+
maxSteps
|
|
2484
|
+
}
|
|
2485
|
+
})
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
async function failSupervisorRun(context, supervisorError) {
|
|
2489
|
+
await failWorkflowRun(context, supervisorError);
|
|
2490
|
+
throw supervisorError;
|
|
2491
|
+
}
|
|
2492
|
+
function normalizeWorkflowRouteTarget(routeTarget) {
|
|
2493
|
+
if (typeof routeTarget === "string") {
|
|
2494
|
+
return {
|
|
2495
|
+
agentName: routeTarget,
|
|
2496
|
+
reason: `route function selected ${routeTarget}`
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
return routeTarget;
|
|
2500
|
+
}
|
|
2501
|
+
function listWorkflowAgentNames(context) {
|
|
2502
|
+
return context.definition.agents.map(
|
|
2503
|
+
(agentDefinition) => agentDefinition.name
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
function buildWorkflowRouteContext(context, loopState) {
|
|
2507
|
+
return {
|
|
2508
|
+
state: context.traceBus.getRunState(),
|
|
2509
|
+
stepIndex: loopState.stepIndex,
|
|
2510
|
+
lastAgentName: loopState.lastAgentName,
|
|
2511
|
+
lastOutput: loopState.lastOutput
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/workflows/workflow-runtime.ts
|
|
2516
|
+
async function runWorkflow(runInput) {
|
|
2517
|
+
const validatedInput = validateWorkflowInput(
|
|
2518
|
+
runInput.definition,
|
|
2519
|
+
runInput.input
|
|
2520
|
+
);
|
|
2521
|
+
const context = createWorkflowRuntimeContext(
|
|
2522
|
+
runInput.definition,
|
|
2523
|
+
validatedInput,
|
|
2524
|
+
runInput.options
|
|
2525
|
+
);
|
|
2526
|
+
await persistWorkflowRunState(context);
|
|
2527
|
+
return await continueWorkflowRunFromStep(context, {
|
|
2528
|
+
input: validatedInput,
|
|
2529
|
+
startStepIndex: 0,
|
|
2530
|
+
lastAgentName: null
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
async function continueWorkflowRunFromStep(context, start) {
|
|
2534
|
+
const outcome = await executeWorkflowTopologySteps(context, start);
|
|
2535
|
+
if (outcome.status === "paused") {
|
|
2536
|
+
return await buildAwaitingApprovalRunResult(context, outcome.approvalId);
|
|
2537
|
+
}
|
|
2538
|
+
return await completeWorkflowRun(context, outcome.output);
|
|
2539
|
+
}
|
|
2540
|
+
async function executeWorkflowTopologySteps(context, start) {
|
|
2541
|
+
const topology = context.definition.topology;
|
|
2542
|
+
if (topology.kind === "supervisor") {
|
|
2543
|
+
return await executeSupervisorSteps(context, topology, start);
|
|
2544
|
+
}
|
|
2545
|
+
if (topology.kind === "parallel") {
|
|
2546
|
+
return await executeParallelSteps(context, topology, start);
|
|
2547
|
+
}
|
|
2548
|
+
return await executePipelineSteps(context, start);
|
|
2549
|
+
}
|
|
2550
|
+
function validateWorkflowInput(definition, input) {
|
|
2551
|
+
const parsedInput = definition.inputSchema.safeParse(input);
|
|
2552
|
+
if (parsedInput.success) {
|
|
2553
|
+
return parsedInput.data;
|
|
2554
|
+
}
|
|
2555
|
+
throw new AppError12({
|
|
2556
|
+
code: AppErrorCode12.ValidationFailed,
|
|
2557
|
+
message: `workflow "${definition.name}" input failed schema validation: ${parsedInput.error.message}`,
|
|
2558
|
+
statusCode: 422
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
function createWorkflowRuntimeContext(definition, validatedInput, options) {
|
|
2562
|
+
const now = options?.now ?? (() => /* @__PURE__ */ new Date());
|
|
2563
|
+
const runId = options?.runId ?? generateRunId();
|
|
2564
|
+
const startedAtMs = now().getTime();
|
|
2565
|
+
const traceBus = createTraceBus({
|
|
2566
|
+
runId,
|
|
2567
|
+
workflowName: definition.name,
|
|
2568
|
+
input: validatedInput,
|
|
2569
|
+
sinks: options?.sinks ?? [],
|
|
2570
|
+
now
|
|
2571
|
+
});
|
|
2572
|
+
const memoryConfig = resolveWorkflowMemoryConfig(definition.memory ?? void 0);
|
|
2573
|
+
const runStartedEvent = emitRunStartedEvent(
|
|
2574
|
+
{ traceBus, runId, workflowName: definition.name, now },
|
|
2575
|
+
validatedInput
|
|
2576
|
+
);
|
|
2577
|
+
return {
|
|
2578
|
+
definition,
|
|
2579
|
+
traceBus,
|
|
2580
|
+
runId,
|
|
2581
|
+
runStartedEventId: runStartedEvent.id,
|
|
2582
|
+
threadMemory: createThreadMemory({
|
|
2583
|
+
provider: memoryConfig.provider,
|
|
2584
|
+
threadKey: runId
|
|
2585
|
+
}),
|
|
2586
|
+
memoryProvider: memoryConfig.provider,
|
|
2587
|
+
artifactStore: createArtifactStore({
|
|
2588
|
+
provider: memoryConfig.provider,
|
|
2589
|
+
runId
|
|
2590
|
+
}),
|
|
2591
|
+
now,
|
|
2592
|
+
startedAtMs,
|
|
2593
|
+
persistRunState: options?.persistRunState ?? true,
|
|
2594
|
+
runStateDirectory: options?.runStateDirectory ?? defaultRunStateDirectory
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
async function executePipelineSteps(context, start) {
|
|
2598
|
+
const stepAgents = context.definition.agents;
|
|
2599
|
+
let currentStepInput = start.input;
|
|
2600
|
+
for (let stepIndex = start.startStepIndex; stepIndex < stepAgents.length; stepIndex += 1) {
|
|
2601
|
+
const stepAgent = stepAgents[stepIndex];
|
|
2602
|
+
if (stepAgent === void 0) {
|
|
2603
|
+
break;
|
|
2604
|
+
}
|
|
2605
|
+
currentStepInput = await executeWorkflowStep(
|
|
2606
|
+
context,
|
|
2607
|
+
stepIndex,
|
|
2608
|
+
stepAgent,
|
|
2609
|
+
currentStepInput
|
|
2610
|
+
);
|
|
2611
|
+
await persistWorkflowRunState(context);
|
|
2612
|
+
const bindingOutcome = await applyAgentToolBindingAfterStep(
|
|
2613
|
+
context,
|
|
2614
|
+
stepIndex,
|
|
2615
|
+
stepAgent.name,
|
|
2616
|
+
currentStepInput
|
|
2617
|
+
);
|
|
2618
|
+
if (bindingOutcome.status === "paused") {
|
|
2619
|
+
return bindingOutcome;
|
|
2620
|
+
}
|
|
2621
|
+
if (stepIndex < stepAgents.length - 1 && shouldStopWorkflowRun(context, stepIndex)) {
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return { status: "completed", output: currentStepInput };
|
|
2626
|
+
}
|
|
2627
|
+
async function buildAwaitingApprovalRunResult(context, approvalId) {
|
|
2628
|
+
return {
|
|
2629
|
+
status: "awaiting_approval",
|
|
2630
|
+
runId: context.runId,
|
|
2631
|
+
approvalId,
|
|
2632
|
+
state: await buildWorkflowRunState(context),
|
|
2633
|
+
events: context.traceBus.listTraceEvents()
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
async function completeWorkflowRun(context, lastStepOutput) {
|
|
2637
|
+
const finalOutput = await validateWorkflowOutput(context, lastStepOutput);
|
|
2638
|
+
emitRunCompletedEvent(
|
|
2639
|
+
buildWorkflowEventContext(context),
|
|
2640
|
+
context.runStartedEventId,
|
|
2641
|
+
finalOutput,
|
|
2642
|
+
calculateWorkflowElapsedMs(context)
|
|
2643
|
+
);
|
|
2644
|
+
await persistWorkflowRunState(context);
|
|
2645
|
+
await context.traceBus.flushSinks();
|
|
2646
|
+
return {
|
|
2647
|
+
status: "completed",
|
|
2648
|
+
runId: context.runId,
|
|
2649
|
+
output: finalOutput,
|
|
2650
|
+
state: await buildWorkflowRunState(context),
|
|
2651
|
+
events: context.traceBus.listTraceEvents()
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
async function validateWorkflowOutput(context, lastStepOutput) {
|
|
2655
|
+
if (context.definition.outputSchema === null) {
|
|
2656
|
+
return lastStepOutput;
|
|
2657
|
+
}
|
|
2658
|
+
const parsedOutput = context.definition.outputSchema.safeParse(lastStepOutput);
|
|
2659
|
+
if (parsedOutput.success) {
|
|
2660
|
+
return parsedOutput.data;
|
|
2661
|
+
}
|
|
2662
|
+
const validationError = new AppError12({
|
|
2663
|
+
code: AppErrorCode12.ValidationFailed,
|
|
2664
|
+
message: `workflow "${context.definition.name}" output failed schema validation: ${parsedOutput.error.message}`,
|
|
2665
|
+
statusCode: 422
|
|
2666
|
+
});
|
|
2667
|
+
await failWorkflowRun(context, validationError);
|
|
2668
|
+
throw validationError;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/workflows/workflow-definition.ts
|
|
2672
|
+
function workflow(config) {
|
|
2673
|
+
const definition = Object.freeze({
|
|
2674
|
+
name: config.name,
|
|
2675
|
+
inputSchema: config.inputSchema,
|
|
2676
|
+
outputSchema: config.outputSchema ?? null,
|
|
2677
|
+
agents: Object.freeze([...config.agents]),
|
|
2678
|
+
tools: Object.freeze([...config.tools ?? []]),
|
|
2679
|
+
agentToolBindings: Object.freeze([...config.agentToolBindings ?? []]),
|
|
2680
|
+
topology: config.topology ?? defaultWorkflowTopology,
|
|
2681
|
+
policies: Object.freeze([...config.policies ?? []]),
|
|
2682
|
+
memory: config.memory ?? null,
|
|
2683
|
+
stopCondition: config.stopCondition ?? null,
|
|
2684
|
+
continueOnAgentFailure: config.continueOnAgentFailure ?? false,
|
|
2685
|
+
run: async (input, options) => {
|
|
2686
|
+
return await runWorkflow({ definition, input, options });
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
return definition;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// src/workflows/resume-run.ts
|
|
2693
|
+
import { existsSync } from "fs";
|
|
2694
|
+
import { AppErrorCode as AppErrorCode13 } from "@assemble-dev/shared-types/errors";
|
|
2695
|
+
import { AppError as AppError13 } from "@assemble-dev/shared-utils/errors";
|
|
2696
|
+
async function resumeRun(resumeInput) {
|
|
2697
|
+
const directory = resumeInput.options?.runStateDirectory ?? defaultRunStateDirectory;
|
|
2698
|
+
const persistedState = loadPersistedRunStateForResume(
|
|
2699
|
+
resumeInput.runId,
|
|
2700
|
+
directory
|
|
2701
|
+
);
|
|
2702
|
+
const pendingApproval = findPendingWorkflowApproval(persistedState);
|
|
2703
|
+
const context = await createResumedWorkflowRuntimeContext(
|
|
2704
|
+
resumeInput.definition,
|
|
2705
|
+
persistedState,
|
|
2706
|
+
pendingApproval,
|
|
2707
|
+
resumeInput.options
|
|
2708
|
+
);
|
|
2709
|
+
if (!resumeInput.decision.approved) {
|
|
2710
|
+
return await rejectPendingWorkflowApproval(
|
|
2711
|
+
context,
|
|
2712
|
+
pendingApproval,
|
|
2713
|
+
resumeInput.decision
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
return await approvePendingWorkflowApproval(
|
|
2717
|
+
context,
|
|
2718
|
+
pendingApproval,
|
|
2719
|
+
resumeInput.decision
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
function loadPersistedRunStateForResume(runId, directory) {
|
|
2723
|
+
const filePath = resolveRunStateFilePath(runId, directory);
|
|
2724
|
+
if (!existsSync(filePath)) {
|
|
2725
|
+
throw new AppError13({
|
|
2726
|
+
code: AppErrorCode13.NotFound,
|
|
2727
|
+
message: `run state file not found at "${filePath}" for run "${runId}"; the run was never persisted or the runStateDirectory is wrong`,
|
|
2728
|
+
statusCode: 404,
|
|
2729
|
+
details: { runId, filePath }
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
const persistedState = loadRunStateFile({ runId, directory });
|
|
2733
|
+
if (persistedState.status !== "awaiting_approval") {
|
|
2734
|
+
throw new AppError13({
|
|
2735
|
+
code: AppErrorCode13.Conflict,
|
|
2736
|
+
message: `run "${runId}" has status "${persistedState.status}" and cannot be resumed; only runs with status "awaiting_approval" can be resumed`,
|
|
2737
|
+
statusCode: 409,
|
|
2738
|
+
details: { runId, status: persistedState.status }
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
return persistedState;
|
|
2742
|
+
}
|
|
2743
|
+
function findPendingWorkflowApproval(persistedState) {
|
|
2744
|
+
const pendingArtifact = persistedState.artifacts.find(
|
|
2745
|
+
(artifact) => artifact.key === pendingApprovalArtifactKey
|
|
2746
|
+
);
|
|
2747
|
+
if (pendingArtifact === void 0) {
|
|
2748
|
+
throw new AppError13({
|
|
2749
|
+
code: AppErrorCode13.Conflict,
|
|
2750
|
+
message: `run "${persistedState.runId}" is awaiting approval but has no "${pendingApprovalArtifactKey}" artifact; the persisted state is incomplete and the run cannot be resumed`,
|
|
2751
|
+
statusCode: 409,
|
|
2752
|
+
details: { runId: persistedState.runId }
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return PendingWorkflowApprovalSchema.parse(pendingArtifact.value);
|
|
2756
|
+
}
|
|
2757
|
+
async function createResumedWorkflowRuntimeContext(definition, persistedState, pendingApproval, options) {
|
|
2758
|
+
const now = options?.now ?? (() => /* @__PURE__ */ new Date());
|
|
2759
|
+
const traceBus = createTraceBus({
|
|
2760
|
+
runId: persistedState.runId,
|
|
2761
|
+
workflowName: definition.name,
|
|
2762
|
+
input: persistedState.input,
|
|
2763
|
+
sinks: options?.sinks ?? [],
|
|
2764
|
+
now,
|
|
2765
|
+
initialRunState: persistedState
|
|
2766
|
+
});
|
|
2767
|
+
const memoryConfig = resolveWorkflowMemoryConfig(definition.memory ?? void 0);
|
|
2768
|
+
const artifactStore = createArtifactStore({
|
|
2769
|
+
provider: memoryConfig.provider,
|
|
2770
|
+
runId: persistedState.runId
|
|
2771
|
+
});
|
|
2772
|
+
for (const artifact of persistedState.artifacts) {
|
|
2773
|
+
await artifactStore.saveRunArtifact(artifact);
|
|
2774
|
+
}
|
|
2775
|
+
return {
|
|
2776
|
+
definition,
|
|
2777
|
+
traceBus,
|
|
2778
|
+
runId: persistedState.runId,
|
|
2779
|
+
runStartedEventId: pendingApproval.runStartedEventId,
|
|
2780
|
+
threadMemory: createThreadMemory({
|
|
2781
|
+
provider: memoryConfig.provider,
|
|
2782
|
+
threadKey: persistedState.runId
|
|
2783
|
+
}),
|
|
2784
|
+
memoryProvider: memoryConfig.provider,
|
|
2785
|
+
artifactStore,
|
|
2786
|
+
now,
|
|
2787
|
+
startedAtMs: resolveResumedRunStartedAtMs(persistedState, now),
|
|
2788
|
+
persistRunState: options?.persistRunState ?? true,
|
|
2789
|
+
runStateDirectory: options?.runStateDirectory ?? defaultRunStateDirectory
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function resolveResumedRunStartedAtMs(persistedState, now) {
|
|
2793
|
+
if (persistedState.startedAt === null) {
|
|
2794
|
+
return now().getTime();
|
|
2795
|
+
}
|
|
2796
|
+
return new Date(persistedState.startedAt).getTime();
|
|
2797
|
+
}
|
|
2798
|
+
async function rejectPendingWorkflowApproval(context, pendingApproval, decision) {
|
|
2799
|
+
const rejectionReason = decision.reason ?? "approval rejected";
|
|
2800
|
+
emitApprovalRejectedEvent(
|
|
2801
|
+
buildWorkflowEventContext(context),
|
|
2802
|
+
{
|
|
2803
|
+
approvalId: pendingApproval.approvalId,
|
|
2804
|
+
decidedBy: decision.approvedBy,
|
|
2805
|
+
parentEventId: pendingApproval.toolRequestEventId
|
|
2806
|
+
},
|
|
2807
|
+
rejectionReason
|
|
2808
|
+
);
|
|
2809
|
+
await failWorkflowRun(
|
|
2810
|
+
context,
|
|
2811
|
+
new AppError13({
|
|
2812
|
+
code: AppErrorCode13.Forbidden,
|
|
2813
|
+
message: `tool "${pendingApproval.toolName}" at step ${pendingApproval.stepIndex} was rejected by "${decision.approvedBy}": ${rejectionReason}`,
|
|
2814
|
+
statusCode: 403,
|
|
2815
|
+
details: {
|
|
2816
|
+
toolName: pendingApproval.toolName,
|
|
2817
|
+
stepIndex: pendingApproval.stepIndex,
|
|
2818
|
+
decidedBy: decision.approvedBy,
|
|
2819
|
+
reason: rejectionReason
|
|
2820
|
+
}
|
|
2821
|
+
})
|
|
2822
|
+
);
|
|
2823
|
+
await context.traceBus.flushSinks();
|
|
2824
|
+
return {
|
|
2825
|
+
status: "rejected",
|
|
2826
|
+
runId: context.runId,
|
|
2827
|
+
state: await buildWorkflowRunState(context),
|
|
2828
|
+
events: context.traceBus.listTraceEvents()
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
async function approvePendingWorkflowApproval(context, pendingApproval, decision) {
|
|
2832
|
+
emitApprovalApprovedEvent(buildWorkflowEventContext(context), {
|
|
2833
|
+
approvalId: pendingApproval.approvalId,
|
|
2834
|
+
decidedBy: decision.approvedBy,
|
|
2835
|
+
parentEventId: pendingApproval.toolRequestEventId
|
|
2836
|
+
});
|
|
2837
|
+
await executeApprovedWorkflowTool(context, pendingApproval);
|
|
2838
|
+
const result = await continueWorkflowRunFromStep(context, {
|
|
2839
|
+
input: pendingApproval.agentOutput,
|
|
2840
|
+
startStepIndex: pendingApproval.stepIndex + 1,
|
|
2841
|
+
lastAgentName: pendingApproval.agentName
|
|
2842
|
+
});
|
|
2843
|
+
await context.traceBus.flushSinks();
|
|
2844
|
+
return result;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// src/memory/json-file-provider.ts
|
|
2848
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2849
|
+
import { dirname } from "path";
|
|
2850
|
+
import { JsonObjectSchema as JsonObjectSchema2 } from "@assemble-dev/shared-types/json";
|
|
2851
|
+
function parseMemoryFileContents(rawContents) {
|
|
2852
|
+
try {
|
|
2853
|
+
const parsedContents = JsonObjectSchema2.parse(JSON.parse(rawContents));
|
|
2854
|
+
return new Map(Object.entries(parsedContents));
|
|
2855
|
+
} catch {
|
|
2856
|
+
return /* @__PURE__ */ new Map();
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
function loadMemoryEntriesFromFile(filePath) {
|
|
2860
|
+
if (!existsSync2(filePath)) {
|
|
2861
|
+
return /* @__PURE__ */ new Map();
|
|
2862
|
+
}
|
|
2863
|
+
return parseMemoryFileContents(readFileSync2(filePath, "utf8"));
|
|
2864
|
+
}
|
|
2865
|
+
function writeMemoryEntriesToFile(filePath, memoryEntries) {
|
|
2866
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
2867
|
+
writeFileSync2(
|
|
2868
|
+
filePath,
|
|
2869
|
+
JSON.stringify(Object.fromEntries(memoryEntries)),
|
|
2870
|
+
"utf8"
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
function createJsonFileMemoryProvider(options) {
|
|
2874
|
+
let memoryEntries = null;
|
|
2875
|
+
const ensureMemoryEntriesLoaded = () => {
|
|
2876
|
+
if (memoryEntries === null) {
|
|
2877
|
+
memoryEntries = loadMemoryEntriesFromFile(options.filePath);
|
|
2878
|
+
}
|
|
2879
|
+
return memoryEntries;
|
|
2880
|
+
};
|
|
2881
|
+
return {
|
|
2882
|
+
async getMemoryValue(key) {
|
|
2883
|
+
return await Promise.resolve(
|
|
2884
|
+
ensureMemoryEntriesLoaded().get(key) ?? null
|
|
2885
|
+
);
|
|
2886
|
+
},
|
|
2887
|
+
async setMemoryValue(key, value) {
|
|
2888
|
+
const loadedEntries = ensureMemoryEntriesLoaded();
|
|
2889
|
+
loadedEntries.set(key, value);
|
|
2890
|
+
writeMemoryEntriesToFile(options.filePath, loadedEntries);
|
|
2891
|
+
await Promise.resolve();
|
|
2892
|
+
},
|
|
2893
|
+
async appendMemoryValue(key, value) {
|
|
2894
|
+
const loadedEntries = ensureMemoryEntriesLoaded();
|
|
2895
|
+
loadedEntries.set(key, buildAppendedMemoryValue(loadedEntries.get(key), value));
|
|
2896
|
+
writeMemoryEntriesToFile(options.filePath, loadedEntries);
|
|
2897
|
+
await Promise.resolve();
|
|
2898
|
+
},
|
|
2899
|
+
async listMemoryKeys() {
|
|
2900
|
+
return await Promise.resolve([...ensureMemoryEntriesLoaded().keys()]);
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
// src/tracing/console-sink.ts
|
|
2906
|
+
var ansiCodes = {
|
|
2907
|
+
reset: "\x1B[0m",
|
|
2908
|
+
dim: "\x1B[2m",
|
|
2909
|
+
green: "\x1B[32m",
|
|
2910
|
+
red: "\x1B[31m",
|
|
2911
|
+
yellow: "\x1B[33m",
|
|
2912
|
+
blue: "\x1B[34m",
|
|
2913
|
+
magenta: "\x1B[35m",
|
|
2914
|
+
cyan: "\x1B[36m"
|
|
2915
|
+
};
|
|
2916
|
+
var colorByEventCategory = {
|
|
2917
|
+
run: "magenta",
|
|
2918
|
+
agent: "blue",
|
|
2919
|
+
tool: "cyan",
|
|
2920
|
+
policy: "yellow",
|
|
2921
|
+
route: "yellow",
|
|
2922
|
+
approval: "yellow",
|
|
2923
|
+
eval: "green",
|
|
2924
|
+
replay: "green"
|
|
2925
|
+
};
|
|
2926
|
+
function createConsoleSink(options = {}) {
|
|
2927
|
+
const writeLine = options.writeLine ?? ((line) => process.stdout.write(`${line}
|
|
2928
|
+
`));
|
|
2929
|
+
const useColor = options.useColor ?? true;
|
|
2930
|
+
return {
|
|
2931
|
+
name: "console",
|
|
2932
|
+
writeTraceEvent(event) {
|
|
2933
|
+
writeLine(formatTraceEventLine(event, useColor));
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
function formatTraceEventLine(event, useColor) {
|
|
2938
|
+
const time = event.timestamp.slice(11, 23);
|
|
2939
|
+
const category = event.type.split(".")[0] ?? "run";
|
|
2940
|
+
const detail = formatTraceEventDetail(event);
|
|
2941
|
+
const suffix = detail === "" ? "" : ` ${detail}`;
|
|
2942
|
+
if (!useColor) {
|
|
2943
|
+
return `[${time}] ${event.type}${suffix}`;
|
|
2944
|
+
}
|
|
2945
|
+
const color = ansiCodes[colorByEventCategory[category] ?? "blue"];
|
|
2946
|
+
const failed = event.type.endsWith(".failed") || event.type.endsWith(".blocked");
|
|
2947
|
+
const typeColor = failed ? ansiCodes.red : color;
|
|
2948
|
+
return `${ansiCodes.dim}[${time}]${ansiCodes.reset} ${typeColor}${event.type}${ansiCodes.reset}${suffix}`;
|
|
2949
|
+
}
|
|
2950
|
+
function formatTraceEventDetail(event) {
|
|
2951
|
+
const parts = [];
|
|
2952
|
+
if ("agentName" in event && event.agentName !== void 0) {
|
|
2953
|
+
parts.push(`agent=${event.agentName}`);
|
|
2954
|
+
}
|
|
2955
|
+
if ("toolName" in event && event.toolName !== void 0) {
|
|
2956
|
+
parts.push(`tool=${event.toolName}`);
|
|
2957
|
+
}
|
|
2958
|
+
if ("selectedAgentName" in event) {
|
|
2959
|
+
parts.push(`selected=${event.selectedAgentName}`);
|
|
2960
|
+
}
|
|
2961
|
+
if ("action" in event) {
|
|
2962
|
+
parts.push(`action=${event.action}`);
|
|
2963
|
+
}
|
|
2964
|
+
if ("approvalId" in event) {
|
|
2965
|
+
parts.push(`approval=${event.approvalId}`);
|
|
2966
|
+
}
|
|
2967
|
+
if ("latencyMs" in event) {
|
|
2968
|
+
parts.push(`${Math.round(event.latencyMs)}ms`);
|
|
2969
|
+
}
|
|
2970
|
+
if ("error" in event) {
|
|
2971
|
+
parts.push(`error="${event.error.message}"`);
|
|
2972
|
+
}
|
|
2973
|
+
if ("reason" in event) {
|
|
2974
|
+
parts.push(`reason="${event.reason}"`);
|
|
2975
|
+
}
|
|
2976
|
+
return parts.join(" ");
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/tracing/jsonl-sink.ts
|
|
2980
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2981
|
+
import { join as join2 } from "path";
|
|
2982
|
+
var defaultTraceDirectory = join2(".assemble", "runs");
|
|
2983
|
+
function resolveTraceFilePath(runId, directory = defaultTraceDirectory) {
|
|
2984
|
+
return join2(directory, `${runId}.jsonl`);
|
|
2985
|
+
}
|
|
2986
|
+
function createJsonlSink(options) {
|
|
2987
|
+
const directory = options.directory ?? defaultTraceDirectory;
|
|
2988
|
+
const filePath = resolveTraceFilePath(options.runId, directory);
|
|
2989
|
+
mkdirSync3(directory, { recursive: true });
|
|
2990
|
+
return {
|
|
2991
|
+
name: "jsonl",
|
|
2992
|
+
writeTraceEvent(event) {
|
|
2993
|
+
appendFileSync(filePath, `${JSON.stringify(event)}
|
|
2994
|
+
`, "utf8");
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// src/platform/platform-trace-sink.ts
|
|
3000
|
+
import {
|
|
3001
|
+
resolveRedactionMode as resolveRedactionMode3
|
|
3002
|
+
} from "@assemble-dev/shared-utils/redaction";
|
|
3003
|
+
|
|
3004
|
+
// src/platform/platform-config.ts
|
|
3005
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3006
|
+
import { homedir } from "os";
|
|
3007
|
+
import { join as join3 } from "path";
|
|
3008
|
+
import { z as z5 } from "zod";
|
|
3009
|
+
var defaultPlatformBaseUrl = "https://api.assemble.dev";
|
|
3010
|
+
var defaultPlatformDashboardUrl = "https://app.assemble.dev";
|
|
3011
|
+
var PlatformEnvSchema = z5.object({
|
|
3012
|
+
ASSEMBLE_API_KEY: z5.string().min(1).optional(),
|
|
3013
|
+
ASSEMBLE_BASE_URL: z5.string().min(1).optional(),
|
|
3014
|
+
ASSEMBLE_DASHBOARD_URL: z5.string().min(1).optional()
|
|
3015
|
+
});
|
|
3016
|
+
var PlatformConfigFileSchema = z5.object({
|
|
3017
|
+
apiKey: z5.string().min(1).optional(),
|
|
3018
|
+
baseUrl: z5.string().min(1).optional()
|
|
3019
|
+
});
|
|
3020
|
+
var PlatformConfigSchema = z5.object({
|
|
3021
|
+
apiKey: z5.string().min(1).nullable(),
|
|
3022
|
+
baseUrl: z5.string().min(1),
|
|
3023
|
+
dashboardUrl: z5.string().min(1)
|
|
3024
|
+
});
|
|
3025
|
+
function resolvePlatformConfigFilePath(homeDirectory) {
|
|
3026
|
+
return join3(homeDirectory, ".assemble", "config.json");
|
|
3027
|
+
}
|
|
3028
|
+
function parsePlatformEnvironment(environmentVariables) {
|
|
3029
|
+
const parsedEnvironment = PlatformEnvSchema.safeParse(environmentVariables);
|
|
3030
|
+
if (parsedEnvironment.success) {
|
|
3031
|
+
return parsedEnvironment.data;
|
|
3032
|
+
}
|
|
3033
|
+
return {};
|
|
3034
|
+
}
|
|
3035
|
+
function isMissingConfigFileError(failure) {
|
|
3036
|
+
return "code" in failure && failure.code === "ENOENT";
|
|
3037
|
+
}
|
|
3038
|
+
function readPlatformConfigFile(homeDirectory) {
|
|
3039
|
+
const configFilePath = resolvePlatformConfigFilePath(homeDirectory);
|
|
3040
|
+
try {
|
|
3041
|
+
const rawContents = readFileSync3(configFilePath, "utf8");
|
|
3042
|
+
return PlatformConfigFileSchema.parse(JSON.parse(rawContents));
|
|
3043
|
+
} catch (caughtError) {
|
|
3044
|
+
const failure = caughtError instanceof Error ? caughtError : new Error(String(caughtError));
|
|
3045
|
+
if (!isMissingConfigFileError(failure)) {
|
|
3046
|
+
console.error(
|
|
3047
|
+
`assemble platform config: failed to load config file at ${configFilePath}: ${failure.message}`
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
return {};
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
function resolvePlatformConfig(options) {
|
|
3054
|
+
const environment = parsePlatformEnvironment(
|
|
3055
|
+
options?.environmentVariables ?? process.env
|
|
3056
|
+
);
|
|
3057
|
+
const configFile = readPlatformConfigFile(
|
|
3058
|
+
options?.homeDirectory ?? homedir()
|
|
3059
|
+
);
|
|
3060
|
+
return PlatformConfigSchema.parse({
|
|
3061
|
+
apiKey: options?.apiKey ?? environment.ASSEMBLE_API_KEY ?? configFile.apiKey ?? null,
|
|
3062
|
+
baseUrl: options?.baseUrl ?? environment.ASSEMBLE_BASE_URL ?? configFile.baseUrl ?? defaultPlatformBaseUrl,
|
|
3063
|
+
dashboardUrl: options?.dashboardUrl ?? environment.ASSEMBLE_DASHBOARD_URL ?? defaultPlatformDashboardUrl
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// src/platform/redact-trace-event.ts
|
|
3068
|
+
import {
|
|
3069
|
+
TraceEventErrorSchema,
|
|
3070
|
+
TraceEventSchema as TraceEventSchema2
|
|
3071
|
+
} from "@assemble-dev/shared-types/trace-events";
|
|
3072
|
+
import {
|
|
3073
|
+
redactTracePayload as redactTracePayload2,
|
|
3074
|
+
redactedValue,
|
|
3075
|
+
resolveRedactionMode as resolveRedactionMode2
|
|
3076
|
+
} from "@assemble-dev/shared-utils/redaction";
|
|
3077
|
+
function buildErrorJsonPayload(error) {
|
|
3078
|
+
const payload = { message: error.message };
|
|
3079
|
+
if (error.code !== void 0) {
|
|
3080
|
+
payload.code = error.code;
|
|
3081
|
+
}
|
|
3082
|
+
if (error.stack !== void 0) {
|
|
3083
|
+
payload.stack = error.stack;
|
|
3084
|
+
}
|
|
3085
|
+
return payload;
|
|
3086
|
+
}
|
|
3087
|
+
function redactTraceEventReason(reason, mode, sensitiveFieldNames) {
|
|
3088
|
+
const stringPreservingMode = mode === "metadata-only" ? "redacted" : mode;
|
|
3089
|
+
const outcome = redactTracePayload2(reason, {
|
|
3090
|
+
mode: stringPreservingMode,
|
|
3091
|
+
sensitiveFieldNames
|
|
3092
|
+
});
|
|
3093
|
+
if (typeof outcome.payload === "string") {
|
|
3094
|
+
return {
|
|
3095
|
+
reason: outcome.payload,
|
|
3096
|
+
redacted: outcome.payload !== reason
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
return { reason: redactedValue, redacted: true };
|
|
3100
|
+
}
|
|
3101
|
+
function redactTraceEventError(error, mode, sensitiveFieldNames) {
|
|
3102
|
+
const outcome = redactTracePayload2(buildErrorJsonPayload(error), {
|
|
3103
|
+
mode,
|
|
3104
|
+
sensitiveFieldNames
|
|
3105
|
+
});
|
|
3106
|
+
const parsedError = TraceEventErrorSchema.safeParse(outcome.payload);
|
|
3107
|
+
if (parsedError.success) {
|
|
3108
|
+
return { error: parsedError.data, redacted: outcome.redacted };
|
|
3109
|
+
}
|
|
3110
|
+
return { error: { message: redactedValue }, redacted: true };
|
|
3111
|
+
}
|
|
3112
|
+
function redactTraceEventPayloads(event, mode, sensitiveFieldNames) {
|
|
3113
|
+
const resolvedMode = resolveRedactionMode2(mode);
|
|
3114
|
+
const redactedEvent = { ...event };
|
|
3115
|
+
let payloadWasRedacted = false;
|
|
3116
|
+
if ("input" in redactedEvent) {
|
|
3117
|
+
const outcome = redactTracePayload2(redactedEvent.input, {
|
|
3118
|
+
mode: resolvedMode,
|
|
3119
|
+
sensitiveFieldNames
|
|
3120
|
+
});
|
|
3121
|
+
redactedEvent.input = outcome.payload;
|
|
3122
|
+
payloadWasRedacted = payloadWasRedacted || outcome.redacted;
|
|
3123
|
+
}
|
|
3124
|
+
if ("output" in redactedEvent) {
|
|
3125
|
+
const outcome = redactTracePayload2(redactedEvent.output, {
|
|
3126
|
+
mode: resolvedMode,
|
|
3127
|
+
sensitiveFieldNames
|
|
3128
|
+
});
|
|
3129
|
+
redactedEvent.output = outcome.payload;
|
|
3130
|
+
payloadWasRedacted = payloadWasRedacted || outcome.redacted;
|
|
3131
|
+
}
|
|
3132
|
+
if ("reason" in redactedEvent) {
|
|
3133
|
+
const outcome = redactTraceEventReason(
|
|
3134
|
+
redactedEvent.reason,
|
|
3135
|
+
resolvedMode,
|
|
3136
|
+
sensitiveFieldNames
|
|
3137
|
+
);
|
|
3138
|
+
redactedEvent.reason = outcome.reason;
|
|
3139
|
+
payloadWasRedacted = payloadWasRedacted || outcome.redacted;
|
|
3140
|
+
}
|
|
3141
|
+
if ("error" in redactedEvent) {
|
|
3142
|
+
const outcome = redactTraceEventError(
|
|
3143
|
+
redactedEvent.error,
|
|
3144
|
+
resolvedMode,
|
|
3145
|
+
sensitiveFieldNames
|
|
3146
|
+
);
|
|
3147
|
+
redactedEvent.error = outcome.error;
|
|
3148
|
+
payloadWasRedacted = payloadWasRedacted || outcome.redacted;
|
|
3149
|
+
}
|
|
3150
|
+
return TraceEventSchema2.parse({
|
|
3151
|
+
...redactedEvent,
|
|
3152
|
+
redacted: redactedEvent.redacted || payloadWasRedacted || resolvedMode !== "full"
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/platform/platform-trace-sink.ts
|
|
3157
|
+
var defaultTraceBatchSize = 20;
|
|
3158
|
+
var defaultTraceQueueSize = 1e3;
|
|
3159
|
+
function resolveTraceIngestUrl(baseUrl) {
|
|
3160
|
+
return `${baseUrl}/v1/ingest/trace-events`;
|
|
3161
|
+
}
|
|
3162
|
+
function buildPlatformSinkContext(options) {
|
|
3163
|
+
return {
|
|
3164
|
+
ingestUrl: resolveTraceIngestUrl(options.baseUrl ?? defaultPlatformBaseUrl),
|
|
3165
|
+
apiKey: options.apiKey,
|
|
3166
|
+
runId: options.runId,
|
|
3167
|
+
workflowName: options.workflowName,
|
|
3168
|
+
redactionMode: resolveRedactionMode3(options.redactionMode),
|
|
3169
|
+
sensitiveFieldNames: options.sensitiveFieldNames,
|
|
3170
|
+
batchSize: options.batchSize ?? defaultTraceBatchSize,
|
|
3171
|
+
maxQueueSize: options.maxQueueSize ?? defaultTraceQueueSize,
|
|
3172
|
+
fetchImplementation: options.fetchImplementation ?? fetch,
|
|
3173
|
+
onUploadError: options.onUploadError,
|
|
3174
|
+
queuedEvents: [],
|
|
3175
|
+
uploadStats: {
|
|
3176
|
+
uploadedEventCount: 0,
|
|
3177
|
+
droppedEventCount: 0,
|
|
3178
|
+
failedBatchCount: 0
|
|
3179
|
+
},
|
|
3180
|
+
uploadPromiseChain: Promise.resolve(),
|
|
3181
|
+
isClosed: false
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
function reportUploadError(context, uploadError) {
|
|
3185
|
+
if (context.onUploadError !== void 0) {
|
|
3186
|
+
context.onUploadError(uploadError);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function buildTraceEventBatchRequest(context, eventsToUpload) {
|
|
3190
|
+
return {
|
|
3191
|
+
method: "POST",
|
|
3192
|
+
headers: {
|
|
3193
|
+
authorization: `Bearer ${context.apiKey}`,
|
|
3194
|
+
"content-type": "application/json"
|
|
3195
|
+
},
|
|
3196
|
+
body: JSON.stringify({
|
|
3197
|
+
runId: context.runId,
|
|
3198
|
+
workflowName: context.workflowName,
|
|
3199
|
+
events: eventsToUpload
|
|
3200
|
+
})
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
async function attemptTraceEventBatchUpload(context, eventsToUpload) {
|
|
3204
|
+
try {
|
|
3205
|
+
const response = await context.fetchImplementation(
|
|
3206
|
+
context.ingestUrl,
|
|
3207
|
+
buildTraceEventBatchRequest(context, eventsToUpload)
|
|
3208
|
+
);
|
|
3209
|
+
if (response.ok) {
|
|
3210
|
+
return true;
|
|
3211
|
+
}
|
|
3212
|
+
reportUploadError(
|
|
3213
|
+
context,
|
|
3214
|
+
new Error(`trace event upload failed with status ${response.status}`)
|
|
3215
|
+
);
|
|
3216
|
+
return false;
|
|
3217
|
+
} catch (caughtError) {
|
|
3218
|
+
reportUploadError(
|
|
3219
|
+
context,
|
|
3220
|
+
caughtError instanceof Error ? caughtError : new Error(String(caughtError))
|
|
3221
|
+
);
|
|
3222
|
+
return false;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
async function uploadTraceEventBatchWithSingleRetry(context, eventsToUpload) {
|
|
3226
|
+
const uploadedOnFirstAttempt = await attemptTraceEventBatchUpload(
|
|
3227
|
+
context,
|
|
3228
|
+
eventsToUpload
|
|
3229
|
+
);
|
|
3230
|
+
const uploaded = uploadedOnFirstAttempt || await attemptTraceEventBatchUpload(context, eventsToUpload);
|
|
3231
|
+
if (uploaded) {
|
|
3232
|
+
context.uploadStats.uploadedEventCount += eventsToUpload.length;
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
context.uploadStats.failedBatchCount += 1;
|
|
3236
|
+
context.uploadStats.droppedEventCount += eventsToUpload.length;
|
|
3237
|
+
}
|
|
3238
|
+
function enqueueTraceEventBatchUpload(context, eventsToUpload) {
|
|
3239
|
+
context.uploadPromiseChain = context.uploadPromiseChain.then(async () => {
|
|
3240
|
+
await uploadTraceEventBatchWithSingleRetry(context, eventsToUpload);
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
function dropOldestQueuedEventsBeyondLimit(context) {
|
|
3244
|
+
while (context.queuedEvents.length > context.maxQueueSize) {
|
|
3245
|
+
context.queuedEvents.shift();
|
|
3246
|
+
context.uploadStats.droppedEventCount += 1;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
function scheduleFullBatchUploads(context) {
|
|
3250
|
+
while (context.queuedEvents.length >= context.batchSize) {
|
|
3251
|
+
enqueueTraceEventBatchUpload(
|
|
3252
|
+
context,
|
|
3253
|
+
context.queuedEvents.splice(0, context.batchSize)
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
function writeTraceEventToPlatformQueue(context, event) {
|
|
3258
|
+
if (context.isClosed) {
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
try {
|
|
3262
|
+
context.queuedEvents.push(
|
|
3263
|
+
redactTraceEventPayloads(
|
|
3264
|
+
event,
|
|
3265
|
+
context.redactionMode,
|
|
3266
|
+
context.sensitiveFieldNames
|
|
3267
|
+
)
|
|
3268
|
+
);
|
|
3269
|
+
} catch (caughtError) {
|
|
3270
|
+
context.uploadStats.droppedEventCount += 1;
|
|
3271
|
+
reportUploadError(
|
|
3272
|
+
context,
|
|
3273
|
+
caughtError instanceof Error ? caughtError : new Error(String(caughtError))
|
|
3274
|
+
);
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
dropOldestQueuedEventsBeyondLimit(context);
|
|
3278
|
+
scheduleFullBatchUploads(context);
|
|
3279
|
+
}
|
|
3280
|
+
async function flushQueuedTraceEvents(context) {
|
|
3281
|
+
if (context.queuedEvents.length > 0) {
|
|
3282
|
+
enqueueTraceEventBatchUpload(
|
|
3283
|
+
context,
|
|
3284
|
+
context.queuedEvents.splice(0, context.queuedEvents.length)
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
await context.uploadPromiseChain;
|
|
3288
|
+
}
|
|
3289
|
+
function createPlatformTraceSink(options) {
|
|
3290
|
+
const context = buildPlatformSinkContext(options);
|
|
3291
|
+
return {
|
|
3292
|
+
name: "platform",
|
|
3293
|
+
writeTraceEvent: (event) => {
|
|
3294
|
+
writeTraceEventToPlatformQueue(context, event);
|
|
3295
|
+
},
|
|
3296
|
+
flush: async () => {
|
|
3297
|
+
await flushQueuedTraceEvents(context);
|
|
3298
|
+
},
|
|
3299
|
+
close: () => {
|
|
3300
|
+
context.isClosed = true;
|
|
3301
|
+
},
|
|
3302
|
+
getUploadStats: () => ({ ...context.uploadStats })
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/platform/platform-messages.ts
|
|
3307
|
+
var platformKeySignupUrl = "https://app.assemble.dev/app/keys";
|
|
3308
|
+
function buildMissingKeyMessage() {
|
|
3309
|
+
return `Get a free developer key to see this trace hosted \u2192 ${platformKeySignupUrl}`;
|
|
3310
|
+
}
|
|
3311
|
+
function buildHostedTraceUrl(input) {
|
|
3312
|
+
return `${input.dashboardUrl}/app/runs/${input.runId}`;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// src/replay/replay-engine.ts
|
|
3316
|
+
import { generateRunId as generateRunId2, generateTraceEventId as generateTraceEventId5 } from "@assemble-dev/shared-utils/ids";
|
|
3317
|
+
|
|
3318
|
+
// src/replay/replay-plan.ts
|
|
3319
|
+
import { AppErrorCode as AppErrorCode15 } from "@assemble-dev/shared-types/errors";
|
|
3320
|
+
import { AppError as AppError15 } from "@assemble-dev/shared-utils/errors";
|
|
3321
|
+
|
|
3322
|
+
// src/replay/replay-mocks.ts
|
|
3323
|
+
import { mockModel } from "@assemble-dev/providers";
|
|
3324
|
+
import { AppErrorCode as AppErrorCode14 } from "@assemble-dev/shared-types/errors";
|
|
3325
|
+
import { AppError as AppError14 } from "@assemble-dev/shared-utils/errors";
|
|
3326
|
+
function buildReplayToolMocks(events) {
|
|
3327
|
+
const queuesByToolName = /* @__PURE__ */ new Map();
|
|
3328
|
+
for (const event of events) {
|
|
3329
|
+
if (event.type !== "tool.completed") {
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
const queue = queuesByToolName.get(event.toolName) ?? {
|
|
3333
|
+
remaining: [],
|
|
3334
|
+
recordedCount: 0
|
|
3335
|
+
};
|
|
3336
|
+
queue.remaining.push(event.output);
|
|
3337
|
+
queue.recordedCount += 1;
|
|
3338
|
+
queuesByToolName.set(event.toolName, queue);
|
|
3339
|
+
}
|
|
3340
|
+
return {
|
|
3341
|
+
takeNextRecordedToolOutput(toolName) {
|
|
3342
|
+
const queue = queuesByToolName.get(toolName);
|
|
3343
|
+
const nextOutput = queue?.remaining.shift();
|
|
3344
|
+
if (nextOutput === void 0) {
|
|
3345
|
+
throw buildReplayTraceExhaustionError(
|
|
3346
|
+
"tool",
|
|
3347
|
+
toolName,
|
|
3348
|
+
"tool.completed",
|
|
3349
|
+
queue?.recordedCount ?? 0
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
return nextOutput;
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
function buildReplayAgentModels(events) {
|
|
3357
|
+
const scriptedResponsesByAgentName = /* @__PURE__ */ new Map();
|
|
3358
|
+
for (const event of events) {
|
|
3359
|
+
if (event.type !== "agent.completed") {
|
|
3360
|
+
continue;
|
|
3361
|
+
}
|
|
3362
|
+
const scriptedResponses = scriptedResponsesByAgentName.get(event.agentName) ?? [];
|
|
3363
|
+
scriptedResponses.push(serializeRecordedAgentOutput(event.output));
|
|
3364
|
+
scriptedResponsesByAgentName.set(event.agentName, scriptedResponses);
|
|
3365
|
+
}
|
|
3366
|
+
const adaptersByAgentName = /* @__PURE__ */ new Map();
|
|
3367
|
+
return {
|
|
3368
|
+
resolveReplayModelForAgent(agentName) {
|
|
3369
|
+
const existingAdapter = adaptersByAgentName.get(agentName);
|
|
3370
|
+
if (existingAdapter !== void 0) {
|
|
3371
|
+
return existingAdapter;
|
|
3372
|
+
}
|
|
3373
|
+
const adapter = buildScriptedReplayModel(
|
|
3374
|
+
agentName,
|
|
3375
|
+
scriptedResponsesByAgentName.get(agentName) ?? []
|
|
3376
|
+
);
|
|
3377
|
+
adaptersByAgentName.set(agentName, adapter);
|
|
3378
|
+
return adapter;
|
|
3379
|
+
}
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
function buildScriptedReplayModel(agentName, scriptedResponses) {
|
|
3383
|
+
const recordedCount = scriptedResponses.length;
|
|
3384
|
+
return mockModel({
|
|
3385
|
+
name: `replay:${agentName}`,
|
|
3386
|
+
scriptedResponses: [...scriptedResponses],
|
|
3387
|
+
respond: () => {
|
|
3388
|
+
throw buildReplayTraceExhaustionError(
|
|
3389
|
+
"agent",
|
|
3390
|
+
agentName,
|
|
3391
|
+
"agent.completed",
|
|
3392
|
+
recordedCount
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
function serializeRecordedAgentOutput(output) {
|
|
3398
|
+
return typeof output === "string" ? output : JSON.stringify(output);
|
|
3399
|
+
}
|
|
3400
|
+
function buildReplayTraceExhaustionError(kind, name, eventType, recordedCount) {
|
|
3401
|
+
return new AppError14({
|
|
3402
|
+
code: AppErrorCode14.Conflict,
|
|
3403
|
+
message: `replay requested recorded output ${recordedCount + 1} for ${kind} "${name}" but the source trace only holds ${recordedCount} "${eventType}" event(s) for it; the replay diverged from the source run, and a replay may only consume outputs the source run recorded. Record a new source run that covers this call, replay from an earlier step, or disable mocking for this ${kind}`,
|
|
3404
|
+
statusCode: 409,
|
|
3405
|
+
details: { kind, name, recordedCount }
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/replay/replay-plan.ts
|
|
3410
|
+
function buildReplayStartPlan(definition, sourceRunId, source, fromStepIndex) {
|
|
3411
|
+
if (fromStepIndex === 0) {
|
|
3412
|
+
return {
|
|
3413
|
+
startInput: validateReplayStartInput(definition, sourceRunId, source.state),
|
|
3414
|
+
lastAgentName: null,
|
|
3415
|
+
fromEventId: null,
|
|
3416
|
+
replayEvents: source.events,
|
|
3417
|
+
seedSteps: []
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
const startedEvents = source.events.filter(
|
|
3421
|
+
(event) => event.type === "agent.started"
|
|
3422
|
+
);
|
|
3423
|
+
const completedEvents = source.events.filter(
|
|
3424
|
+
(event) => event.type === "agent.completed"
|
|
3425
|
+
);
|
|
3426
|
+
const fromStepEvent = startedEvents[fromStepIndex];
|
|
3427
|
+
const previousStepEvent = completedEvents[fromStepIndex - 1];
|
|
3428
|
+
if (fromStepEvent === void 0 || previousStepEvent === void 0) {
|
|
3429
|
+
throw buildFromStepOutOfRangeError(
|
|
3430
|
+
sourceRunId,
|
|
3431
|
+
fromStepIndex,
|
|
3432
|
+
startedEvents.length
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
return {
|
|
3436
|
+
startInput: previousStepEvent.output,
|
|
3437
|
+
lastAgentName: previousStepEvent.agentName,
|
|
3438
|
+
fromEventId: fromStepEvent.id,
|
|
3439
|
+
replayEvents: source.events.slice(source.events.indexOf(fromStepEvent)),
|
|
3440
|
+
seedSteps: buildReplaySeedSteps(
|
|
3441
|
+
sourceRunId,
|
|
3442
|
+
startedEvents,
|
|
3443
|
+
completedEvents,
|
|
3444
|
+
fromStepIndex
|
|
3445
|
+
)
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
function buildReplaySeedSteps(sourceRunId, startedEvents, completedEvents, fromStepIndex) {
|
|
3449
|
+
const seedSteps = [];
|
|
3450
|
+
for (let stepIndex = 0; stepIndex < fromStepIndex; stepIndex += 1) {
|
|
3451
|
+
const startedEvent = startedEvents[stepIndex];
|
|
3452
|
+
const completedEvent = completedEvents[stepIndex];
|
|
3453
|
+
if (startedEvent === void 0 || completedEvent === void 0) {
|
|
3454
|
+
throw buildFromStepOutOfRangeError(
|
|
3455
|
+
sourceRunId,
|
|
3456
|
+
fromStepIndex,
|
|
3457
|
+
startedEvents.length
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
seedSteps.push({
|
|
3461
|
+
agentName: completedEvent.agentName,
|
|
3462
|
+
input: startedEvent.input,
|
|
3463
|
+
output: completedEvent.output
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
return seedSteps;
|
|
3467
|
+
}
|
|
3468
|
+
function buildFromStepOutOfRangeError(sourceRunId, fromStepIndex, recordedStepCount) {
|
|
3469
|
+
return new AppError15({
|
|
3470
|
+
code: AppErrorCode15.BadRequest,
|
|
3471
|
+
message: `fromStepIndex ${fromStepIndex} is out of range for source run "${sourceRunId}"; the source trace recorded ${recordedStepCount} completed agent step(s), so fromStepIndex must be between 0 and ${Math.max(0, recordedStepCount - 1)}`,
|
|
3472
|
+
statusCode: 400,
|
|
3473
|
+
details: { sourceRunId, fromStepIndex, recordedStepCount }
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
function validateReplayStartInput(definition, sourceRunId, sourceState) {
|
|
3477
|
+
const parsedInput = definition.inputSchema.safeParse(sourceState.input);
|
|
3478
|
+
if (parsedInput.success) {
|
|
3479
|
+
return parsedInput.data;
|
|
3480
|
+
}
|
|
3481
|
+
throw new AppError15({
|
|
3482
|
+
code: AppErrorCode15.ValidationFailed,
|
|
3483
|
+
message: `source run "${sourceRunId}" input failed workflow "${definition.name}" input schema validation: ${parsedInput.error.message}`,
|
|
3484
|
+
statusCode: 422,
|
|
3485
|
+
details: { sourceRunId, workflowName: definition.name }
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
function buildMockedReplayDefinition(definition, replayEvents, mockTools) {
|
|
3489
|
+
const agentModels = buildReplayAgentModels(replayEvents);
|
|
3490
|
+
const toolMocks = buildReplayToolMocks(replayEvents);
|
|
3491
|
+
return workflow({
|
|
3492
|
+
name: definition.name,
|
|
3493
|
+
inputSchema: definition.inputSchema,
|
|
3494
|
+
outputSchema: definition.outputSchema ?? void 0,
|
|
3495
|
+
agents: definition.agents.map((agentDefinition) => ({
|
|
3496
|
+
...agentDefinition,
|
|
3497
|
+
model: agentModels.resolveReplayModelForAgent(agentDefinition.name)
|
|
3498
|
+
})),
|
|
3499
|
+
tools: definition.tools.map(
|
|
3500
|
+
(toolDefinition) => mockTools ? {
|
|
3501
|
+
...toolDefinition,
|
|
3502
|
+
execute: async () => await Promise.resolve(
|
|
3503
|
+
toolMocks.takeNextRecordedToolOutput(toolDefinition.name)
|
|
3504
|
+
)
|
|
3505
|
+
} : toolDefinition
|
|
3506
|
+
),
|
|
3507
|
+
agentToolBindings: [...definition.agentToolBindings],
|
|
3508
|
+
topology: definition.topology,
|
|
3509
|
+
policies: [...definition.policies],
|
|
3510
|
+
memory: definition.memory ?? void 0,
|
|
3511
|
+
stopCondition: definition.stopCondition ?? void 0,
|
|
3512
|
+
continueOnAgentFailure: definition.continueOnAgentFailure
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// src/replay/replay-source.ts
|
|
3517
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
3518
|
+
import { AppErrorCode as AppErrorCode16 } from "@assemble-dev/shared-types/errors";
|
|
3519
|
+
import { TraceEventSchema as TraceEventSchema3 } from "@assemble-dev/shared-types/trace-events";
|
|
3520
|
+
import { AppError as AppError16 } from "@assemble-dev/shared-utils/errors";
|
|
3521
|
+
function loadReplaySource(input) {
|
|
3522
|
+
const runStateDirectory = input.runStateDirectory ?? defaultRunStateDirectory;
|
|
3523
|
+
const traceDirectory = input.traceDirectory ?? defaultTraceDirectory;
|
|
3524
|
+
return {
|
|
3525
|
+
state: loadSourceRunState(input.runId, runStateDirectory),
|
|
3526
|
+
events: loadSourceTraceEvents(input.runId, traceDirectory)
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
function loadSourceRunState(runId, directory) {
|
|
3530
|
+
const filePath = resolveRunStateFilePath(runId, directory);
|
|
3531
|
+
if (!existsSync3(filePath)) {
|
|
3532
|
+
throw new AppError16({
|
|
3533
|
+
code: AppErrorCode16.NotFound,
|
|
3534
|
+
message: `run state file not found at "${filePath}" for run "${runId}"; the run was never persisted or the runStateDirectory is wrong`,
|
|
3535
|
+
statusCode: 404,
|
|
3536
|
+
details: { runId, filePath }
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
return loadRunStateFile({ runId, directory });
|
|
3540
|
+
}
|
|
3541
|
+
function loadSourceTraceEvents(runId, directory) {
|
|
3542
|
+
const filePath = resolveTraceFilePath(runId, directory);
|
|
3543
|
+
if (!existsSync3(filePath)) {
|
|
3544
|
+
throw new AppError16({
|
|
3545
|
+
code: AppErrorCode16.NotFound,
|
|
3546
|
+
message: `trace file not found at "${filePath}" for run "${runId}"; run the workflow with a jsonl sink or fix the traceDirectory`,
|
|
3547
|
+
statusCode: 404,
|
|
3548
|
+
details: { runId, filePath }
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
const lines = readFileSync4(filePath, "utf8").split("\n");
|
|
3552
|
+
const events = [];
|
|
3553
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
3554
|
+
const line = lines[lineIndex];
|
|
3555
|
+
if (line === void 0 || line.trim().length === 0) {
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
events.push(parseTraceEventLine(line, lineIndex + 1, filePath));
|
|
3559
|
+
}
|
|
3560
|
+
return events;
|
|
3561
|
+
}
|
|
3562
|
+
function parseTraceEventLine(line, lineNumber, filePath) {
|
|
3563
|
+
try {
|
|
3564
|
+
return TraceEventSchema3.parse(JSON.parse(line));
|
|
3565
|
+
} catch (thrownError) {
|
|
3566
|
+
const reason = thrownError instanceof Error ? thrownError.message : String(thrownError);
|
|
3567
|
+
throw new AppError16({
|
|
3568
|
+
code: AppErrorCode16.ValidationFailed,
|
|
3569
|
+
message: `trace file "${filePath}" has an invalid trace event on line ${lineNumber}: ${reason}`,
|
|
3570
|
+
statusCode: 422,
|
|
3571
|
+
details: { filePath, lineNumber }
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// src/replay/replay-engine.ts
|
|
3577
|
+
async function replayRun(replayInput) {
|
|
3578
|
+
const settings = resolveReplaySettings(replayInput.options);
|
|
3579
|
+
const source = loadReplaySource({
|
|
3580
|
+
runId: replayInput.sourceRunId,
|
|
3581
|
+
runStateDirectory: settings.runStateDirectory,
|
|
3582
|
+
traceDirectory: settings.traceDirectory
|
|
3583
|
+
});
|
|
3584
|
+
const plan = buildReplayStartPlan(
|
|
3585
|
+
replayInput.definition,
|
|
3586
|
+
replayInput.sourceRunId,
|
|
3587
|
+
source,
|
|
3588
|
+
settings.fromStepIndex
|
|
3589
|
+
);
|
|
3590
|
+
const replayDefinition = buildMockedReplayDefinition(
|
|
3591
|
+
replayInput.definition,
|
|
3592
|
+
plan.replayEvents,
|
|
3593
|
+
settings.mockTools
|
|
3594
|
+
);
|
|
3595
|
+
const context = createReplayRuntimeContext(
|
|
3596
|
+
replayDefinition,
|
|
3597
|
+
source,
|
|
3598
|
+
settings,
|
|
3599
|
+
replayInput.sourceRunId,
|
|
3600
|
+
plan.fromEventId
|
|
3601
|
+
);
|
|
3602
|
+
await persistWorkflowRunState(context);
|
|
3603
|
+
await seedReplayContextFromSource(context, source, plan, settings.fromStepIndex);
|
|
3604
|
+
const runResult = await continueWorkflowRunFromStep(context, {
|
|
3605
|
+
input: plan.startInput,
|
|
3606
|
+
startStepIndex: settings.fromStepIndex,
|
|
3607
|
+
lastAgentName: plan.lastAgentName
|
|
3608
|
+
});
|
|
3609
|
+
return await finishReplayRun(context, replayInput.sourceRunId, runResult);
|
|
3610
|
+
}
|
|
3611
|
+
function resolveReplaySettings(options) {
|
|
3612
|
+
return {
|
|
3613
|
+
fromStepIndex: options?.fromStepIndex ?? 0,
|
|
3614
|
+
mockTools: options?.mockTools ?? true,
|
|
3615
|
+
sinks: options?.sinks ?? [],
|
|
3616
|
+
runStateDirectory: options?.runStateDirectory ?? defaultRunStateDirectory,
|
|
3617
|
+
traceDirectory: options?.traceDirectory ?? defaultTraceDirectory,
|
|
3618
|
+
now: options?.now ?? (() => /* @__PURE__ */ new Date()),
|
|
3619
|
+
replayRunId: options?.replayRunId ?? null
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
function createReplayRuntimeContext(replayDefinition, source, settings, sourceRunId, fromEventId) {
|
|
3623
|
+
const runId = settings.replayRunId ?? generateRunId2();
|
|
3624
|
+
const traceBus = createTraceBus({
|
|
3625
|
+
runId,
|
|
3626
|
+
workflowName: replayDefinition.name,
|
|
3627
|
+
input: source.state.input,
|
|
3628
|
+
sinks: settings.sinks,
|
|
3629
|
+
now: settings.now
|
|
3630
|
+
});
|
|
3631
|
+
emitReplayStartedEvent(traceBus, runId, replayDefinition.name, settings.now, {
|
|
3632
|
+
sourceRunId,
|
|
3633
|
+
fromEventId
|
|
3634
|
+
});
|
|
3635
|
+
const runStartedEvent = emitRunStartedEvent(
|
|
3636
|
+
{ traceBus, runId, workflowName: replayDefinition.name, now: settings.now },
|
|
3637
|
+
source.state.input
|
|
3638
|
+
);
|
|
3639
|
+
const memoryConfig = resolveWorkflowMemoryConfig(
|
|
3640
|
+
replayDefinition.memory ?? void 0
|
|
3641
|
+
);
|
|
3642
|
+
return {
|
|
3643
|
+
definition: replayDefinition,
|
|
3644
|
+
traceBus,
|
|
3645
|
+
runId,
|
|
3646
|
+
runStartedEventId: runStartedEvent.id,
|
|
3647
|
+
threadMemory: createThreadMemory({
|
|
3648
|
+
provider: memoryConfig.provider,
|
|
3649
|
+
threadKey: runId
|
|
3650
|
+
}),
|
|
3651
|
+
memoryProvider: memoryConfig.provider,
|
|
3652
|
+
artifactStore: createArtifactStore({
|
|
3653
|
+
provider: memoryConfig.provider,
|
|
3654
|
+
runId
|
|
3655
|
+
}),
|
|
3656
|
+
now: settings.now,
|
|
3657
|
+
startedAtMs: settings.now().getTime(),
|
|
3658
|
+
persistRunState: true,
|
|
3659
|
+
runStateDirectory: settings.runStateDirectory
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function emitReplayStartedEvent(traceBus, runId, workflowName, now, replayLink) {
|
|
3663
|
+
traceBus.emitTraceEvent({
|
|
3664
|
+
id: generateTraceEventId5(),
|
|
3665
|
+
runId,
|
|
3666
|
+
workflowName,
|
|
3667
|
+
timestamp: now().toISOString(),
|
|
3668
|
+
parentEventId: null,
|
|
3669
|
+
redacted: false,
|
|
3670
|
+
type: "replay.started",
|
|
3671
|
+
sourceRunId: replayLink.sourceRunId,
|
|
3672
|
+
fromEventId: replayLink.fromEventId
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
async function seedReplayContextFromSource(context, source, plan, fromStepIndex) {
|
|
3676
|
+
if (fromStepIndex === 0) {
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
for (const artifact of source.state.artifacts) {
|
|
3680
|
+
const artifactStep = parseArtifactStepIndex(artifact.key);
|
|
3681
|
+
if (artifactStep !== null && artifactStep < fromStepIndex) {
|
|
3682
|
+
await context.artifactStore.saveRunArtifact(artifact);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
for (const seedStep of plan.seedSteps) {
|
|
3686
|
+
await appendAgentExchangeToThreadMemory(
|
|
3687
|
+
context.threadMemory,
|
|
3688
|
+
seedStep.agentName,
|
|
3689
|
+
seedStep.input,
|
|
3690
|
+
serializeAgentInputToText(seedStep.output),
|
|
3691
|
+
context.now
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
function parseArtifactStepIndex(artifactKey) {
|
|
3696
|
+
const match = /^step-(\d+):/.exec(artifactKey);
|
|
3697
|
+
const capturedStepIndex = match?.[1];
|
|
3698
|
+
if (capturedStepIndex === void 0) {
|
|
3699
|
+
return null;
|
|
3700
|
+
}
|
|
3701
|
+
return Number(capturedStepIndex);
|
|
3702
|
+
}
|
|
3703
|
+
async function finishReplayRun(context, sourceRunId, runResult) {
|
|
3704
|
+
context.traceBus.emitTraceEvent({
|
|
3705
|
+
id: generateTraceEventId5(),
|
|
3706
|
+
runId: context.runId,
|
|
3707
|
+
workflowName: context.definition.name,
|
|
3708
|
+
timestamp: context.now().toISOString(),
|
|
3709
|
+
parentEventId: context.runStartedEventId,
|
|
3710
|
+
redacted: false,
|
|
3711
|
+
type: "replay.completed",
|
|
3712
|
+
sourceRunId,
|
|
3713
|
+
latencyMs: calculateWorkflowElapsedMs(context)
|
|
3714
|
+
});
|
|
3715
|
+
await context.traceBus.flushSinks();
|
|
3716
|
+
return {
|
|
3717
|
+
status: runResult.status,
|
|
3718
|
+
runId: context.runId,
|
|
3719
|
+
sourceRunId,
|
|
3720
|
+
output: runResult.status === "completed" ? runResult.output : null,
|
|
3721
|
+
approvalId: runResult.status === "awaiting_approval" ? runResult.approvalId : null,
|
|
3722
|
+
state: await buildWorkflowRunState(context),
|
|
3723
|
+
events: context.traceBus.listTraceEvents()
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// src/replay/replay-comparison.ts
|
|
3728
|
+
var maxReplayOutputDiffCount = 50;
|
|
3729
|
+
function compareRunToReplay(input) {
|
|
3730
|
+
const outputDiff = collectJsonLeafDiffs(
|
|
3731
|
+
"$",
|
|
3732
|
+
input.original.finalOutput,
|
|
3733
|
+
input.replay.finalOutput,
|
|
3734
|
+
[]
|
|
3735
|
+
);
|
|
3736
|
+
return {
|
|
3737
|
+
statusMatches: input.original.status === input.replay.status,
|
|
3738
|
+
originalStatus: input.original.status,
|
|
3739
|
+
replayStatus: input.replay.status,
|
|
3740
|
+
outputMatches: outputDiff.length === 0,
|
|
3741
|
+
outputDiff,
|
|
3742
|
+
latency: compareRunLatency(input.original, input.replay),
|
|
3743
|
+
tokenUsage: {
|
|
3744
|
+
original: input.original.metrics.tokenUsage,
|
|
3745
|
+
replay: input.replay.metrics.tokenUsage
|
|
3746
|
+
},
|
|
3747
|
+
routingMatches: compareRoutingSequences(input.original, input.replay)
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
function compareRunLatency(original, replay) {
|
|
3751
|
+
const originalMs = original.metrics.totalLatencyMs;
|
|
3752
|
+
const replayMs = replay.metrics.totalLatencyMs;
|
|
3753
|
+
return {
|
|
3754
|
+
originalMs,
|
|
3755
|
+
replayMs,
|
|
3756
|
+
deltaMs: originalMs !== null && replayMs !== null ? replayMs - originalMs : null
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
function compareRoutingSequences(original, replay) {
|
|
3760
|
+
const originalSequence = original.routingDecisions.map(
|
|
3761
|
+
(decision) => decision.selectedAgentName
|
|
3762
|
+
);
|
|
3763
|
+
const replaySequence = replay.routingDecisions.map(
|
|
3764
|
+
(decision) => decision.selectedAgentName
|
|
3765
|
+
);
|
|
3766
|
+
if (originalSequence.length !== replaySequence.length) {
|
|
3767
|
+
return false;
|
|
3768
|
+
}
|
|
3769
|
+
return originalSequence.every(
|
|
3770
|
+
(selectedAgentName, index) => selectedAgentName === replaySequence[index]
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
function collectJsonLeafDiffs(path, original, replay, diffs) {
|
|
3774
|
+
if (diffs.length >= maxReplayOutputDiffCount) {
|
|
3775
|
+
return diffs;
|
|
3776
|
+
}
|
|
3777
|
+
if (isJsonObject(original) && isJsonObject(replay)) {
|
|
3778
|
+
return collectJsonObjectDiffs(path, original, replay, diffs);
|
|
3779
|
+
}
|
|
3780
|
+
if (Array.isArray(original) && Array.isArray(replay)) {
|
|
3781
|
+
return collectJsonArrayDiffs(path, original, replay, diffs);
|
|
3782
|
+
}
|
|
3783
|
+
if (!jsonValuesEqual(original, replay)) {
|
|
3784
|
+
diffs.push({ path, original, replay });
|
|
3785
|
+
}
|
|
3786
|
+
return diffs;
|
|
3787
|
+
}
|
|
3788
|
+
function collectJsonObjectDiffs(path, original, replay, diffs) {
|
|
3789
|
+
const keys = [
|
|
3790
|
+
.../* @__PURE__ */ new Set([...Object.keys(original), ...Object.keys(replay)])
|
|
3791
|
+
].sort();
|
|
3792
|
+
for (const key of keys) {
|
|
3793
|
+
collectJsonLeafDiffs(
|
|
3794
|
+
`${path}.${key}`,
|
|
3795
|
+
original[key] ?? null,
|
|
3796
|
+
replay[key] ?? null,
|
|
3797
|
+
diffs
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
return diffs;
|
|
3801
|
+
}
|
|
3802
|
+
function collectJsonArrayDiffs(path, original, replay, diffs) {
|
|
3803
|
+
const length = Math.max(original.length, replay.length);
|
|
3804
|
+
for (let index = 0; index < length; index += 1) {
|
|
3805
|
+
collectJsonLeafDiffs(
|
|
3806
|
+
`${path}[${index}]`,
|
|
3807
|
+
original[index] ?? null,
|
|
3808
|
+
replay[index] ?? null,
|
|
3809
|
+
diffs
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
return diffs;
|
|
3813
|
+
}
|
|
3814
|
+
function jsonValuesEqual(original, replay) {
|
|
3815
|
+
if (original === replay) {
|
|
3816
|
+
return true;
|
|
3817
|
+
}
|
|
3818
|
+
if (Array.isArray(original) && Array.isArray(replay)) {
|
|
3819
|
+
return original.length === replay.length && original.every(
|
|
3820
|
+
(item, index) => jsonValuesEqual(item, replay[index] ?? null)
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
if (isJsonObject(original) && isJsonObject(replay)) {
|
|
3824
|
+
const originalKeys = Object.keys(original).sort();
|
|
3825
|
+
const replayKeys = Object.keys(replay).sort();
|
|
3826
|
+
return originalKeys.length === replayKeys.length && originalKeys.every(
|
|
3827
|
+
(key, index) => key === replayKeys[index] && jsonValuesEqual(original[key] ?? null, replay[key] ?? null)
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3830
|
+
return false;
|
|
3831
|
+
}
|
|
3832
|
+
function isJsonObject(value) {
|
|
3833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3834
|
+
}
|
|
3835
|
+
export {
|
|
3836
|
+
agent,
|
|
3837
|
+
applyTraceEventToRunState,
|
|
3838
|
+
buildHostedTraceUrl,
|
|
3839
|
+
buildMissingKeyMessage,
|
|
3840
|
+
buildPipelineTeamWorkflowConfig,
|
|
3841
|
+
buildReviewPanelWorkflowConfig,
|
|
3842
|
+
buildSupervisorTeamWorkflowConfig,
|
|
3843
|
+
compareRunToReplay,
|
|
3844
|
+
createArtifactStore,
|
|
3845
|
+
createConsoleSink,
|
|
3846
|
+
createInMemoryMemoryProvider,
|
|
3847
|
+
createInitialRunState,
|
|
3848
|
+
createJsonFileMemoryProvider,
|
|
3849
|
+
createJsonlSink,
|
|
3850
|
+
createPlatformTraceSink,
|
|
3851
|
+
createThreadMemory,
|
|
3852
|
+
createTraceBus,
|
|
3853
|
+
defaultParallelMergedArtifactKey,
|
|
3854
|
+
defaultRunStateDirectory,
|
|
3855
|
+
defaultSupervisorMaxSteps,
|
|
3856
|
+
defaultTraceDirectory,
|
|
3857
|
+
defaultWorkflowTopology,
|
|
3858
|
+
evaluateToolPolicies,
|
|
3859
|
+
executeToolCall,
|
|
3860
|
+
finishWorkflow,
|
|
3861
|
+
formatTraceEventLine,
|
|
3862
|
+
loadReplaySource,
|
|
3863
|
+
loadRunStateFile,
|
|
3864
|
+
maxReplayOutputDiffCount,
|
|
3865
|
+
policy,
|
|
3866
|
+
replayRun,
|
|
3867
|
+
resolvePlatformConfig,
|
|
3868
|
+
resolveRunStateFilePath,
|
|
3869
|
+
resolveTraceFilePath,
|
|
3870
|
+
resolveWorkflowMemoryConfig,
|
|
3871
|
+
resumeRun,
|
|
3872
|
+
runAgent,
|
|
3873
|
+
runWorkflow,
|
|
3874
|
+
team,
|
|
3875
|
+
tool,
|
|
3876
|
+
workflow
|
|
3877
|
+
};
|