deepline 0.3.47 → 0.3.48

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.
@@ -3106,6 +3106,14 @@ export class DeeplineClient {
3106
3106
  signal?: AbortSignal;
3107
3107
  lastEventId?: string;
3108
3108
  mode?: 'cli' | 'ui';
3109
+ /**
3110
+ * A run just accepted by durable scheduler admission can take a short
3111
+ * time to appear in the asynchronous Convex read model. Callers that
3112
+ * received that admission may opt into a bounded retry of the initial
3113
+ * pending response; a normal arbitrary run id still fails loudly by
3114
+ * default.
3115
+ */
3116
+ waitForProjection?: boolean;
3109
3117
  },
3110
3118
  ): AsyncGenerator<PlayLiveEvent> {
3111
3119
  const headers =
@@ -3114,12 +3122,33 @@ export class DeeplineClient {
3114
3122
  : undefined;
3115
3123
  const params = new URLSearchParams();
3116
3124
  params.set('mode', options?.mode ?? 'cli');
3117
- for await (const event of this.http.streamSse<PlayLiveEvent>(
3118
- `/api/v2/runs/${encodeURIComponent(workflowId)}/tail?${params.toString()}`,
3119
- { signal: options?.signal, headers },
3120
- )) {
3121
- if (event.scope === 'play') {
3122
- yield event;
3125
+ const projectionDeadline = Date.now() + 30_000;
3126
+ let projectionAttempt = 0;
3127
+ for (;;) {
3128
+ let sawEvent = false;
3129
+ try {
3130
+ for await (const event of this.http.streamSse<PlayLiveEvent>(
3131
+ `/api/v2/runs/${encodeURIComponent(workflowId)}/tail?${params.toString()}`,
3132
+ { signal: options?.signal, headers },
3133
+ )) {
3134
+ sawEvent = true;
3135
+ if (event.scope === 'play') {
3136
+ yield event;
3137
+ }
3138
+ }
3139
+ return;
3140
+ } catch (error) {
3141
+ const projectionPending =
3142
+ options?.waitForProjection === true &&
3143
+ !sawEvent &&
3144
+ error instanceof DeeplineError &&
3145
+ (error.statusCode === 404 ||
3146
+ (error.statusCode === 202 &&
3147
+ error.code === 'RUN_PROJECTION_PENDING')) &&
3148
+ Date.now() < projectionDeadline;
3149
+ if (!projectionPending) throw error;
3150
+ await sleep(streamReconnectDelayMs(projectionAttempt));
3151
+ projectionAttempt += 1;
3123
3152
  }
3124
3153
  }
3125
3154
  }
@@ -4439,6 +4468,7 @@ export class DeeplineClient {
4439
4468
  for await (const event of this.streamPlayRunEvents(workflowId, {
4440
4469
  mode: 'cli',
4441
4470
  signal: options?.signal,
4471
+ waitForProjection: true,
4442
4472
  })) {
4443
4473
  if (options?.signal?.aborted) {
4444
4474
  await this.cancelPlay(workflowId);
@@ -15,6 +15,7 @@ import {
15
15
  type ToolExecutionErrorOptions,
16
16
  type ToolExecutionNetworkKind,
17
17
  type ToolExecutionNetworkScope,
18
+ type ToolExecutionPublicDetails,
18
19
  } from '../../shared_libs/plays/tool-execution-error';
19
20
 
20
21
  export {
@@ -33,6 +34,7 @@ export {
33
34
  type ToolExecutionErrorOptions,
34
35
  type ToolExecutionNetworkKind,
35
36
  type ToolExecutionNetworkScope,
37
+ type ToolExecutionPublicDetails,
36
38
  } from '../../shared_libs/plays/tool-execution-error';
37
39
 
38
40
  /**
@@ -145,6 +147,8 @@ export class ToolRateLimitError extends RateLimitError {
145
147
  readonly networkKind: ToolExecutionError['networkKind'];
146
148
  /** Network boundary that failed, or `null` for non-network failures. */
147
149
  readonly networkScope: ToolExecutionError['networkScope'];
150
+ /** Explicitly allowlisted diagnostics safe for SDK callers. */
151
+ readonly publicDetails: ToolExecutionError['publicDetails'];
148
152
 
149
153
  /** Constructed by the SDK after a structured tool HTTP 429. */
150
154
  constructor(message: string, options: ToolExecutionErrorOptions) {
@@ -161,6 +165,7 @@ export class ToolRateLimitError extends RateLimitError {
161
165
  this.requestId = options.requestId;
162
166
  this.networkKind = options.networkKind;
163
167
  this.networkScope = options.networkScope;
168
+ this.publicDetails = options.publicDetails ?? null;
164
169
  this.details = options.details;
165
170
  brandAsToolExecutionError(this);
166
171
  if (isProviderTransientFailure(this)) {
@@ -621,7 +621,10 @@ export class HttpClient {
621
621
  signal: options?.signal,
622
622
  });
623
623
 
624
- if (!response.ok) {
624
+ // An SSE endpoint must either establish an event stream or reject the
625
+ // request. A 202 JSON response is a deliberately retryable admission
626
+ // state, not an empty, successfully completed stream.
627
+ if (!response.ok || response.status === 202) {
625
628
  const body = await response.text();
626
629
  const parsed = parseResponseBody(body);
627
630
  if (
@@ -145,6 +145,7 @@ export type {
145
145
  ToolExecutionErrorOptions,
146
146
  ToolExecutionNetworkKind,
147
147
  ToolExecutionNetworkScope,
148
+ ToolExecutionPublicDetails,
148
149
  } from './errors.js';
149
150
 
150
151
  // ——— Config ———
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.47',
202
+ version: '0.3.48',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {
@@ -156,6 +156,7 @@ type RuntimeApiRequest =
156
156
  action: 'start_run';
157
157
  idempotencyKey?: string;
158
158
  playName: string;
159
+ playReference?: string | null;
159
160
  runId: string;
160
161
  artifactStorageKey?: string | null;
161
162
  artifactHash?: string | null;
@@ -172,6 +173,45 @@ type RuntimeApiRequest =
172
173
  inputSha256?: string;
173
174
  replayedFromRunId?: string | null;
174
175
  }
176
+ | {
177
+ /** Ordered projection of a Runtime Postgres/Absurd admission. */
178
+ action: 'project_run_created';
179
+ idempotencyKey?: string;
180
+ playName: string;
181
+ playReference?: string | null;
182
+ workflowId: string;
183
+ runId: string;
184
+ workflowFamilyKey: string;
185
+ definitionScope?: 'org' | 'system';
186
+ revisionId?: string | null;
187
+ artifactStorageKey?: string | null;
188
+ artifactHash?: string | null;
189
+ graphHash?: string | null;
190
+ runtimeBackend?: string | null;
191
+ schedulerBackend?: string | null;
192
+ schedulerSchema?: string | null;
193
+ executionProfile?: string | null;
194
+ runtimeReleaseId?: string | null;
195
+ runtimeDeployVersion?: string | null;
196
+ runtimeCallbackBaseUrl?: string | null;
197
+ coordinatorUrl?: string | null;
198
+ coordinatorWorkerName?: string | null;
199
+ runtimeWorkflowName?: string | null;
200
+ runtimeHarnessWorkerName?: string | null;
201
+ maxCreditsPerRun?: number | null;
202
+ staticPipeline?: unknown;
203
+ source?: 'published' | 'ad_hoc' | 'draft';
204
+ triggerSource?: 'webhook' | 'cron' | 'sql_listener' | 'api';
205
+ inputFileId?: string;
206
+ inputBytes?: number;
207
+ inputSha256?: string;
208
+ replayedFromRunId?: string | null;
209
+ secretRefs?: unknown[];
210
+ inputSummary?: Record<string, unknown>;
211
+ /** Terminal bypass used only when the causal admission projection is
212
+ * permanently blocked and no queued row can be projected. */
213
+ admissionFailure?: string;
214
+ }
175
215
  | ({
176
216
  action: 'save_results';
177
217
  } & RuntimeSaveResults)
@@ -668,7 +708,8 @@ function isRetryableAppRuntimeAction(
668
708
  action === 'release_runtime_step_receipt' ||
669
709
  action === 'save_results' ||
670
710
  action === 'skip_runtime_step_receipt' ||
671
- action === 'start_run'
711
+ action === 'start_run' ||
712
+ action === 'project_run_created'
672
713
  );
673
714
  }
674
715
 
@@ -1925,6 +1966,7 @@ export async function startRunViaAppRuntime(
1925
1966
  context: WorkerRuntimeApiContext,
1926
1967
  input: {
1927
1968
  playName: string;
1969
+ playReference?: string | null;
1928
1970
  runId: string;
1929
1971
  artifactStorageKey?: string | null;
1930
1972
  artifactHash?: string | null;
@@ -1949,6 +1991,24 @@ export async function startRunViaAppRuntime(
1949
1991
  });
1950
1992
  }
1951
1993
 
1994
+ /**
1995
+ * Deliver the causally-first `run.created` projection after Runtime Postgres
1996
+ * has atomically admitted the run and its Absurd task. It is safe to retry:
1997
+ * the server preserves any existing queued, running, or terminal run.
1998
+ */
1999
+ export async function projectRunCreatedViaAppRuntime(
2000
+ context: WorkerRuntimeApiContext,
2001
+ input: Omit<
2002
+ Extract<RuntimeApiRequest, { action: 'project_run_created' }>,
2003
+ 'action'
2004
+ >,
2005
+ ): Promise<void> {
2006
+ await postAppRuntimeApi<{ ok: true; status: 'queued' }>(context, {
2007
+ action: 'project_run_created',
2008
+ ...input,
2009
+ });
2010
+ }
2011
+
1952
2012
  export async function saveResultsViaAppRuntime(
1953
2013
  context: WorkerRuntimeApiContext,
1954
2014
  input: RuntimeSaveResults,
@@ -42,7 +42,15 @@ export type { PlayRunInputPayload };
42
42
  export type PlaySchedulerSubmitInput = {
43
43
  runId: string;
44
44
  playId: string;
45
+ /** Canonical definition name used by the Convex read-model projector. */
46
+ definitionName?: string | null;
45
47
  playName: string;
48
+ /** Immutable display/lookup name selected at admission, if distinct. */
49
+ playReference?: string | null;
50
+ /** Definition ownership selected at admission; never re-resolve on replay. */
51
+ definitionScope?: 'org' | 'system' | null;
52
+ /** Immutable published revision selected at admission. */
53
+ revisionId?: string | null;
46
54
  workflowFamilyKey?: string | null;
47
55
  artifactStorageKey: string;
48
56
  /** Optional inline artifact for the Node scheduler. */
@@ -52,6 +60,10 @@ export type PlaySchedulerSubmitInput = {
52
60
  /** Immutable scheduler/worker/runner ABI snapshot (not a Git SHA). */
53
61
  runtimeProtocolVersion?: PlayRuntimeProtocolVersion | null;
54
62
  input: PlayRunInputPayload;
63
+ /** Immutable secret authority selected at admission; never re-resolve on projection. */
64
+ secretRefs?: unknown[];
65
+ /** Display-only, size-bounded projection of the admitted input. */
66
+ inputSummary?: Record<string, unknown>;
55
67
  /** Convex metadata for the exact input saved before scheduler submission. */
56
68
  inputFileId?: string;
57
69
  inputBytes?: number;
@@ -71,6 +83,8 @@ export type PlaySchedulerSubmitInput = {
71
83
  storageKey?: string;
72
84
  fileName?: string;
73
85
  logicalPath?: string;
86
+ storageKind?: 'r2';
87
+ contentHash?: string;
74
88
  contentType?: string;
75
89
  bytes?: number;
76
90
  } | null;
@@ -87,6 +101,8 @@ export type PlaySchedulerSubmitInput = {
87
101
  logicalPath?: string;
88
102
  fileName?: string;
89
103
  storageKey: string;
104
+ storageKind?: 'r2';
105
+ contentHash?: string;
90
106
  contentType?: string;
91
107
  bytes?: number;
92
108
  inlineText?: string;
@@ -136,6 +152,16 @@ export type PlaySchedulerSubmitInput = {
136
152
  */
137
153
  queuePriority?: number | null;
138
154
  executionProfile?: string | null;
155
+ /** Concrete Runtime Postgres namespace selected at admission. */
156
+ schedulerSchema?: string | null;
157
+ /** Immutable release tuple for the Convex read-model projection. */
158
+ runtimeReleaseId?: string | null;
159
+ runtimeCallbackBaseUrl?: string | null;
160
+ coordinatorWorkerName?: string | null;
161
+ runtimeWorkflowName?: string | null;
162
+ runtimeHarnessWorkerName?: string | null;
163
+ /** Billing admission cap from the immutable contract snapshot. */
164
+ maxCreditsPerRun?: number | null;
139
165
  /** runner backend to use for executing attempts */
140
166
  runtimeBackend: string;
141
167
  /**
@@ -103,6 +103,7 @@ class StructuredToolHttpError extends ToolExecutionError {
103
103
  retryAfterMs: input.options.retryAfterMs ?? null,
104
104
  networkKind: input.options.networkKind ?? null,
105
105
  networkScope: input.options.networkScope ?? null,
106
+ publicDetails: input.options.publicDetails ?? null,
106
107
  });
107
108
  this.billing = input.billing;
108
109
  this.status = input.status;
@@ -447,6 +448,7 @@ export function normalizeToolHttpErrorMessage(input: {
447
448
  (origin === 'provider' && category === 'network'
448
449
  ? 'deepline_to_provider'
449
450
  : null),
451
+ publicDetails: hydratedFailure?.publicDetails ?? null,
450
452
  };
451
453
  const billing = getObjectField(parsed, 'billing');
452
454
  if (isInsufficientCreditsBilling(billing)) {
@@ -84,6 +84,17 @@ export type ToolExecutionNetworkScope =
84
84
  | 'runtime_to_deepline'
85
85
  | 'deepline_to_provider';
86
86
 
87
+ /**
88
+ * Bounded, primitive-only diagnostics explicitly approved for customers.
89
+ * Raw provider bodies, credentials, prompts, stacks, and causes never belong
90
+ * in this shared API/SDK/Play contract.
91
+ *
92
+ * @sdkReference errors 063
93
+ */
94
+ export type ToolExecutionPublicDetails = Readonly<
95
+ Record<string, string | number | boolean | null>
96
+ >;
97
+
87
98
  /**
88
99
  * Portable version-1 `tool_error` payload.
89
100
  *
@@ -120,6 +131,8 @@ export type ToolExecutionFailureV1 = {
120
131
  networkKind: ToolExecutionNetworkKind | null;
121
132
  /** Network boundary that failed, or `null`. */
122
133
  networkScope: ToolExecutionNetworkScope | null;
134
+ /** Explicitly allowlisted customer diagnostics, when present. */
135
+ publicDetails?: ToolExecutionPublicDetails | null;
123
136
  };
124
137
 
125
138
  /**
@@ -265,6 +278,8 @@ export class ToolExecutionError extends DeeplineError {
265
278
  readonly networkKind: ToolExecutionNetworkKind | null;
266
279
  /** Network boundary that failed, or `null` for non-network failures. */
267
280
  readonly networkScope: ToolExecutionNetworkScope | null;
281
+ /** Explicitly allowlisted diagnostics safe for SDK and Play callers. */
282
+ readonly publicDetails: ToolExecutionPublicDetails | null;
268
283
 
269
284
  /**
270
285
  * Construct a structured tool error.
@@ -290,6 +305,7 @@ export class ToolExecutionError extends DeeplineError {
290
305
  this.retryAfterMs = options.retryAfterMs;
291
306
  this.networkKind = options.networkKind;
292
307
  this.networkScope = options.networkScope;
308
+ this.publicDetails = options.publicDetails ?? null;
293
309
  applyBrand(this, TOOL_EXECUTION_ERROR_BRAND);
294
310
  if (isProviderTransientFailure(options)) {
295
311
  applyBrand(this, PROVIDER_TRANSIENT_ERROR_BRAND);
@@ -547,6 +563,69 @@ function isRecord(value: unknown): value is Record<string, unknown> {
547
563
  return value !== null && typeof value === 'object' && !Array.isArray(value);
548
564
  }
549
565
 
566
+ const MAX_PUBLIC_DETAIL_ENTRIES = 20;
567
+ const MAX_PUBLIC_DETAIL_KEY_LENGTH = 80;
568
+ const MAX_PUBLIC_DETAIL_STRING_LENGTH = 512;
569
+ const publicDetailsByError = new WeakMap<Error, ToolExecutionPublicDetails>();
570
+
571
+ /**
572
+ * Normalizes the one public-detail shape at every transport boundary. Nested
573
+ * values are rejected to prevent accidental disclosure of provider responses.
574
+ */
575
+ export function normalizeToolExecutionPublicDetails(
576
+ value: unknown,
577
+ ): ToolExecutionPublicDetails | null {
578
+ if (!isRecord(value)) return null;
579
+ const details: Record<string, string | number | boolean | null> = {};
580
+ for (const [key, entry] of Object.entries(value)) {
581
+ if (Object.keys(details).length >= MAX_PUBLIC_DETAIL_ENTRIES) break;
582
+ if (
583
+ key.length === 0 ||
584
+ key.length > MAX_PUBLIC_DETAIL_KEY_LENGTH ||
585
+ !/^[a-z][a-zA-Z0-9_]*$/.test(key)
586
+ ) {
587
+ continue;
588
+ }
589
+ if (typeof entry === 'string') {
590
+ if (entry.length <= MAX_PUBLIC_DETAIL_STRING_LENGTH) details[key] = entry;
591
+ continue;
592
+ }
593
+ if (typeof entry === 'number') {
594
+ if (Number.isFinite(entry)) details[key] = entry;
595
+ continue;
596
+ }
597
+ if (typeof entry === 'boolean') {
598
+ details[key] = entry;
599
+ continue;
600
+ }
601
+ if (entry === null) details[key] = null;
602
+ }
603
+ return Object.keys(details).length > 0 ? details : null;
604
+ }
605
+
606
+ /**
607
+ * Attaches allowlisted details to an in-process error without making them
608
+ * enumerable. The execute boundary can later serialize them through the
609
+ * portable `tool_error.publicDetails` field.
610
+ */
611
+ export function withToolExecutionPublicDetails<T extends Error>(
612
+ error: T,
613
+ details: unknown,
614
+ ): T {
615
+ const normalized = normalizeToolExecutionPublicDetails(details);
616
+ if (normalized) publicDetailsByError.set(error, normalized);
617
+ return error;
618
+ }
619
+
620
+ /** Reads details deliberately attached through `withToolExecutionPublicDetails`. */
621
+ export function getToolExecutionPublicDetails(
622
+ error: unknown,
623
+ ): ToolExecutionPublicDetails | null {
624
+ return error instanceof Error
625
+ ? (publicDetailsByError.get(error) ?? null)
626
+ : null;
627
+ }
628
+
550
629
  export function normalizeToolExecutionFailure(
551
630
  value: unknown,
552
631
  ): ToolExecutionFailureV1 | null {
@@ -565,6 +644,7 @@ export function normalizeToolExecutionFailure(
565
644
  operation,
566
645
  });
567
646
  const category = normalizeToolExecutionCategory(value.category);
647
+ const publicDetails = normalizeToolExecutionPublicDetails(value.publicDetails);
568
648
  const trustworthy =
569
649
  origin !== 'unknown' &&
570
650
  category !== 'unknown' &&
@@ -583,6 +663,7 @@ export function normalizeToolExecutionFailure(
583
663
  retryAfterMs: finiteNonNegativeInteger(value.retryAfterMs),
584
664
  networkKind: normalizeNetworkKind(value.networkKind),
585
665
  networkScope: normalizeNetworkScope(value.networkScope),
666
+ ...(publicDetails ? { publicDetails } : {}),
586
667
  };
587
668
  }
588
669
 
@@ -604,6 +685,7 @@ export function serializeToolExecutionFailure(
604
685
  retryAfterMs: error.retryAfterMs,
605
686
  networkKind: error.networkKind,
606
687
  networkScope: error.networkScope,
688
+ publicDetails: error.publicDetails,
607
689
  });
608
690
  }
609
691
 
@@ -84,6 +84,17 @@ export type ToolExecutionNetworkScope =
84
84
  | 'runtime_to_deepline'
85
85
  | 'deepline_to_provider';
86
86
 
87
+ /**
88
+ * Bounded, primitive-only diagnostics explicitly approved for customers.
89
+ * Raw provider bodies, credentials, prompts, stacks, and causes never belong
90
+ * in this shared API/SDK/Play contract.
91
+ *
92
+ * @sdkReference errors 063
93
+ */
94
+ export type ToolExecutionPublicDetails = Readonly<
95
+ Record<string, string | number | boolean | null>
96
+ >;
97
+
87
98
  /**
88
99
  * Portable version-1 `tool_error` payload.
89
100
  *
@@ -120,6 +131,8 @@ export type ToolExecutionFailureV1 = {
120
131
  networkKind: ToolExecutionNetworkKind | null;
121
132
  /** Network boundary that failed, or `null`. */
122
133
  networkScope: ToolExecutionNetworkScope | null;
134
+ /** Explicitly allowlisted customer diagnostics, when present. */
135
+ publicDetails?: ToolExecutionPublicDetails | null;
123
136
  };
124
137
 
125
138
  /**
@@ -265,6 +278,8 @@ export class ToolExecutionError extends DeeplineError {
265
278
  readonly networkKind: ToolExecutionNetworkKind | null;
266
279
  /** Network boundary that failed, or `null` for non-network failures. */
267
280
  readonly networkScope: ToolExecutionNetworkScope | null;
281
+ /** Explicitly allowlisted diagnostics safe for SDK and Play callers. */
282
+ readonly publicDetails: ToolExecutionPublicDetails | null;
268
283
 
269
284
  /**
270
285
  * Construct a structured tool error.
@@ -290,6 +305,7 @@ export class ToolExecutionError extends DeeplineError {
290
305
  this.retryAfterMs = options.retryAfterMs;
291
306
  this.networkKind = options.networkKind;
292
307
  this.networkScope = options.networkScope;
308
+ this.publicDetails = options.publicDetails ?? null;
293
309
  applyBrand(this, TOOL_EXECUTION_ERROR_BRAND);
294
310
  if (isProviderTransientFailure(options)) {
295
311
  applyBrand(this, PROVIDER_TRANSIENT_ERROR_BRAND);
@@ -547,6 +563,69 @@ function isRecord(value: unknown): value is Record<string, unknown> {
547
563
  return value !== null && typeof value === 'object' && !Array.isArray(value);
548
564
  }
549
565
 
566
+ const MAX_PUBLIC_DETAIL_ENTRIES = 20;
567
+ const MAX_PUBLIC_DETAIL_KEY_LENGTH = 80;
568
+ const MAX_PUBLIC_DETAIL_STRING_LENGTH = 512;
569
+ const publicDetailsByError = new WeakMap<Error, ToolExecutionPublicDetails>();
570
+
571
+ /**
572
+ * Normalizes the one public-detail shape at every transport boundary. Nested
573
+ * values are rejected to prevent accidental disclosure of provider responses.
574
+ */
575
+ export function normalizeToolExecutionPublicDetails(
576
+ value: unknown,
577
+ ): ToolExecutionPublicDetails | null {
578
+ if (!isRecord(value)) return null;
579
+ const details: Record<string, string | number | boolean | null> = {};
580
+ for (const [key, entry] of Object.entries(value)) {
581
+ if (Object.keys(details).length >= MAX_PUBLIC_DETAIL_ENTRIES) break;
582
+ if (
583
+ key.length === 0 ||
584
+ key.length > MAX_PUBLIC_DETAIL_KEY_LENGTH ||
585
+ !/^[a-z][a-zA-Z0-9_]*$/.test(key)
586
+ ) {
587
+ continue;
588
+ }
589
+ if (typeof entry === 'string') {
590
+ if (entry.length <= MAX_PUBLIC_DETAIL_STRING_LENGTH) details[key] = entry;
591
+ continue;
592
+ }
593
+ if (typeof entry === 'number') {
594
+ if (Number.isFinite(entry)) details[key] = entry;
595
+ continue;
596
+ }
597
+ if (typeof entry === 'boolean') {
598
+ details[key] = entry;
599
+ continue;
600
+ }
601
+ if (entry === null) details[key] = null;
602
+ }
603
+ return Object.keys(details).length > 0 ? details : null;
604
+ }
605
+
606
+ /**
607
+ * Attaches allowlisted details to an in-process error without making them
608
+ * enumerable. The execute boundary can later serialize them through the
609
+ * portable `tool_error.publicDetails` field.
610
+ */
611
+ export function withToolExecutionPublicDetails<T extends Error>(
612
+ error: T,
613
+ details: unknown,
614
+ ): T {
615
+ const normalized = normalizeToolExecutionPublicDetails(details);
616
+ if (normalized) publicDetailsByError.set(error, normalized);
617
+ return error;
618
+ }
619
+
620
+ /** Reads details deliberately attached through `withToolExecutionPublicDetails`. */
621
+ export function getToolExecutionPublicDetails(
622
+ error: unknown,
623
+ ): ToolExecutionPublicDetails | null {
624
+ return error instanceof Error
625
+ ? (publicDetailsByError.get(error) ?? null)
626
+ : null;
627
+ }
628
+
550
629
  export function normalizeToolExecutionFailure(
551
630
  value: unknown,
552
631
  ): ToolExecutionFailureV1 | null {
@@ -565,6 +644,7 @@ export function normalizeToolExecutionFailure(
565
644
  operation,
566
645
  });
567
646
  const category = normalizeToolExecutionCategory(value.category);
647
+ const publicDetails = normalizeToolExecutionPublicDetails(value.publicDetails);
568
648
  const trustworthy =
569
649
  origin !== 'unknown' &&
570
650
  category !== 'unknown' &&
@@ -583,6 +663,7 @@ export function normalizeToolExecutionFailure(
583
663
  retryAfterMs: finiteNonNegativeInteger(value.retryAfterMs),
584
664
  networkKind: normalizeNetworkKind(value.networkKind),
585
665
  networkScope: normalizeNetworkScope(value.networkScope),
666
+ ...(publicDetails ? { publicDetails } : {}),
586
667
  };
587
668
  }
588
669
 
@@ -604,6 +685,7 @@ export function serializeToolExecutionFailure(
604
685
  retryAfterMs: error.retryAfterMs,
605
686
  networkKind: error.networkKind,
606
687
  networkScope: error.networkScope,
688
+ publicDetails: error.publicDetails,
607
689
  });
608
690
  }
609
691