@dogpile/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/browser/index.js +3992 -4997
  3. package/dist/browser/index.js.map +1 -1
  4. package/dist/providers/openai-compatible.d.ts.map +1 -1
  5. package/dist/providers/openai-compatible.js +5 -1
  6. package/dist/providers/openai-compatible.js.map +1 -1
  7. package/dist/runtime/broadcast.d.ts +1 -0
  8. package/dist/runtime/broadcast.d.ts.map +1 -1
  9. package/dist/runtime/broadcast.js +132 -69
  10. package/dist/runtime/broadcast.js.map +1 -1
  11. package/dist/runtime/coordinator.d.ts +4 -2
  12. package/dist/runtime/coordinator.d.ts.map +1 -1
  13. package/dist/runtime/coordinator.js +114 -39
  14. package/dist/runtime/coordinator.js.map +1 -1
  15. package/dist/runtime/defaults.d.ts.map +1 -1
  16. package/dist/runtime/defaults.js +2 -1
  17. package/dist/runtime/defaults.js.map +1 -1
  18. package/dist/runtime/engine.d.ts.map +1 -1
  19. package/dist/runtime/engine.js +54 -34
  20. package/dist/runtime/engine.js.map +1 -1
  21. package/dist/runtime/model.d.ts.map +1 -1
  22. package/dist/runtime/model.js +6 -3
  23. package/dist/runtime/model.js.map +1 -1
  24. package/dist/runtime/redaction.d.ts +13 -0
  25. package/dist/runtime/redaction.d.ts.map +1 -0
  26. package/dist/runtime/redaction.js +278 -0
  27. package/dist/runtime/redaction.js.map +1 -0
  28. package/dist/runtime/sanitization.d.ts +4 -0
  29. package/dist/runtime/sanitization.d.ts.map +1 -0
  30. package/dist/runtime/sanitization.js +63 -0
  31. package/dist/runtime/sanitization.js.map +1 -0
  32. package/dist/runtime/shared.d.ts +1 -0
  33. package/dist/runtime/shared.d.ts.map +1 -1
  34. package/dist/runtime/shared.js +128 -65
  35. package/dist/runtime/shared.js.map +1 -1
  36. package/dist/runtime/tools/built-in.d.ts +2 -0
  37. package/dist/runtime/tools/built-in.d.ts.map +1 -1
  38. package/dist/runtime/tools/built-in.js +153 -15
  39. package/dist/runtime/tools/built-in.js.map +1 -1
  40. package/dist/runtime/tools.d.ts.map +1 -1
  41. package/dist/runtime/tools.js +29 -7
  42. package/dist/runtime/tools.js.map +1 -1
  43. package/dist/runtime/validation.d.ts.map +1 -1
  44. package/dist/runtime/validation.js +3 -0
  45. package/dist/runtime/validation.js.map +1 -1
  46. package/dist/types/events.d.ts +3 -3
  47. package/dist/types/events.d.ts.map +1 -1
  48. package/dist/types/replay.d.ts +3 -1
  49. package/dist/types/replay.d.ts.map +1 -1
  50. package/dist/types.d.ts +20 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +8 -1
  53. package/src/providers/openai-compatible.ts +5 -1
  54. package/src/runtime/broadcast.ts +156 -72
  55. package/src/runtime/coordinator.ts +143 -47
  56. package/src/runtime/defaults.ts +2 -1
  57. package/src/runtime/engine.ts +77 -40
  58. package/src/runtime/model.ts +6 -3
  59. package/src/runtime/redaction.ts +355 -0
  60. package/src/runtime/sanitization.ts +81 -0
  61. package/src/runtime/shared.ts +152 -68
  62. package/src/runtime/tools/built-in.ts +168 -15
  63. package/src/runtime/tools.ts +39 -8
  64. package/src/runtime/validation.ts +3 -0
  65. package/src/types/events.ts +3 -3
  66. package/src/types/replay.ts +3 -1
  67. package/src/types.ts +20 -0
