@dogpile/sdk 0.4.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.
- package/CHANGELOG.md +92 -0
- package/dist/browser/index.js +4156 -4611
- package/dist/browser/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +6 -1
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/runtime/audit.d.ts +42 -0
- package/dist/runtime/audit.d.ts.map +1 -0
- package/dist/runtime/audit.js +73 -0
- package/dist/runtime/audit.js.map +1 -0
- package/dist/runtime/broadcast.d.ts +1 -0
- package/dist/runtime/broadcast.d.ts.map +1 -1
- package/dist/runtime/broadcast.js +171 -105
- package/dist/runtime/broadcast.js.map +1 -1
- package/dist/runtime/coordinator.d.ts +9 -2
- package/dist/runtime/coordinator.d.ts.map +1 -1
- package/dist/runtime/coordinator.js +164 -78
- package/dist/runtime/coordinator.js.map +1 -1
- package/dist/runtime/defaults.d.ts.map +1 -1
- package/dist/runtime/defaults.js +14 -5
- package/dist/runtime/defaults.js.map +1 -1
- package/dist/runtime/engine.d.ts +17 -4
- package/dist/runtime/engine.d.ts.map +1 -1
- package/dist/runtime/engine.js +577 -52
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/health.d.ts +51 -0
- package/dist/runtime/health.d.ts.map +1 -0
- package/dist/runtime/health.js +85 -0
- package/dist/runtime/health.js.map +1 -0
- package/dist/runtime/introspection.d.ts +96 -0
- package/dist/runtime/introspection.d.ts.map +1 -0
- package/dist/runtime/introspection.js +31 -0
- package/dist/runtime/introspection.js.map +1 -0
- package/dist/runtime/metrics.d.ts +44 -0
- package/dist/runtime/metrics.d.ts.map +1 -0
- package/dist/runtime/metrics.js +12 -0
- package/dist/runtime/metrics.js.map +1 -0
- package/dist/runtime/model.d.ts.map +1 -1
- package/dist/runtime/model.js +40 -10
- package/dist/runtime/model.js.map +1 -1
- package/dist/runtime/provenance.d.ts +25 -0
- package/dist/runtime/provenance.d.ts.map +1 -0
- package/dist/runtime/provenance.js +13 -0
- package/dist/runtime/provenance.js.map +1 -0
- package/dist/runtime/redaction.d.ts +13 -0
- package/dist/runtime/redaction.d.ts.map +1 -0
- package/dist/runtime/redaction.js +278 -0
- package/dist/runtime/redaction.js.map +1 -0
- package/dist/runtime/sanitization.d.ts +4 -0
- package/dist/runtime/sanitization.d.ts.map +1 -0
- package/dist/runtime/sanitization.js +63 -0
- package/dist/runtime/sanitization.js.map +1 -0
- package/dist/runtime/sequential.d.ts.map +1 -1
- package/dist/runtime/sequential.js +39 -36
- package/dist/runtime/sequential.js.map +1 -1
- package/dist/runtime/shared.d.ts +1 -0
- package/dist/runtime/shared.d.ts.map +1 -1
- package/dist/runtime/shared.js +167 -101
- package/dist/runtime/shared.js.map +1 -1
- package/dist/runtime/tools/built-in.d.ts +2 -0
- package/dist/runtime/tools/built-in.d.ts.map +1 -1
- package/dist/runtime/tools/built-in.js +153 -15
- package/dist/runtime/tools/built-in.js.map +1 -1
- package/dist/runtime/tools.d.ts.map +1 -1
- package/dist/runtime/tools.js +29 -7
- package/dist/runtime/tools.js.map +1 -1
- package/dist/runtime/tracing.d.ts +31 -0
- package/dist/runtime/tracing.d.ts.map +1 -0
- package/dist/runtime/tracing.js +18 -0
- package/dist/runtime/tracing.js.map +1 -0
- package/dist/runtime/validation.d.ts.map +1 -1
- package/dist/runtime/validation.js +3 -0
- package/dist/runtime/validation.js.map +1 -1
- package/dist/types/events.d.ts +13 -7
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/replay.d.ts +5 -1
- package/dist/types/replay.d.ts.map +1 -1
- package/dist/types.d.ts +144 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +46 -1
- package/src/index.ts +5 -0
- package/src/providers/openai-compatible.ts +6 -1
- package/src/runtime/audit.ts +121 -0
- package/src/runtime/broadcast.ts +195 -108
- package/src/runtime/coordinator.ts +197 -86
- package/src/runtime/defaults.ts +15 -5
- package/src/runtime/engine.ts +725 -58
- package/src/runtime/health.ts +136 -0
- package/src/runtime/introspection.ts +122 -0
- package/src/runtime/metrics.ts +45 -0
- package/src/runtime/model.ts +44 -9
- package/src/runtime/provenance.ts +43 -0
- package/src/runtime/redaction.ts +355 -0
- package/src/runtime/sanitization.ts +81 -0
- package/src/runtime/sequential.ts +40 -37
- package/src/runtime/shared.ts +191 -104
- package/src/runtime/tools/built-in.ts +168 -15
- package/src/runtime/tools.ts +39 -8
- package/src/runtime/tracing.ts +35 -0
- package/src/runtime/validation.ts +3 -0
- package/src/types/events.ts +13 -7
- package/src/types/replay.ts +5 -1
- package/src/types.ts +152 -1
package/src/runtime/shared.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
TerminationCondition,
|
|
17
17
|
TerminationStopRecord,
|
|
18
18
|
Tier,
|
|
19
|
+
Trace,
|
|
19
20
|
TranscriptEntry
|
|
20
21
|
} from "../types.js";
|
|
21
22
|
import { createRunId, elapsedMs, nowMs, providerCallIdFor } from "./ids.js";
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
createTranscriptLink,
|
|
35
36
|
emptyCost
|
|
36
37
|
} from "./defaults.js";
|
|
38
|
+
import { computeHealth, DEFAULT_HEALTH_THRESHOLDS } from "./health.js";
|
|
37
39
|
import { throwIfAborted } from "./cancellation.js";
|
|
38
40
|
import { parseAgentDecision } from "./decisions.js";
|
|
39
41
|
import { generateModelTurn } from "./model.js";
|
|
@@ -54,9 +56,12 @@ interface SharedRunOptions {
|
|
|
54
56
|
readonly signal?: AbortSignal;
|
|
55
57
|
readonly terminate?: TerminationCondition;
|
|
56
58
|
readonly wrapUpHint?: DogpileOptions["wrapUpHint"];
|
|
59
|
+
readonly maxConcurrentAgentTurns?: number;
|
|
57
60
|
readonly emit?: (event: RunEvent) => void;
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
const DEFAULT_MAX_CONCURRENT_AGENT_TURNS = 4;
|
|
64
|
+
|
|
60
65
|
export async function runShared(options: SharedRunOptions): Promise<RunResult> {
|
|
61
66
|
const runId = createRunId();
|
|
62
67
|
const events: RunEvent[] = [];
|
|
@@ -122,77 +127,88 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
|
|
|
122
127
|
|
|
123
128
|
if (!stopIfNeeded()) {
|
|
124
129
|
const providerCallSlots: ReplayTraceProviderCall[] = [];
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
152
|
},
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
153
178
|
runId,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
};
|
|
173
206
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
agentId: agent.id,
|
|
180
|
-
role: agent.role,
|
|
181
|
-
turn
|
|
182
|
-
});
|
|
183
|
-
throwIfAborted(options.signal, options.model.id);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
agent,
|
|
187
|
-
turn,
|
|
188
|
-
input,
|
|
189
|
-
response,
|
|
190
|
-
decision,
|
|
191
|
-
toolCalls,
|
|
192
|
-
turnCost: responseCost(response)
|
|
193
|
-
};
|
|
194
|
-
})
|
|
195
|
-
);
|
|
207
|
+
);
|
|
208
|
+
} finally {
|
|
209
|
+
fanout.cleanup();
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
196
212
|
providerCalls.push(...providerCallSlots.filter((call): call is ReplayTraceProviderCall => call !== undefined));
|
|
197
213
|
|
|
198
214
|
for (const result of turnResults) {
|
|
@@ -242,45 +258,46 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
|
|
|
242
258
|
transcriptEntryCount: transcript.length
|
|
243
259
|
});
|
|
244
260
|
const finalEvent = events.at(-1);
|
|
261
|
+
const trace: Trace = {
|
|
262
|
+
schemaVersion: "1.0",
|
|
263
|
+
runId,
|
|
264
|
+
protocol: "shared",
|
|
265
|
+
tier: options.tier,
|
|
266
|
+
modelProviderId: options.model.id,
|
|
267
|
+
agentsUsed: activeAgents,
|
|
268
|
+
inputs: createReplayTraceRunInputs({
|
|
269
|
+
intent: options.intent,
|
|
270
|
+
protocol: options.protocol,
|
|
271
|
+
tier: options.tier,
|
|
272
|
+
modelProviderId: options.model.id,
|
|
273
|
+
agents: activeAgents,
|
|
274
|
+
temperature: options.temperature
|
|
275
|
+
}),
|
|
276
|
+
budget: createReplayTraceBudget({
|
|
277
|
+
tier: options.tier,
|
|
278
|
+
...(options.budget ? { caps: options.budget } : {}),
|
|
279
|
+
...(options.terminate ? { termination: options.terminate } : {})
|
|
280
|
+
}),
|
|
281
|
+
budgetStateChanges: createReplayTraceBudgetStateChanges(events),
|
|
282
|
+
seed: createReplayTraceSeed(options.seed),
|
|
283
|
+
protocolDecisions,
|
|
284
|
+
providerCalls,
|
|
285
|
+
finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
|
|
286
|
+
type: "final",
|
|
287
|
+
runId,
|
|
288
|
+
at: "",
|
|
289
|
+
output,
|
|
290
|
+
cost: totalCost,
|
|
291
|
+
transcript: createTranscriptLink(transcript)
|
|
292
|
+
}),
|
|
293
|
+
events,
|
|
294
|
+
transcript
|
|
295
|
+
};
|
|
245
296
|
|
|
246
297
|
return {
|
|
247
298
|
output,
|
|
248
299
|
eventLog: createRunEventLog(runId, "shared", events),
|
|
249
|
-
trace
|
|
250
|
-
schemaVersion: "1.0",
|
|
251
|
-
runId,
|
|
252
|
-
protocol: "shared",
|
|
253
|
-
tier: options.tier,
|
|
254
|
-
modelProviderId: options.model.id,
|
|
255
|
-
agentsUsed: activeAgents,
|
|
256
|
-
inputs: createReplayTraceRunInputs({
|
|
257
|
-
intent: options.intent,
|
|
258
|
-
protocol: options.protocol,
|
|
259
|
-
tier: options.tier,
|
|
260
|
-
modelProviderId: options.model.id,
|
|
261
|
-
agents: activeAgents,
|
|
262
|
-
temperature: options.temperature
|
|
263
|
-
}),
|
|
264
|
-
budget: createReplayTraceBudget({
|
|
265
|
-
tier: options.tier,
|
|
266
|
-
...(options.budget ? { caps: options.budget } : {}),
|
|
267
|
-
...(options.terminate ? { termination: options.terminate } : {})
|
|
268
|
-
}),
|
|
269
|
-
budgetStateChanges: createReplayTraceBudgetStateChanges(events),
|
|
270
|
-
seed: createReplayTraceSeed(options.seed),
|
|
271
|
-
protocolDecisions,
|
|
272
|
-
providerCalls,
|
|
273
|
-
finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
|
|
274
|
-
type: "final",
|
|
275
|
-
runId,
|
|
276
|
-
at: "",
|
|
277
|
-
output,
|
|
278
|
-
cost: totalCost,
|
|
279
|
-
transcript: createTranscriptLink(transcript)
|
|
280
|
-
}),
|
|
281
|
-
events,
|
|
282
|
-
transcript
|
|
283
|
-
},
|
|
300
|
+
trace,
|
|
284
301
|
transcript,
|
|
285
302
|
usage: createRunUsage(totalCost),
|
|
286
303
|
metadata: createRunMetadata({
|
|
@@ -298,7 +315,8 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
|
|
|
298
315
|
cost: totalCost,
|
|
299
316
|
events
|
|
300
317
|
}),
|
|
301
|
-
cost: totalCost
|
|
318
|
+
cost: totalCost,
|
|
319
|
+
health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
|
|
302
320
|
};
|
|
303
321
|
|
|
304
322
|
function stopIfNeeded(): boolean {
|
|
@@ -376,3 +394,72 @@ function responseCost(response: ModelResponse): CostSummary {
|
|
|
376
394
|
};
|
|
377
395
|
}
|
|
378
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
|
+
}
|
|
@@ -60,6 +60,8 @@ export interface WebSearchToolAdapterOptions {
|
|
|
60
60
|
readonly fetch?: WebSearchFetch;
|
|
61
61
|
readonly headers?: HeadersInit;
|
|
62
62
|
readonly defaultMaxResults?: number;
|
|
63
|
+
readonly allowHosts?: readonly string[];
|
|
64
|
+
readonly allowPrivateNetwork?: boolean;
|
|
63
65
|
readonly identity?: BuiltInDogpileToolIdentityOptions;
|
|
64
66
|
readonly permissions?: readonly RuntimeToolPermission[];
|
|
65
67
|
readonly buildRequest?: WebSearchFetchRequestBuilder;
|
|
@@ -271,11 +273,15 @@ export function createWebSearchToolAdapter(
|
|
|
271
273
|
options: WebSearchToolAdapterOptions
|
|
272
274
|
): RuntimeToolAdapterContract<WebSearchToolInput, WebSearchToolOutput> {
|
|
273
275
|
const identity = mergeIdentity(webSearchIdentity, options.identity);
|
|
276
|
+
const endpointUrl = new URL(String(options.endpoint));
|
|
277
|
+
const allowedHosts = normalizeAllowedHosts(options.allowHosts ?? [endpointUrl.hostname]);
|
|
278
|
+
const allowPrivateNetwork = options.allowPrivateNetwork ?? false;
|
|
279
|
+
const permissions = options.permissions ?? webSearchPermissionsFor(allowedHosts, allowPrivateNetwork);
|
|
274
280
|
|
|
275
281
|
return normalizeBuiltInDogpileTool({
|
|
276
282
|
name: "webSearch",
|
|
277
283
|
...(options.identity ? { identity: options.identity } : {}),
|
|
278
|
-
|
|
284
|
+
permissions,
|
|
279
285
|
async execute(input, context): Promise<RuntimeToolResult<WebSearchToolOutput>> {
|
|
280
286
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
281
287
|
|
|
@@ -295,6 +301,15 @@ export function createWebSearchToolAdapter(
|
|
|
295
301
|
const request = options.buildRequest
|
|
296
302
|
? options.buildRequest(input, context)
|
|
297
303
|
: defaultWebSearchRequest(options, input, context);
|
|
304
|
+
const policyError = validateWebSearchRequestPolicy(request.url, allowedHosts, allowPrivateNetwork);
|
|
305
|
+
if (policyError !== undefined) {
|
|
306
|
+
return {
|
|
307
|
+
type: "error",
|
|
308
|
+
toolCallId: context.toolCallId,
|
|
309
|
+
tool: identity,
|
|
310
|
+
error: policyError
|
|
311
|
+
};
|
|
312
|
+
}
|
|
298
313
|
const response = await fetchImplementation(request.url, {
|
|
299
314
|
...request.init,
|
|
300
315
|
...(context.abortSignal ? { signal: context.abortSignal } : {})
|
|
@@ -616,10 +631,19 @@ async function executeSandboxWithPolicy(
|
|
|
616
631
|
} satisfies RuntimeToolAdapterError;
|
|
617
632
|
}
|
|
618
633
|
|
|
619
|
-
const
|
|
634
|
+
const policy = createToolPolicyAbortController(context.abortSignal);
|
|
635
|
+
const executionContext: RuntimeToolExecutionContext = {
|
|
636
|
+
...context,
|
|
637
|
+
abortSignal: policy.signal
|
|
638
|
+
};
|
|
639
|
+
const execution = Promise.resolve().then(() => execute(input, executionContext));
|
|
620
640
|
|
|
621
641
|
if (timeoutMs === undefined && context.abortSignal === undefined) {
|
|
622
|
-
|
|
642
|
+
try {
|
|
643
|
+
return await execution;
|
|
644
|
+
} finally {
|
|
645
|
+
policy.cleanup();
|
|
646
|
+
}
|
|
623
647
|
}
|
|
624
648
|
|
|
625
649
|
return await new Promise<CodeExecToolOutput>((resolve, reject) => {
|
|
@@ -627,31 +651,33 @@ async function executeSandboxWithPolicy(
|
|
|
627
651
|
|
|
628
652
|
const cleanup = (): void => {
|
|
629
653
|
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
630
|
-
|
|
654
|
+
policy.signal.removeEventListener("abort", abortHandler);
|
|
655
|
+
policy.cleanup();
|
|
631
656
|
};
|
|
632
657
|
|
|
633
658
|
const abortHandler = (): void => {
|
|
634
659
|
cleanup();
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
660
|
+
const reason = policy.signal.reason;
|
|
661
|
+
reject(isRuntimeToolAdapterError(reason)
|
|
662
|
+
? reason
|
|
663
|
+
: {
|
|
664
|
+
code: "aborted",
|
|
665
|
+
message: "Code execution was aborted.",
|
|
666
|
+
retryable: true
|
|
667
|
+
} satisfies RuntimeToolAdapterError);
|
|
640
668
|
};
|
|
641
669
|
|
|
642
|
-
|
|
643
|
-
context.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
644
|
-
}
|
|
670
|
+
policy.signal.addEventListener("abort", abortHandler, { once: true });
|
|
645
671
|
|
|
646
672
|
if (timeoutMs !== undefined) {
|
|
647
673
|
timeoutId = setTimeout(() => {
|
|
648
|
-
|
|
649
|
-
reject({
|
|
674
|
+
const error = {
|
|
650
675
|
code: "timeout",
|
|
651
676
|
message: `Code execution exceeded timeout of ${timeoutMs}ms.`,
|
|
652
677
|
retryable: true,
|
|
653
678
|
detail: { timeoutMs }
|
|
654
|
-
} satisfies RuntimeToolAdapterError
|
|
679
|
+
} satisfies RuntimeToolAdapterError;
|
|
680
|
+
policy.abort(error);
|
|
655
681
|
}, timeoutMs);
|
|
656
682
|
}
|
|
657
683
|
|
|
@@ -662,6 +688,133 @@ async function executeSandboxWithPolicy(
|
|
|
662
688
|
});
|
|
663
689
|
}
|
|
664
690
|
|
|
691
|
+
function webSearchPermissionsFor(
|
|
692
|
+
allowedHosts: ReadonlySet<string>,
|
|
693
|
+
allowPrivateNetwork: boolean
|
|
694
|
+
): readonly RuntimeToolPermission[] {
|
|
695
|
+
return [
|
|
696
|
+
{
|
|
697
|
+
kind: "network",
|
|
698
|
+
allowHosts: Array.from(allowedHosts).sort(),
|
|
699
|
+
allowPrivateNetwork
|
|
700
|
+
}
|
|
701
|
+
];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function validateWebSearchRequestPolicy(
|
|
705
|
+
requestUrl: string | URL,
|
|
706
|
+
allowedHosts: ReadonlySet<string>,
|
|
707
|
+
allowPrivateNetwork: boolean
|
|
708
|
+
): RuntimeToolAdapterError | undefined {
|
|
709
|
+
const url = new URL(String(requestUrl));
|
|
710
|
+
const host = normalizeHost(url.hostname);
|
|
711
|
+
|
|
712
|
+
if (!allowedHosts.has(host)) {
|
|
713
|
+
return {
|
|
714
|
+
code: "permission-denied",
|
|
715
|
+
message: `webSearch request host ${url.hostname} is not allowed.`,
|
|
716
|
+
retryable: false,
|
|
717
|
+
detail: {
|
|
718
|
+
reason: "host-not-allowed",
|
|
719
|
+
host: url.hostname,
|
|
720
|
+
allowedHosts: Array.from(allowedHosts).sort()
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!allowPrivateNetwork && classifyNetworkHost(host) === "private") {
|
|
726
|
+
return {
|
|
727
|
+
code: "permission-denied",
|
|
728
|
+
message: `webSearch request host ${url.hostname} is private-network and is not allowed.`,
|
|
729
|
+
retryable: false,
|
|
730
|
+
detail: {
|
|
731
|
+
reason: "private-network-not-allowed",
|
|
732
|
+
host: url.hostname
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function normalizeAllowedHosts(hosts: readonly string[]): ReadonlySet<string> {
|
|
741
|
+
return new Set(hosts.map(normalizeHost));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeHost(host: string): string {
|
|
745
|
+
return host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function classifyNetworkHost(host: string): "private" | "public" {
|
|
749
|
+
const mappedIpv4 = ipv4MappedToDottedQuad(host);
|
|
750
|
+
if (mappedIpv4 !== undefined) {
|
|
751
|
+
return classifyNetworkHost(mappedIpv4);
|
|
752
|
+
}
|
|
753
|
+
if (host === "localhost") return "private";
|
|
754
|
+
if (host.endsWith(".local")) return "private";
|
|
755
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(host)) return "private";
|
|
756
|
+
if (/^10(?:\.\d{1,3}){3}$/.test(host)) return "private";
|
|
757
|
+
if (/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/.test(host)) return "private";
|
|
758
|
+
if (/^192\.168(?:\.\d{1,3}){2}$/.test(host)) return "private";
|
|
759
|
+
if (/^169\.254(?:\.\d{1,3}){2}$/.test(host)) return "private";
|
|
760
|
+
if (host === "::1") return "private";
|
|
761
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host)) return "private";
|
|
762
|
+
if (/^fe[89ab][0-9a-f]?:/.test(host)) return "private";
|
|
763
|
+
return "public";
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function ipv4MappedToDottedQuad(host: string): string | undefined {
|
|
767
|
+
const match = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
768
|
+
if (match === null) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
const high = Number.parseInt(match[1] ?? "", 16);
|
|
772
|
+
const low = Number.parseInt(match[2] ?? "", 16);
|
|
773
|
+
if (!Number.isFinite(high) || !Number.isFinite(low)) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
interface ToolPolicyAbortController {
|
|
780
|
+
readonly signal: AbortSignal;
|
|
781
|
+
abort(reason: unknown): void;
|
|
782
|
+
cleanup(): void;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function createToolPolicyAbortController(parentSignal: AbortSignal | undefined): ToolPolicyAbortController {
|
|
786
|
+
const controller = new AbortController();
|
|
787
|
+
let removeParentListener = (): void => {};
|
|
788
|
+
|
|
789
|
+
if (parentSignal?.aborted) {
|
|
790
|
+
controller.abort(parentSignal.reason);
|
|
791
|
+
} else if (parentSignal !== undefined) {
|
|
792
|
+
const abortFromParent = (): void => {
|
|
793
|
+
controller.abort(parentSignal.reason ?? {
|
|
794
|
+
code: "aborted",
|
|
795
|
+
message: "Code execution was aborted.",
|
|
796
|
+
retryable: true
|
|
797
|
+
} satisfies RuntimeToolAdapterError);
|
|
798
|
+
};
|
|
799
|
+
parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
800
|
+
removeParentListener = (): void => {
|
|
801
|
+
parentSignal.removeEventListener("abort", abortFromParent);
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
signal: controller.signal,
|
|
807
|
+
abort(reason: unknown): void {
|
|
808
|
+
if (!controller.signal.aborted) {
|
|
809
|
+
controller.abort(reason);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
cleanup(): void {
|
|
813
|
+
removeParentListener();
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
665
818
|
function normalizeWebSearchResult(value: unknown): WebSearchToolResult {
|
|
666
819
|
if (!isJsonObject(value)) {
|
|
667
820
|
throw {
|
package/src/runtime/tools.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
import { validateRuntimeToolRegistrations } from "./validation.js";
|
|
17
17
|
import { normalizeRuntimeToolAdapterError } from "./tools/built-in.js";
|
|
18
18
|
|
|
19
|
+
const DEFAULT_MAX_CONCURRENT_TOOL_CALLS = 4;
|
|
20
|
+
|
|
19
21
|
// Re-export the public surface from the split modules so the
|
|
20
22
|
// `@dogpile/sdk/runtime/tools` subpath stays stable.
|
|
21
23
|
export {
|
|
@@ -98,12 +100,18 @@ export interface RuntimeToolExecutorOptions {
|
|
|
98
100
|
export function createRuntimeToolExecutor(options: RuntimeToolExecutorOptions): RuntimeToolExecutor {
|
|
99
101
|
validateRuntimeToolRegistrations(options.tools);
|
|
100
102
|
const tools = Array.from(options.tools);
|
|
103
|
+
const toolsById = new Map<string, RuntimeTool<JsonObject, JsonValue>>();
|
|
104
|
+
for (const tool of tools) {
|
|
105
|
+
if (!toolsById.has(tool.identity.id)) {
|
|
106
|
+
toolsById.set(tool.identity.id, tool);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
101
109
|
let callCount = 0;
|
|
102
110
|
|
|
103
111
|
return {
|
|
104
112
|
tools,
|
|
105
113
|
async execute(request: RuntimeToolExecutionRequest): Promise<RuntimeToolResult> {
|
|
106
|
-
const tool =
|
|
114
|
+
const tool = toolsById.get(request.toolId);
|
|
107
115
|
const identity = tool?.identity ?? {
|
|
108
116
|
id: request.toolId,
|
|
109
117
|
name: request.toolId
|
|
@@ -182,9 +190,9 @@ export async function executeModelResponseToolRequests(options: {
|
|
|
182
190
|
readonly turn: number;
|
|
183
191
|
readonly metadata?: JsonObject;
|
|
184
192
|
}): Promise<readonly TranscriptToolCall[]> {
|
|
185
|
-
const
|
|
193
|
+
const requests = options.response.toolRequests ?? [];
|
|
186
194
|
|
|
187
|
-
|
|
195
|
+
return mapWithConcurrency(requests, DEFAULT_MAX_CONCURRENT_TOOL_CALLS, async (request) => {
|
|
188
196
|
const result = await options.executor.execute({
|
|
189
197
|
...request,
|
|
190
198
|
agentId: request.agentId ?? options.agentId,
|
|
@@ -192,15 +200,14 @@ export async function executeModelResponseToolRequests(options: {
|
|
|
192
200
|
turn: request.turn ?? options.turn,
|
|
193
201
|
metadata: mergeToolMetadata(options.metadata, request.metadata)
|
|
194
202
|
});
|
|
195
|
-
|
|
203
|
+
|
|
204
|
+
return {
|
|
196
205
|
toolCallId: result.toolCallId,
|
|
197
206
|
tool: result.tool,
|
|
198
207
|
input: request.input,
|
|
199
208
|
result
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return toolCalls;
|
|
209
|
+
};
|
|
210
|
+
});
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
function runtimeToolIdentityManifest(identity: RuntimeToolIdentity): JsonObject {
|
|
@@ -321,3 +328,27 @@ function mergeToolMetadata(base: JsonObject | undefined, request: JsonObject | u
|
|
|
321
328
|
function defaultToolCallId(runId: string, callIndex: number): string {
|
|
322
329
|
return `${runId}:tool-${callIndex + 1}`;
|
|
323
330
|
}
|
|
331
|
+
|
|
332
|
+
async function mapWithConcurrency<T, R>(
|
|
333
|
+
items: readonly T[],
|
|
334
|
+
maxConcurrent: number,
|
|
335
|
+
mapper: (item: T, index: number) => Promise<R>
|
|
336
|
+
): Promise<R[]> {
|
|
337
|
+
if (items.length === 0) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const results: R[] = new Array(items.length);
|
|
342
|
+
let nextIndex = 0;
|
|
343
|
+
const workerCount = Math.min(maxConcurrent, items.length);
|
|
344
|
+
|
|
345
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
346
|
+
while (nextIndex < items.length) {
|
|
347
|
+
const index = nextIndex;
|
|
348
|
+
nextIndex += 1;
|
|
349
|
+
results[index] = await mapper(items[index]!, index);
|
|
350
|
+
}
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
return results;
|
|
354
|
+
}
|