deepline 0.3.289 → 0.3.291

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 (64) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +8 -6
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/runs/overview.ts +16 -6
  4. package/dist/bundling-sources/sdk/src/types.ts +21 -2
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +41 -11
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +317 -63
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +19 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +6 -5
  14. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/run-event-catalog.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +74 -33
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +73 -3
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +39 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +72 -9
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +2 -3
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +77 -10
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool-options.ts +145 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +17 -9
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -139
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +19 -5
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +176 -18
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +7 -5
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1404 -478
  32. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +55 -25
  33. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +97 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +34 -15
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +558 -57
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +10 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/test-admission-batching.ts +103 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
  40. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
  42. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +75 -28
  44. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  45. package/dist/cli/index.js +195 -582
  46. package/dist/cli/index.mjs +195 -582
  47. package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
  48. package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
  49. package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
  50. package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/index.d.ts +4 -4
  53. package/dist/index.js +14 -5
  54. package/dist/index.mjs +14 -5
  55. package/dist/install-integrity.json +7 -4
  56. package/dist/plays/bundle-play-file.d.mts +2 -2
  57. package/dist/plays/bundle-play-file.d.ts +2 -2
  58. package/dist/plays/run-request.d.mts +2 -2
  59. package/dist/plays/run-request.d.ts +2 -2
  60. package/dist/release.d.mts +1 -1
  61. package/dist/release.d.ts +1 -1
  62. package/dist/release.js +1 -1
  63. package/dist/release.mjs +1 -1
  64. package/package.json +1 -1
@@ -85,6 +85,8 @@ type RuntimeReceiptValueStorageInput = {
85
85
  inlineFields?: RuntimeReceiptValueInlineFields;
86
86
  inlineMaxBytes?: number;
87
87
  store?: RuntimeReceiptValueBlobStore;
88
+ /** Consult the durable writer fence only when this value needs a reference. */
89
+ referencesWritable?: () => Promise<boolean>;
88
90
  };
89
91
 
90
92
  export type PersistedRuntimeReceiptValue =
