@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
@@ -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
- ...(options.permissions ? { permissions: options.permissions } : {}),
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 execution = Promise.resolve().then(() => execute(input, context));
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
- return await execution;
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
- context.abortSignal?.removeEventListener("abort", abortHandler);
654
+ policy.signal.removeEventListener("abort", abortHandler);
655
+ policy.cleanup();
631
656
  };
632
657
 
633
658
  const abortHandler = (): void => {
634
659
  cleanup();
635
- reject({
636
- code: "aborted",
637
- message: "Code execution was aborted.",
638
- retryable: true
639
- } satisfies RuntimeToolAdapterError);
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
- if (context.abortSignal) {
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
- cleanup();
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 {
@@ -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 = tools.find((candidate) => candidate.identity.id === request.toolId);
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 toolCalls: TranscriptToolCall[] = [];
193
+ const requests = options.response.toolRequests ?? [];
186
194
 
187
- for (const request of options.response.toolRequests ?? []) {
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
- toolCalls.push({
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
+ }
@@ -73,6 +73,7 @@ export function validateDogpileOptions(options: DogpileOptions): void {
73
73
  validateOptionalAbortSignal(options.signal, "signal");
74
74
  validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
75
75
  validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
76
+ validateOptionalPositiveInteger(options.maxConcurrentAgentTurns, "maxConcurrentAgentTurns");
76
77
  validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
77
78
  validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
78
79
  }
@@ -91,6 +92,7 @@ export function validateRunCallOptions(options: unknown, path = "options"): void
91
92
  const record = requireRecord(options, path);
92
93
  validateOptionalNonNegativeInteger(record.maxDepth, `${path}.maxDepth`);
93
94
  validateOptionalPositiveInteger(record.maxConcurrentChildren, `${path}.maxConcurrentChildren`);
95
+ validateOptionalPositiveInteger(record.maxConcurrentAgentTurns, `${path}.maxConcurrentAgentTurns`);
94
96
  validateOptionalOnChildFailure(record.onChildFailure, `${path}.onChildFailure`);
95
97
  }
96
98
 
@@ -113,6 +115,7 @@ export function validateEngineOptions(options: EngineOptions): void {
113
115
  validateOptionalAbortSignal(options.signal, "signal");
114
116
  validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
115
117
  validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
118
+ validateOptionalPositiveInteger(options.maxConcurrentAgentTurns, "maxConcurrentAgentTurns");
116
119
  validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
117
120
  validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
118
121
  }
@@ -140,7 +140,7 @@ export interface ModelResponseEvent {
140
140
  * - `input`: prompt text visible to that agent for this turn.
141
141
  * - `chunkIndex`: zero-based chunk index within this model turn.
142
142
  * - `text`: text delta from the provider.
143
- * - `output`: accumulated output for this turn after applying the chunk.
143
+ * - `outputLength`: accumulated output character length after applying the chunk.
144
144
  */
145
145
  export interface ModelOutputChunkEvent {
146
146
  /** Discriminant for event rendering and exhaustive switches. */
@@ -161,8 +161,8 @@ export interface ModelOutputChunkEvent {
161
161
  readonly chunkIndex: number;
162
162
  /** Text delta produced by the model provider. */
163
163
  readonly text: string;
164
- /** Accumulated output for this turn after applying this chunk. */
165
- readonly output: string;
164
+ /** Accumulated output character length after applying this chunk. */
165
+ readonly outputLength: number;
166
166
  }
167
167
 
168
168
  /**
@@ -163,8 +163,10 @@ export interface ReplayTraceProtocolDecision {
163
163
  readonly contributionCount?: number;
164
164
  /** Prompt/input associated with turn decisions. */
165
165
  readonly input?: string;
166
- /** Output associated with turn or final decisions. */
166
+ /** Output associated with turn/final decisions, or a streaming text delta. */
167
167
  readonly output?: string;
168
+ /** Accumulated output character length for streaming output observations. */
169
+ readonly outputLength?: number;
168
170
  /** Cumulative cost visible at this decision point. */
169
171
  readonly cost?: CostSummary;
170
172
  /** Normalized budget stop reason for budget-stop decisions. */
package/src/types.ts CHANGED
@@ -1929,6 +1929,14 @@ export interface DogpileOptions extends BudgetCostTierOptions {
1929
1929
  * ceiling; the effective value is `min(engine, run ?? Infinity, decision ?? Infinity)`.
1930
1930
  */
1931
1931
  readonly maxConcurrentChildren?: number;
1932
+ /**
1933
+ * Maximum agent model turns that may execute in parallel for shared,
1934
+ * broadcast, and coordinator worker fan-out.
1935
+ *
1936
+ * Defaults to 4. Per-run values can only lower the engine ceiling.
1937
+ * This is independent from delegated child-run concurrency.
1938
+ */
1939
+ readonly maxConcurrentAgentTurns?: number;
1932
1940
  /**
1933
1941
  * Fallback timeout (milliseconds) applied to delegated sub-runs when neither
1934
1942
  * the parent's `budget.timeoutMs` nor the decision-level
@@ -2058,6 +2066,14 @@ export interface EngineOptions {
2058
2066
  * call sites via {@link RunCallOptions.maxConcurrentChildren}.
2059
2067
  */
2060
2068
  readonly maxConcurrentChildren?: number;
2069
+ /**
2070
+ * Maximum agent model turns that may execute in parallel for shared,
2071
+ * broadcast, and coordinator worker fan-out.
2072
+ *
2073
+ * Defaults to 4. Per-run lowering happens at `engine.run` / `engine.stream`
2074
+ * call sites via {@link RunCallOptions.maxConcurrentAgentTurns}.
2075
+ */
2076
+ readonly maxConcurrentAgentTurns?: number;
2061
2077
  /**
2062
2078
  * Fallback timeout (milliseconds) applied to delegated sub-runs when neither
2063
2079
  * the parent's `budget.timeoutMs` nor the decision-level
@@ -2097,6 +2113,10 @@ export interface RunCallOptions {
2097
2113
  * ceiling.
2098
2114
  */
2099
2115
  readonly maxConcurrentChildren?: number;
2116
+ /**
2117
+ * Per-run agent-turn fan-out ceiling. Cannot raise the engine's ceiling.
2118
+ */
2119
+ readonly maxConcurrentAgentTurns?: number;
2100
2120
  /** Per-run child-failure behavior. Overrides the engine default. */
2101
2121
  readonly onChildFailure?: OnChildFailureMode;
2102
2122
  }