@@ -0,0 +1,355 @@
1
+ import type {
2
+ AgentDecision,
3
+ DelegateAgentDecision,
4
+ JsonObject,
5
+ ModelRequest,
6
+ ModelResponse,
7
+ ReplayTraceProtocolDecision,
8
+ ReplayTraceProviderCall,
9
+ RunEvent,
10
+ RunEventLog,
11
+ RunResult,
12
+ RuntimeToolExecutionRequest,
13
+ RuntimeToolResult,
14
+ Trace,
15
+ TranscriptEntry,
16
+ TranscriptToolCall
17
+ } from "../types.js";
18
+
19
+ export interface TraceRedactionOptions {
20
+ readonly replacementText?: string;
21
+ readonly redactPrompts?: boolean;
22
+ readonly redactOutputs?: boolean;
23
+ readonly redactToolInputs?: boolean;
24
+ readonly redactToolOutputs?: boolean;
25
+ readonly redactProviderResponses?: boolean;
26
+ readonly redactEmbeddedChildTraces?: boolean;
27
+ }
28
+
29
+ const DEFAULT_REPLACEMENT_TEXT = "[REDACTED]";
30
+ const REDACTED_JSON_OBJECT: JsonObject = { redacted: true };
31
+
32
+ export function redactTrace(trace: Trace, options: TraceRedactionOptions = {}): Trace {
33
+ return {
34
+ ...trace,
35
+ inputs: shouldRedactPrompts(options)
36
+ ? {
37
+ ...trace.inputs,
38
+ intent: replacement(options)
39
+ }
40
+ : trace.inputs,
41
+ protocolDecisions: trace.protocolDecisions.map((decision) => redactProtocolDecision(decision, options)),
42
+ providerCalls: trace.providerCalls.map((call) => redactProviderCall(call, options)),
43
+ finalOutput: shouldRedactOutputs(options)
44
+ ? {
45
+ ...trace.finalOutput,
46
+ output: replacement(options)
47
+ }
48
+ : trace.finalOutput,
49
+ events: trace.events.map((event) => redactRunEvent(event, options)),
50
+ transcript: trace.transcript.map((entry) => redactTranscriptEntry(entry, options))
51
+ };
52
+ }
53
+
54
+ export function redactRunResult(result: RunResult, options: TraceRedactionOptions = {}): RunResult {
55
+ const trace = redactTrace(result.trace, options);
56
+ const eventLog: RunEventLog = {
57
+ ...result.eventLog,
58
+ events: trace.events,
59
+ eventTypes: trace.events.map((event) => event.type),
60
+ eventCount: trace.events.length
61
+ };
62
+
63
+ return {
64
+ ...result,
65
+ output: shouldRedactOutputs(options) ? replacement(options) : result.output,
66
+ eventLog,
67
+ trace,
68
+ transcript: trace.transcript
69
+ };
70
+ }
71
+
72
+ function redactRunEvent(event: RunEvent, options: TraceRedactionOptions): RunEvent {
73
+ switch (event.type) {
74
+ case "model-request":
75
+ return {
76
+ ...event,
77
+ request: redactModelRequest(event.request, options)
78
+ };
79
+ case "model-response":
80
+ return {
81
+ ...event,
82
+ response: redactModelResponse(event.response, options)
83
+ };
84
+ case "model-output-chunk":
85
+ return shouldRedactOutputs(options)
86
+ ? {
87
+ ...event,
88
+ text: replacement(options),
89
+ outputLength: 0
90
+ }
91
+ : event;
92
+ case "tool-call":
93
+ return shouldRedactToolInputs(options)
94
+ ? {
95
+ ...event,
96
+ input: REDACTED_JSON_OBJECT
97
+ }
98
+ : event;
99
+ case "tool-result":
100
+ return {
101
+ ...event,
102
+ result: redactRuntimeToolResult(event.result, options)
103
+ };
104
+ case "agent-turn":
105
+ return {
106
+ ...event,
107
+ ...(shouldRedactPrompts(options) ? { input: replacement(options) } : {}),
108
+ ...(shouldRedactOutputs(options) ? { output: replacement(options) } : {}),
109
+ ...(event.decision !== undefined ? { decision: redactDecision(event.decision, options) } : {})
110
+ };
111
+ case "broadcast":
112
+ return {
113
+ ...event,
114
+ contributions: event.contributions.map((contribution) => ({
115
+ ...contribution,
116
+ ...(shouldRedactOutputs(options) ? { output: replacement(options) } : {}),
117
+ ...(contribution.decision !== undefined
118
+ ? { decision: redactDecision(contribution.decision, options) }
119
+ : {})
120
+ }))
121
+ };
122
+ case "sub-run-completed":
123
+ return shouldRedactEmbeddedChildTraces(options)
124
+ ? {
125
+ ...event,
126
+ subResult: redactRunResult(event.subResult, options)
127
+ }
128
+ : event;
129
+ case "sub-run-failed":
130
+ return shouldRedactEmbeddedChildTraces(options)
131
+ ? {
132
+ ...event,
133
+ error: {
134
+ ...event.error,
135
+ ...(event.error.detail !== undefined
136
+ ? { detail: redactErrorDetail(event.error.detail, options) }
137
+ : {})
138
+ },
139
+ partialTrace: redactTrace(event.partialTrace, options)
140
+ }
141
+ : event;
142
+ case "final":
143
+ return shouldRedactOutputs(options)
144
+ ? {
145
+ ...event,
146
+ output: replacement(options)
147
+ }
148
+ : event;
149
+ case "role-assignment":
150
+ case "budget-stop":
151
+ case "sub-run-started":
152
+ case "sub-run-parent-aborted":
153
+ case "sub-run-budget-clamped":
154
+ case "sub-run-queued":
155
+ case "sub-run-concurrency-clamped":
156
+ return event;
157
+ }
158
+ }
159
+
160
+ function redactProviderCall(
161
+ call: ReplayTraceProviderCall,
162
+ options: TraceRedactionOptions
163
+ ): ReplayTraceProviderCall {
164
+ return {
165
+ ...call,
166
+ request: redactModelRequest(call.request, options),
167
+ response: redactModelResponse(call.response, options)
168
+ };
169
+ }
170
+
171
+ function redactProtocolDecision(
172
+ decision: ReplayTraceProtocolDecision,
173
+ options: TraceRedactionOptions
174
+ ): ReplayTraceProtocolDecision {
175
+ return {
176
+ ...decision,
177
+ ...(decision.input !== undefined && shouldRedactPrompts(options) ? { input: replacement(options) } : {}),
178
+ ...(decision.output !== undefined && shouldRedactOutputs(options) ? { output: replacement(options) } : {})
179
+ };
180
+ }
181
+
182
+ function redactModelRequest(request: ModelRequest, options: TraceRedactionOptions): ModelRequest {
183
+ return shouldRedactPrompts(options)
184
+ ? {
185
+ ...request,
186
+ messages: request.messages.map((message) => ({
187
+ ...message,
188
+ content: replacement(options)
189
+ }))
190
+ }
191
+ : request;
192
+ }
193
+
194
+ function redactModelResponse(response: ModelResponse, options: TraceRedactionOptions): ModelResponse {
195
+ const redactedToolRequests = response.toolRequests?.map((request) => redactToolRequest(request, options));
196
+
197
+ if (shouldRedactProviderResponses(options)) {
198
+ return {
199
+ text: replacement(options),
200
+ ...(response.finishReason !== undefined ? { finishReason: response.finishReason } : {}),
201
+ ...(redactedToolRequests !== undefined && redactedToolRequests.length > 0 ? { toolRequests: redactedToolRequests } : {}),
202
+ ...(response.usage !== undefined ? { usage: response.usage } : {}),
203
+ ...(response.costUsd !== undefined ? { costUsd: response.costUsd } : {})
204
+ };
205
+ }
206
+
207
+ return {
208
+ ...response,
209
+ ...(shouldRedactOutputs(options) ? { text: replacement(options) } : {}),
210
+ ...(redactedToolRequests !== undefined ? { toolRequests: redactedToolRequests } : {})
211
+ };
212
+ }
213
+
214
+ function redactToolRequest(
215
+ request: RuntimeToolExecutionRequest,
216
+ options: TraceRedactionOptions
217
+ ): RuntimeToolExecutionRequest {
218
+ return shouldRedactToolInputs(options)
219
+ ? {
220
+ ...request,
221
+ input: REDACTED_JSON_OBJECT
222
+ }
223
+ : request;
224
+ }
225
+
226
+ function redactTranscriptEntry(entry: TranscriptEntry, options: TraceRedactionOptions): TranscriptEntry {
227
+ return {
228
+ ...entry,
229
+ ...(shouldRedactPrompts(options) ? { input: replacement(options) } : {}),
230
+ ...(shouldRedactOutputs(options) ? { output: replacement(options) } : {}),
231
+ ...(entry.decision !== undefined ? { decision: redactDecision(entry.decision, options) } : {}),
232
+ ...(entry.toolCalls !== undefined
233
+ ? { toolCalls: entry.toolCalls.map((toolCall) => redactTranscriptToolCall(toolCall, options)) }
234
+ : {})
235
+ };
236
+ }
237
+
238
+ function redactTranscriptToolCall(
239
+ toolCall: TranscriptToolCall,
240
+ options: TraceRedactionOptions
241
+ ): TranscriptToolCall {
242
+ return {
243
+ ...toolCall,
244
+ ...(shouldRedactToolInputs(options) ? { input: REDACTED_JSON_OBJECT } : {}),
245
+ result: redactRuntimeToolResult(toolCall.result, options)
246
+ };
247
+ }
248
+
249
+ function redactRuntimeToolResult(result: RuntimeToolResult, options: TraceRedactionOptions): RuntimeToolResult {
250
+ if (!shouldRedactToolOutputs(options)) {
251
+ return result;
252
+ }
253
+
254
+ if (result.type === "success") {
255
+ return {
256
+ ...result,
257
+ output: REDACTED_JSON_OBJECT
258
+ };
259
+ }
260
+
261
+ return {
262
+ ...result,
263
+ error: {
264
+ ...result.error,
265
+ ...(result.error.detail !== undefined ? { detail: REDACTED_JSON_OBJECT } : {})
266
+ }
267
+ };
268
+ }
269
+
270
+ function redactDecision(
271
+ decision: AgentDecision | readonly DelegateAgentDecision[],
272
+ options: TraceRedactionOptions
273
+ ): AgentDecision | readonly DelegateAgentDecision[] {
274
+ if (isDelegateDecisionArray(decision)) {
275
+ return decision.map((delegate) => redactDelegateDecision(delegate, options));
276
+ }
277
+ if (decision.type === "delegate") {
278
+ return redactDelegateDecision(decision, options);
279
+ }
280
+ return {
281
+ ...decision,
282
+ ...(shouldRedactOutputs(options)
283
+ ? {
284
+ rationale: replacement(options),
285
+ contribution: replacement(options)
286
+ }
287
+ : {})
288
+ };
289
+ }
290
+
291
+ function isDelegateDecisionArray(
292
+ decision: AgentDecision | readonly DelegateAgentDecision[]
293
+ ): decision is readonly DelegateAgentDecision[] {
294
+ return Array.isArray(decision);
295
+ }
296
+
297
+ function redactDelegateDecision(
298
+ decision: DelegateAgentDecision,
299
+ options: TraceRedactionOptions
300
+ ): DelegateAgentDecision {
301
+ return shouldRedactPrompts(options)
302
+ ? {
303
+ ...decision,
304
+ intent: replacement(options)
305
+ }
306
+ : decision;
307
+ }
308
+
309
+ function redactErrorDetail(detail: JsonObject, options: TraceRedactionOptions): JsonObject {
310
+ const failedDecision = detail["failedDecision"];
311
+ if (
312
+ !shouldRedactPrompts(options) ||
313
+ typeof failedDecision !== "object" ||
314
+ failedDecision === null ||
315
+ Array.isArray(failedDecision)
316
+ ) {
317
+ return detail;
318
+ }
319
+
320
+ return {
321
+ ...detail,
322
+ failedDecision: {
323
+ ...failedDecision,
324
+ intent: replacement(options)
325
+ }
326
+ };
327
+ }
328
+
329
+ function shouldRedactPrompts(options: TraceRedactionOptions): boolean {
330
+ return options.redactPrompts ?? true;
331
+ }
332
+
333
+ function shouldRedactOutputs(options: TraceRedactionOptions): boolean {
334
+ return options.redactOutputs ?? true;
335
+ }
336
+
337
+ function shouldRedactToolInputs(options: TraceRedactionOptions): boolean {
338
+ return options.redactToolInputs ?? true;
339
+ }
340
+
341
+ function shouldRedactToolOutputs(options: TraceRedactionOptions): boolean {
342
+ return options.redactToolOutputs ?? true;
343
+ }
344
+
345
+ function shouldRedactProviderResponses(options: TraceRedactionOptions): boolean {
346
+ return options.redactProviderResponses ?? true;
347
+ }
348
+
349
+ function shouldRedactEmbeddedChildTraces(options: TraceRedactionOptions): boolean {
350
+ return options.redactEmbeddedChildTraces ?? true;
351
+ }
352
+
353
+ function replacement(options: TraceRedactionOptions): string {
354
+ return options.replacementText ?? DEFAULT_REPLACEMENT_TEXT;
355
+ }
@@ -0,0 +1,81 @@
1
+ import type { JsonObject, JsonValue } from "../types.js";
2
+
3
+ const safeResponseHeaderNames = new Set([
4
+ "content-type",
5
+ "date",
6
+ "request-id",
7
+ "retry-after",
8
+ "x-ratelimit-limit",
9
+ "x-ratelimit-remaining",
10
+ "x-ratelimit-reset",
11
+ "x-request-id",
12
+ "x-stream-request-id"
13
+ ]);
14
+
15
+ const authLikeKeyPattern =
16
+ /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api-key|apikey|api_key|x-auth-token|x-access-token|x-goog-api-key)$/i;
17
+
18
+ export function sanitizeProviderJsonValue(value: unknown): JsonValue | undefined {
19
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
20
+ return value;
21
+ }
22
+
23
+ if (typeof value === "number") {
24
+ return Number.isFinite(value) ? value : null;
25
+ }
26
+
27
+ if (value instanceof Date) {
28
+ return value.toISOString();
29
+ }
30
+
31
+ if (Array.isArray(value)) {
32
+ return value.map((child) => sanitizeProviderJsonValue(child) ?? null);
33
+ }
34
+
35
+ if (isRecord(value)) {
36
+ const result: Record<string, JsonValue> = {};
37
+
38
+ for (const [key, child] of Object.entries(value)) {
39
+ if (isAuthLikeKey(key)) {
40
+ continue;
41
+ }
42
+ const jsonValue = sanitizeProviderJsonValue(child);
43
+ if (jsonValue !== undefined) {
44
+ result[key] = jsonValue;
45
+ }
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ return undefined;
52
+ }
53
+
54
+ export function sanitizeProviderResponseHeaders(headers: Headers | Record<string, unknown> | undefined): JsonObject | undefined {
55
+ if (headers === undefined) {
56
+ return undefined;
57
+ }
58
+
59
+ const result: Record<string, JsonValue> = {};
60
+ const entries =
61
+ typeof Headers !== "undefined" && headers instanceof Headers
62
+ ? Array.from(headers.entries())
63
+ : Object.entries(headers).map(([key, value]) => [key, String(value)] as const);
64
+
65
+ for (const [key, value] of entries) {
66
+ const normalizedKey = key.toLowerCase();
67
+ if (safeResponseHeaderNames.has(normalizedKey) && value !== undefined) {
68
+ result[normalizedKey] = String(value);
69
+ }
70
+ }
71
+
72
+ return Object.keys(result).length > 0 ? result : undefined;
73
+ }
74
+
75
+ function isAuthLikeKey(key: string): boolean {
76
+ return authLikeKeyPattern.test(key);
77
+ }
78
+
79
+ function isRecord(value: unknown): value is Record<string, unknown> {
80
+ return value !== null && typeof value === "object" && !Array.isArray(value);
81
+ }
@@ -56,9 +56,12 @@ interface SharedRunOptions {
56
56
  readonly signal?: AbortSignal;
57
57
  readonly terminate?: TerminationCondition;
58
58
  readonly wrapUpHint?: DogpileOptions["wrapUpHint"];
59
+ readonly maxConcurrentAgentTurns?: number;
59
60
  readonly emit?: (event: RunEvent) => void;
60
61
  }
61
62
 
63
+ const DEFAULT_MAX_CONCURRENT_AGENT_TURNS = 4;
64
+
62
65
  export async function runShared(options: SharedRunOptions): Promise<RunResult> {
63
66
  const runId = createRunId();
64
67
  const events: RunEvent[] = [];
@@ -124,77 +127,88 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
124
127
 
125
128
  if (!stopIfNeeded()) {
126
129
  const providerCallSlots: ReplayTraceProviderCall[] = [];
127
- const turnResults = await Promise.all(
128
- activeAgents.map(async (agent, index) => {
129
- const turn = index + 1;
130
- const input = buildSharedInput(options.intent, sharedState, turn);
131
- const request: ModelRequest = {
132
- temperature: options.temperature,
133
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
134
- metadata: {
135
- runId,
136
- protocol: "shared",
137
- agentId: agent.id,
138
- role: agent.role,
139
- tier: options.tier,
140
- turn,
141
- ...toolAvailability
142
- },
143
- messages: wrapUpHint.inject(
144
- [
145
- {
146
- role: "system",
147
- content: buildSystemPrompt(agent)
130
+ const fanout = createFanoutAbortController(options.signal);
131
+ const turnResults = await (async () => {
132
+ try {
133
+ return await mapWithConcurrency(
134
+ activeAgents,
135
+ options.maxConcurrentAgentTurns ?? DEFAULT_MAX_CONCURRENT_AGENT_TURNS,
136
+ fanout,
137
+ async (agent, index) => {
138
+ throwIfAborted(fanout.signal, options.model.id);
139
+ const turn = index + 1;
140
+ const input = buildSharedInput(options.intent, sharedState, turn);
141
+ const request: ModelRequest = {
142
+ temperature: options.temperature,
143
+ signal: fanout.signal,
144
+ metadata: {
145
+ runId,
146
+ protocol: "shared",
147
+ agentId: agent.id,
148
+ role: agent.role,
149
+ tier: options.tier,
150
+ turn,
151
+ ...toolAvailability
148
152
  },
149
- {
150
- role: "user",
151
- content: input
152
- }
153
- ],
154
- {
153
+ messages: wrapUpHint.inject(
154
+ [
155
+ {
156
+ role: "system",
157
+ content: buildSystemPrompt(agent)
158
+ },
159
+ {
160
+ role: "user",
161
+ content: input
162
+ }
163
+ ],
164
+ {
165
+ runId,
166
+ protocol: "shared",
167
+ cost: totalCost,
168
+ events,
169
+ transcript,
170
+ iteration: transcript.length,
171
+ elapsedMs: elapsedMs(startedAtMs)
172
+ }
173
+ )
174
+ };
175
+ const response = await generateModelTurn({
176
+ model: options.model,
177
+ request,
155
178
  runId,
156
- protocol: "shared",
157
- cost: totalCost,
158
- events,
159
- transcript,
160
- iteration: transcript.length,
161
- elapsedMs: elapsedMs(startedAtMs)
162
- }
163
- )
164
- };
165
- const response = await generateModelTurn({
166
- model: options.model,
167
- request,
168
- runId,
169
- agent,
170
- input,
171
- emit,
172
- callId: providerCallIdFor(runId, providerCalls.length + index + 1),
173
- onProviderCall(call): void {
174
- providerCallSlots[index] = call;
179
+ agent,
180
+ input,
181
+ emit,
182
+ callId: providerCallIdFor(runId, providerCalls.length + index + 1),
183
+ onProviderCall(call): void {
184
+ providerCallSlots[index] = call;
185
+ }
186
+ });
187
+ const decision = parseAgentDecision(response.text);
188
+ const toolCalls = await executeModelResponseToolRequests({
189
+ response,
190
+ executor: toolExecutor,
191
+ agentId: agent.id,
192
+ role: agent.role,
193
+ turn
194
+ });
195
+ throwIfAborted(fanout.signal, options.model.id);
196
+
197
+ return {
198
+ agent,
199
+ turn,
200
+ input,
201
+ response,
202
+ decision,
203
+ toolCalls,
204
+ turnCost: responseCost(response)
205
+ };
175
206
  }
176
- });
177
- const decision = parseAgentDecision(response.text);
178
- const toolCalls = await executeModelResponseToolRequests({
179
- response,
180
- executor: toolExecutor,
181
- agentId: agent.id,
182
- role: agent.role,
183
- turn
184
- });
185
- throwIfAborted(options.signal, options.model.id);
186
-
187
- return {
188
- agent,
189
- turn,
190
- input,
191
- response,
192
- decision,
193
- toolCalls,
194
- turnCost: responseCost(response)
195
- };
196
- })
197
- );
207
+ );
208
+ } finally {
209
+ fanout.cleanup();
210
+ }
211
+ })();
198
212
  providerCalls.push(...providerCallSlots.filter((call): call is ReplayTraceProviderCall => call !== undefined));
199
213
 
200
214
  for (const result of turnResults) {
@@ -379,3 +393,73 @@ function responseCost(response: ModelResponse): CostSummary {
379
393
  totalTokens: response.usage?.totalTokens ?? 0
380
394
  };
381
395
  }
396
+
397
+ interface FanoutAbortController {
398
+ readonly signal: AbortSignal;
399
+ abort(reason: unknown): void;
400
+ cleanup(): void;
401
+ }
402
+
403
+ function createFanoutAbortController(parentSignal: AbortSignal | undefined): FanoutAbortController {
404
+ const controller = new AbortController();
405
+ let removeParentListener = (): void => {};
406
+
407
+ if (parentSignal?.aborted) {
408
+ controller.abort(parentSignal.reason);
409
+ } else if (parentSignal !== undefined) {
410
+ const abortFromParent = (): void => {
411
+ controller.abort(parentSignal.reason);
412
+ };
413
+ parentSignal.addEventListener("abort", abortFromParent, { once: true });
414
+ removeParentListener = (): void => {
415
+ parentSignal.removeEventListener("abort", abortFromParent);
416
+ };
417
+ }
418
+
419
+ return {
420
+ signal: controller.signal,
421
+ abort(reason: unknown): void {
422
+ if (!controller.signal.aborted) {
423
+ controller.abort(reason);
424
+ }
425
+ },
426
+ cleanup(): void {
427
+ removeParentListener();
428
+ }
429
+ };
430
+ }
431
+
432
+ async function mapWithConcurrency<T, R>(
433
+ items: readonly T[],
434
+ maxConcurrent: number,
435
+ fanout: FanoutAbortController,
436
+ mapper: (item: T, index: number) => Promise<R>
437
+ ): Promise<R[]> {
438
+ if (items.length === 0) {
439
+ return [];
440
+ }
441
+
442
+ const results: R[] = new Array(items.length);
443
+ let nextIndex = 0;
444
+ let firstError: unknown;
445
+ const workerCount = Math.min(maxConcurrent, items.length);
446
+
447
+ await Promise.all(Array.from({ length: workerCount }, async () => {
448
+ while (nextIndex < items.length && firstError === undefined) {
449
+ const index = nextIndex;
450
+ nextIndex += 1;
451
+ try {
452
+ results[index] = await mapper(items[index]!, index);
453
+ } catch (error) {
454
+ firstError ??= error;
455
+ fanout.abort(error);
456
+ }
457
+ }
458
+ }));
459
+
460
+ if (firstError !== undefined) {
461
+ throw firstError;
462
+ }
463
+
464
+ return results;
465
+ }