@@ -206,7 +208,10 @@ export async function persistRuntimeReceiptValue(
206
208
  input.inlineFields === undefined
207
209
  ? undefined
208
210
  : normalizeRuntimeReceiptValueInlineFields(input.inlineFields);
209
- if (body.byteLength <= inlineMaxBytes) {
211
+ if (
212
+ body.byteLength <= inlineMaxBytes ||
213
+ (input.referencesWritable && !(await input.referencesWritable()))
214
+ ) {
210
215
  return { kind: 'inline', outputJson: input.serialized };
211
216
  }
212
217
 
@@ -242,6 +247,7 @@ export function createRuntimeReceiptValuePersister(
242
247
  input: {
243
248
  store?: RuntimeReceiptValueBlobStore;
244
249
  inlineMaxBytes?: number;
250
+ referencesWritable?: () => Promise<boolean>;
245
251
  } = {},
246
252
  ): RuntimeReceiptValuePersister {
247
253
  const inlineMaxBytes =
@@ -278,6 +284,9 @@ export function createRuntimeReceiptValuePersister(
278
284
  ...value,
279
285
  inlineMaxBytes,
280
286
  ...(input.store ? { store: input.store } : {}),
287
+ ...(input.referencesWritable
288
+ ? { referencesWritable: input.referencesWritable }
289
+ : {}),
281
290
  }),
282
291
  };
283
292
  } catch (error) {
@@ -45,6 +45,8 @@ export type PlayVisualNodeProgress = {
45
45
  completedAt?: number | null;
46
46
  artifactTableNamespace?: string | null;
47
47
  nodeIo?: PlayDocflowNodeIoState;
48
+ /** Error reported by this step, distinct from a run stopping other work. */
49
+ error?: string;
48
50
  conditionCounts?: PlayConditionCounts;
49
51
  };
50
52
 
@@ -193,6 +195,9 @@ export function normalizePlayVisualNodeProgressMap(
193
195
  ...(startedAt !== undefined ? { startedAt } : {}),
194
196
  ...(completedAt !== undefined ? { completedAt } : {}),
195
197
  ...(message !== undefined ? { message } : {}),
198
+ ...(typeof rawProgress.error === 'string' && rawProgress.error.trim()
199
+ ? { error: rawProgress.error.trim() }
200
+ : {}),
196
201
  ...(artifactTableNamespace !== undefined
197
202
  ? { artifactTableNamespace }
198
203
  : {}),
@@ -305,7 +310,7 @@ export function applyVisualNodeProgress(input: {
305
310
  const completed = Number(progress.completed ?? 0);
306
311
  const total = Number(progress.total ?? 0);
307
312
  const progressStatus =
308
- nodeState.status === 'failed'
313
+ nodeState.status === 'failed' || progress.error || progress.nodeIo?.error
309
314
  ? 'failed'
310
315
  : Number.isFinite(total) && total > 0
311
316
  ? Number.isFinite(completed) && completed >= total
@@ -0,0 +1,103 @@
1
+ import {
2
+ defineBatchStrategyMap,
3
+ type BatchOperationStrategy,
4
+ } from './batching-types';
5
+
6
+ type Single = {
7
+ key: string;
8
+ simulated_delay_ms?: number;
9
+ overlap_group?: string;
10
+ };
11
+ type Result = {
12
+ key: string;
13
+ physical_id: string;
14
+ batch_size: number;
15
+ started_at_ms: number;
16
+ finished_at_ms: number;
17
+ };
18
+ type Bulk = {
19
+ items: Array<{ key: string }>;
20
+ simulated_delay_ms?: number;
21
+ overlap_group?: string;
22
+ };
23
+ const bucket = (input: Single) =>
24
+ JSON.stringify([input.simulated_delay_ms ?? 0, input.overlap_group ?? null]);
25
+
26
+ const strategy: BatchOperationStrategy<
27
+ Single,
28
+ Single | Bulk,
29
+ Result | { items: Result[] },
30
+ Result
31
+ > = {
32
+ sourceOperation: 'test_batching_single',
33
+ batchOperation: 'test_batching_bulk',
34
+ kind: 'identifier_batch',
35
+ maxBatchSize: 100,
36
+ bucketKeyPayloadFields: ['simulated_delay_ms', 'overlap_group'],
37
+ toBucketKey: bucket,
38
+ toItemKey: (input) => input.key,
39
+ canBatchWith: (left, right) => bucket(left) === bucket(right),
40
+ compile(inputs) {
41
+ const first = inputs[0];
42
+ if (
43
+ !first ||
44
+ inputs.length > 100 ||
45
+ inputs.some((input) => !input.key || bucket(input) !== bucket(first))
46
+ ) {
47
+ throw new Error('Synthetic batch needs 1–100 compatible keyed members.');
48
+ }
49
+ const keys = [...new Set(inputs.map((input) => input.key))];
50
+ return {
51
+ batchOperation:
52
+ keys.length === 1 ? 'test_batching_single' : 'test_batching_bulk',
53
+ batchPayload:
54
+ keys.length === 1
55
+ ? first
56
+ : {
57
+ items: keys.map((key) => ({ key })),
58
+ simulated_delay_ms: first.simulated_delay_ms,
59
+ ...(first.overlap_group
60
+ ? { overlap_group: first.overlap_group }
61
+ : {}),
62
+ },
63
+ items: inputs.map((payload) => ({ itemKey: payload.key, payload })),
64
+ };
65
+ },
66
+ splitResult(result, compiled) {
67
+ const rows =
68
+ compiled.batchOperation === 'test_batching_single'
69
+ ? [result as Result]
70
+ : (result as { items: Result[] }).items;
71
+ if (!Array.isArray(rows))
72
+ throw new Error('Synthetic batch omitted its result list.');
73
+ const byKey = new Map<string, Result>();
74
+ const expected = new Set(compiled.items.map((item) => item.itemKey));
75
+ for (const row of rows) {
76
+ if (
77
+ !row ||
78
+ !expected.has(row.key) ||
79
+ byKey.has(row.key) ||
80
+ typeof row.physical_id !== 'string' ||
81
+ row.physical_id.length === 0 ||
82
+ row.batch_size !== expected.size ||
83
+ !Number.isInteger(row.started_at_ms) ||
84
+ row.started_at_ms < 0 ||
85
+ !Number.isInteger(row.finished_at_ms) ||
86
+ row.finished_at_ms < row.started_at_ms
87
+ ) {
88
+ throw new Error(
89
+ 'Synthetic batch returned a malformed or unexpected member.',
90
+ );
91
+ }
92
+ byKey.set(row.key, row);
93
+ }
94
+ return compiled.items.map((item) => {
95
+ const row = byKey.get(item.itemKey);
96
+ if (!row) throw new Error('Synthetic batch omitted a member.');
97
+ return { itemKey: item.itemKey, result: row, rawResult: row };
98
+ });
99
+ },
100
+ };
101
+ export const testAdmissionBatchStrategies = defineBatchStrategyMap({
102
+ test_batching_single: strategy,
103
+ });
@@ -349,10 +349,14 @@ export function createRuntimeStepToolResultReceipts(
349
349
  // exists to cap how many stored payloads expand at once; holding one
350
350
  // of its slots across this gateway round trip only queued unrelated
351
351
  // rows behind this row's write, so the mutation runs outside it.
352
+ const storedResult = stampResultReceipt(
353
+ candidate.result,
354
+ record.resultReceiptKey,
355
+ );
352
356
  const persisted = await input.store.complete(
353
357
  record.resultReceiptKey,
354
358
  ownerRunId,
355
- serializeToolExecuteResult(candidate.result),
359
+ serializeToolExecuteResult(storedResult),
356
360
  COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
357
361
  );
358
362
  // Publication is immutable, but an independent miss keeps the
@@ -369,10 +373,10 @@ export function createRuntimeStepToolResultReceipts(
369
373
  }
370
374
  }
371
375
  await input.onStored?.({
372
- result: candidate.result,
376
+ result: storedResult,
373
377
  resultReceiptKey: record.resultReceiptKey,
374
378
  });
375
- return { kind: 'stored', result: candidate.result };
379
+ return { kind: 'stored', result: storedResult };
376
380
  } finally {
377
381
  await stopHeartbeat(record);
378
382
  await releaseExecutionLock(record);
@@ -394,6 +398,16 @@ export function createRuntimeStepToolResultReceipts(
394
398
  };
395
399
  }
396
400
 
401
+ function stampResultReceipt(
402
+ result: ToolExecuteResult,
403
+ resultReceiptKey: string,
404
+ ): ToolExecuteResult {
405
+ return deserializeToolExecuteResult({
406
+ ...serializeToolExecuteResult(result),
407
+ execution: { ...result._metadata.execution, resultReceiptKey },
408
+ });
409
+ }
410
+
397
411
  function completedResult(
398
412
  receipt: RuntimeStepReceipt | null,
399
413
  resultReceiptKey: string,
@@ -402,10 +416,13 @@ function completedResult(
402
416
  return null;
403
417
  }
404
418
  if (isSerializedToolExecuteResult(receipt.output)) {
405
- return deserializeToolExecuteResult(receipt.output);
419
+ return stampResultReceipt(
420
+ deserializeToolExecuteResult(receipt.output),
421
+ resultReceiptKey,
422
+ );
406
423
  }
407
424
  if (isToolExecuteResult(receipt.output)) {
408
- return receipt.output;
425
+ return stampResultReceipt(receipt.output, resultReceiptKey);
409
426
  }
410
427
  throw new Error(
411
428
  `Completed Tool Result Receipt ${resultReceiptKey} has no valid serialized ToolExecuteResult.`,
@@ -12,6 +12,41 @@ import {
12
12
  } from '../plays/tool-execution-error';
13
13
 
14
14
  const TOOL_HTTP_ERROR_BRAND = Symbol.for('deepline.tool-http-error.v1');
15
+ const callEvidenceByError = new WeakMap<object, unknown>();
16
+
17
+ /** Payload evidence stays out of the scalar portable error/status contract. */
18
+ export function attachToolCallFailureEvidence(
19
+ error: object,
20
+ evidence: unknown,
21
+ ): void {
22
+ callEvidenceByError.set(error, evidence);
23
+ }
24
+
25
+ /** Read the already consumed error body without changing failure classification. */
26
+ export function attachToolCallFailureEvidenceFromResponse(
27
+ error: object,
28
+ body: string,
29
+ ): void {
30
+ let response: unknown;
31
+ try {
32
+ response = JSON.parse(body);
33
+ } catch {
34
+ return;
35
+ }
36
+ if (!response || typeof response !== 'object') return;
37
+ const meta = (response as Record<string, unknown>).meta;
38
+ if (!meta || typeof meta !== 'object') return;
39
+ attachToolCallFailureEvidence(
40
+ error,
41
+ (meta as Record<string, unknown>).callEvidence,
42
+ );
43
+ }
44
+
45
+ export function readToolCallFailureEvidence(error: unknown): unknown {
46
+ return error && typeof error === 'object'
47
+ ? callEvidenceByError.get(error)
48
+ : undefined;
49
+ }
15
50
 
16
51
  type ToolHttpErrorConstructorInput = {
17
52
  message: string;
@@ -1,3 +1,7 @@
1
+ import {
2
+ jsonByteLengthUpTo,
3
+ RUNTIME_RECEIPT_OUTPUT_MAX_BYTES,
4
+ } from './output-size-limits';
1
5
  export type {
2
6
  SerializedToolExecuteResult,
3
7
  ToolExecuteResult,
@@ -83,6 +87,11 @@ const relayPublishedResultReferences = new WeakMap<
83
87
  RuntimeReceiptValueReference
84
88
  >();
85
89
 
90
+ // Call evidence belongs to the saved per-call payload. Keep it out of the
91
+ // enumerable Play result so returning a tool result or storing it in a row
92
+ // does not enlarge customer output with diagnostic request bodies.
93
+ const savedToolCallEvidence = new WeakMap<object, unknown>();
94
+
86
95
  export function attachRelayPublishedToolResultReference(
87
96
  result: ToolExecuteResult,
88
97
  reference: RuntimeReceiptValueReference,
@@ -1271,6 +1280,7 @@ export function createToolExecuteResult<TResult = unknown>(input: {
1271
1280
  meta?: Record<string, unknown>;
1272
1281
  };
1273
1282
  }): ToolExecuteResult<TResult> {
1283
+ const { callEvidence, ...publicMeta } = input.meta ?? {};
1274
1284
  const result = toResultEnvelope(input.result);
1275
1285
  const resultRoot = {
1276
1286
  toolResponse: {
@@ -1320,7 +1330,7 @@ export function createToolExecuteResult<TResult = unknown>(input: {
1320
1330
  const wrapper = {
1321
1331
  status: input.status,
1322
1332
  ...(input.jobId ? { job_id: input.jobId } : {}),
1323
- ...(input.meta ? { meta: input.meta } : {}),
1333
+ ...(input.meta ? { meta: publicMeta } : {}),
1324
1334
  ...(input.billing ? { billing: input.billing } : {}),
1325
1335
  toolResponse,
1326
1336
  extractedValues,
@@ -1336,6 +1346,9 @@ export function createToolExecuteResult<TResult = unknown>(input: {
1336
1346
  value: metadata,
1337
1347
  enumerable: false,
1338
1348
  });
1349
+ if (callEvidence !== undefined) {
1350
+ savedToolCallEvidence.set(wrapper, callEvidence);
1351
+ }
1339
1352
  return wrapper;
1340
1353
  }
1341
1354
 
@@ -1976,9 +1989,50 @@ function rawV2WithSerializedListRows(input: {
1976
1989
  });
1977
1990
  }
1978
1991
 
1992
+ /** Optional call evidence cannot turn a valid provider result into a retry. */
1993
+ function fitSavedCallEvidence(
1994
+ value: SerializedToolExecuteResult,
1995
+ ): SerializedToolExecuteResult {
1996
+ if (
1997
+ !value.meta?.callEvidence ||
1998
+ !jsonByteLengthUpTo(value, RUNTIME_RECEIPT_OUTPUT_MAX_BYTES).exceeded
1999
+ )
2000
+ return value;
2001
+ const { callEvidence: _evidence, ...meta } = value.meta;
2002
+ const withoutEvidence = { ...value, meta };
2003
+ // Preserve the original oversized-output failure if the result itself is
2004
+ // invalid. Only newly added diagnostic evidence may be omitted.
2005
+ if (
2006
+ jsonByteLengthUpTo(withoutEvidence, RUNTIME_RECEIPT_OUTPUT_MAX_BYTES)
2007
+ .exceeded
2008
+ )
2009
+ return value;
2010
+ const reason =
2011
+ 'Call evidence exceeds the saved-result size limit; the complete tool output is retained.';
2012
+ const unavailable = {
2013
+ ...value,
2014
+ meta: { ...meta, callEvidence: { unavailable: reason } },
2015
+ };
2016
+ if (
2017
+ !jsonByteLengthUpTo(unavailable, RUNTIME_RECEIPT_OUTPUT_MAX_BYTES).exceeded
2018
+ )
2019
+ return unavailable;
2020
+ // An output exactly at the historical limit has no room for the marker.
2021
+ // Its original bytes remain legal; the runner retains the warning instead.
2022
+ console.warn(reason);
2023
+ return withoutEvidence;
2024
+ }
2025
+
1979
2026
  export function serializeToolExecuteResult(
1980
2027
  value: ToolExecuteResult,
2028
+ options?: { includeCallEvidence?: boolean },
1981
2029
  ): SerializedToolExecuteResult {
2030
+ const callEvidence =
2031
+ options?.includeCallEvidence === false
2032
+ ? undefined
2033
+ : savedToolCallEvidence.get(value);
2034
+ const meta =
2035
+ callEvidence !== undefined ? { ...value.meta, callEvidence } : value.meta;
1982
2036
  const metadata = metadataInputFromToolExecuteResult(value);
1983
2037
  const listDatasets = serializedListDatasetsFromResult(value);
1984
2038
  const listRows = serializedListRowsFromResult({
@@ -2007,11 +2061,11 @@ export function serializeToolExecuteResult(
2007
2061
  view,
2008
2062
  meta: value.toolResponse.meta,
2009
2063
  });
2010
- return {
2064
+ return fitSavedCallEvidence({
2011
2065
  __kind: SERIALIZED_TOOL_EXECUTE_RESULT_V2_KIND,
2012
2066
  status: value.status,
2013
2067
  ...(typeof value.job_id === 'string' ? { job_id: value.job_id } : {}),
2014
- ...(isRecord(value.meta) ? { meta: value.meta } : {}),
2068
+ ...(isRecord(meta) ? { meta } : {}),
2015
2069
  ...(isRecord(value.billing) ? { billing: value.billing } : {}),
2016
2070
  toolResponse: {
2017
2071
  // rawV2 is the canonical complete provider response. Only the legacy
@@ -2035,13 +2089,13 @@ export function serializeToolExecuteResult(
2035
2089
  ...(targetValues ? { targetValues } : {}),
2036
2090
  metadata,
2037
2091
  execution: value._metadata.execution,
2038
- };
2092
+ });
2039
2093
  }
2040
- return {
2094
+ return fitSavedCallEvidence({
2041
2095
  __kind: SERIALIZED_TOOL_EXECUTE_RESULT_V1_KIND,
2042
2096
  status: value.status,
2043
2097
  ...(typeof value.job_id === 'string' ? { job_id: value.job_id } : {}),
2044
- ...(isRecord(value.meta) ? { meta: value.meta } : {}),
2098
+ ...(isRecord(meta) ? { meta } : {}),
2045
2099
  ...(isRecord(value.billing) ? { billing: value.billing } : {}),
2046
2100
  toolResponse: {
2047
2101
  raw: serializedRaw,
@@ -2052,7 +2106,7 @@ export function serializeToolExecuteResult(
2052
2106
  ...(targetValues ? { targetValues } : {}),
2053
2107
  metadata,
2054
2108
  execution: value._metadata.execution,
2055
- };
2109
+ });
2056
2110
  }
2057
2111
 
2058
2112
  export function isSerializedToolExecuteResult(
@@ -2156,7 +2210,13 @@ export function cloneToolExecuteResultWithExecution<TResult>(
2156
2210
  ): ToolExecuteResult<TResult> {
2157
2211
  return deserializeToolExecuteResult({
2158
2212
  ...serializeToolExecuteResult(value),
2159
- execution,
2213
+ execution: {
2214
+ ...execution,
2215
+ ...(!execution.resultReceiptKey &&
2216
+ value._metadata.execution.resultReceiptKey
2217
+ ? { resultReceiptKey: value._metadata.execution.resultReceiptKey }
2218
+ : {}),
2219
+ },
2160
2220
  }) as ToolExecuteResult<TResult>;
2161
2221
  }
2162
2222
 
@@ -34,6 +34,8 @@ export type PlayVisualNodeProgress = {
34
34
  completedAt?: number;
35
35
  artifactTableNamespace?: string | null;
36
36
  nodeIo?: PlayDocflowNodeIoState;
37
+ /** Error reported by this step, distinct from a run stopping other work. */
38
+ error?: string;
37
39
  conditionCounts?: PlayConditionCounts;
38
40
  };
39
41
 
@@ -8,11 +8,38 @@ type Bulk = {
8
8
  email_batch: Array<{ email_address: string; ip_address?: string }>;
9
9
  };
10
10
  type Verdict = Record<string, unknown> & { address: string; status: string };
11
- type Results = { email_batch?: unknown[]; errors?: unknown[]; data?: Results };
11
+ type Results = {
12
+ email_batch?: unknown[];
13
+ errors?: unknown[];
14
+ data?: Results;
15
+ // The scalar validate endpoint returns one verdict at the top level.
16
+ address?: unknown;
17
+ status?: unknown;
18
+ };
19
+
20
+ const VERDICT_STATUSES = [
21
+ 'valid',
22
+ 'invalid',
23
+ 'catch-all',
24
+ 'unknown',
25
+ 'spamtrap',
26
+ 'abuse',
27
+ 'do_not_mail',
28
+ ];
12
29
 
13
30
  function emailKey(email: string) {
14
31
  return email.trim().toLowerCase();
15
32
  }
33
+
34
+ function isVerdict(value: unknown): value is Verdict {
35
+ const row = value as Verdict | null;
36
+ return Boolean(
37
+ row &&
38
+ typeof row.address === 'string' &&
39
+ typeof row.status === 'string' &&
40
+ VERDICT_STATUSES.includes(row.status),
41
+ );
42
+ }
16
43
  function bucket(payload: Single) {
17
44
  // The response identifies emails, but does not echo the supplied IP.
18
45
  return JSON.stringify([
@@ -23,7 +50,7 @@ function bucket(payload: Single) {
23
50
 
24
51
  export const zerobounceValidateBatchStrategy: BatchOperationStrategy<
25
52
  Single,
26
- Bulk,
53
+ Bulk | Single,
27
54
  Results,
28
55
  { data: Verdict }
29
56
  > = {
@@ -47,25 +74,58 @@ export const zerobounceValidateBatchStrategy: BatchOperationStrategy<
47
74
  'ZeroBounce batch requires 1–100 emails with matching IP options.',
48
75
  );
49
76
  }
77
+ const items = payloads.map((payload) => ({
78
+ itemKey: zerobounceValidateBatchStrategy.toItemKey(payload),
79
+ payload,
80
+ }));
81
+ const emails = [...new Set(payloads.map((p) => emailKey(p.email!)))];
82
+ if (emails.length === 1) {
83
+ // One distinct email (a per-row waterfall, duplicate rows, or receipt
84
+ // dedup leaving one owned member) must not ride the bulk endpoint:
85
+ // zerobounce_batch_validate is gated at 30/min provider_shared, while
86
+ // the scalar validate endpoint allows 80,000 validations per 10 s.
87
+ // Compiling here covers both the receipt-owned cohort
88
+ // (compileOwnedPhysicalBatch) and the legacy path.
89
+ return {
90
+ batchOperation: 'zerobounce_validate',
91
+ batchPayload: first,
92
+ items,
93
+ };
94
+ }
50
95
  return {
51
- // Receipt deduplication may leave one owned email. Keep the bulk operation.
52
96
  batchOperation: 'zerobounce_batch_validate',
53
97
  batchPayload: {
54
- email_batch: [...new Set(payloads.map((p) => emailKey(p.email!)))].map(
55
- (email_address) => ({
56
- email_address,
57
- ...(first.ip_address ? { ip_address: first.ip_address } : {}),
58
- }),
59
- ),
98
+ email_batch: emails.map((email_address) => ({
99
+ email_address,
100
+ ...(first.ip_address ? { ip_address: first.ip_address } : {}),
101
+ })),
60
102
  },
61
- items: payloads.map((payload) => ({
62
- itemKey: zerobounceValidateBatchStrategy.toItemKey(payload),
63
- payload,
64
- })),
103
+ items,
65
104
  };
66
105
  },
67
106
  splitResult(fullResult, compiled) {
68
107
  const result = fullResult.data ?? fullResult;
108
+ if (compiled.batchOperation === 'zerobounce_validate') {
109
+ if (
110
+ !isVerdict(result) ||
111
+ compiled.items.some(
112
+ (item) => emailKey(result.address) !== emailKey(item.payload.email!),
113
+ )
114
+ ) {
115
+ throw new Error(
116
+ 'ZeroBounce single verification returned a malformed or mismatched verdict.',
117
+ );
118
+ }
119
+ // Every item shares the one distinct email this call verified.
120
+ return compiled.items.map((item) => ({
121
+ itemKey: item.itemKey,
122
+ result: { data: result },
123
+ rawResult: result,
124
+ }));
125
+ }
126
+ if (!('email_batch' in compiled.batchPayload)) {
127
+ throw new Error('ZeroBounce bulk split received a scalar payload.');
128
+ }
69
129
  if (
70
130
  !Array.isArray(result.email_batch) ||
71
131
  (result.errors !== undefined &&
@@ -79,21 +139,8 @@ export const zerobounceValidateBatchStrategy: BatchOperationStrategy<
79
139
  compiled.batchPayload.email_batch.map((p) => emailKey(p.email_address)),
80
140
  );
81
141
  const byEmail = new Map<string, Verdict>();
82
- for (const value of result.email_batch) {
83
- const row = value as Verdict | null;
84
- if (
85
- !row ||
86
- typeof row.address !== 'string' ||
87
- ![
88
- 'valid',
89
- 'invalid',
90
- 'catch-all',
91
- 'unknown',
92
- 'spamtrap',
93
- 'abuse',
94
- 'do_not_mail',
95
- ].includes(row.status)
96
- ) {
142
+ for (const row of result.email_batch) {
143
+ if (!isVerdict(row)) {
97
144
  throw new Error(
98
145
  'ZeroBounce bulk verification returned a malformed verdict.',
99
146
  );
@@ -37,6 +37,8 @@ export type ToolResultExecutionMetadata = {
37
37
  cacheKey?: string;
38
38
  receiptRole?: 'owner' | 'follower';
39
39
  receiptKey?: string;
40
+ /** Canonical saved result identity; receiptKey may name a transport alias. */
41
+ resultReceiptKey?: string;
40
42
  attachedToReceiptKey?: string;
41
43
  };
42
44