deepline 0.1.140 → 0.1.142

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 (23) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +54 -15
  2. package/dist/bundling-sources/apps/play-runner-workers/src/durable-object-deploy-handoff.ts +24 -0
  3. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +46 -22
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/live-progress.ts +4 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-http-errors.ts +7 -262
  6. package/dist/bundling-sources/sdk/src/client.ts +24 -0
  7. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  8. package/dist/bundling-sources/sdk/src/types.ts +32 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +54 -34
  10. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +4 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +4 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +32 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +12 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +55 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +248 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +4 -0
  17. package/dist/cli/index.js +165 -42
  18. package/dist/cli/index.mjs +165 -42
  19. package/dist/index.d.mts +44 -0
  20. package/dist/index.d.ts +44 -0
  21. package/dist/index.js +36 -2
  22. package/dist/index.mjs +36 -2
  23. package/package.json +1 -1
@@ -65,6 +65,11 @@ import {
65
65
  } from './workflow-retry-state';
66
66
  import { createOrAttachWorkflowInstance } from './workflow-instance-create';
67
67
  import { sanitizeLiveLogLines } from './runtime/live-progress';
68
+ import {
69
+ DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS,
70
+ durableObjectDeployHandoffMessage,
71
+ sleepForDurableObjectDeployHandoffRetry,
72
+ } from './durable-object-deploy-handoff';
68
73
 
69
74
  export { DynamicWorkflowBinding };
70
75
 
@@ -784,24 +789,58 @@ async function callRunScopedControl<T>(
784
789
  path: string,
785
790
  init?: RequestInit,
786
791
  ): Promise<T> {
787
- const response = await runScopedDurableObject(env, runId).fetch(
788
- `https://deepline.run-state.internal${path}`,
789
- {
790
- ...(init ?? {}),
791
- headers: {
792
- 'content-type': 'application/json',
793
- ...(init?.headers ?? {}),
794
- },
795
- },
796
- );
797
- if (!response.ok) {
792
+ const maxAttempts = DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS.length + 1;
793
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
794
+ let response: Response;
795
+ try {
796
+ response = await runScopedDurableObject(env, runId).fetch(
797
+ `https://deepline.run-state.internal${path}`,
798
+ {
799
+ ...(init ?? {}),
800
+ headers: {
801
+ 'content-type': 'application/json',
802
+ ...(init?.headers ?? {}),
803
+ },
804
+ },
805
+ );
806
+ } catch (error) {
807
+ const handoffMessage = durableObjectDeployHandoffMessage(error);
808
+ if (handoffMessage && attempt < maxAttempts) {
809
+ console.warn('[coordinator] run state DO deploy handoff retry', {
810
+ runId,
811
+ path,
812
+ attempt,
813
+ error: handoffMessage,
814
+ });
815
+ await sleepForDurableObjectDeployHandoffRetry(attempt - 1);
816
+ continue;
817
+ }
818
+ throw error;
819
+ }
820
+
821
+ if (response.ok) {
822
+ return (await response.json()) as T;
823
+ }
824
+
825
+ const responseText = (await response.text().catch(() => '')).slice(0, 400);
826
+ const handoffMessage = durableObjectDeployHandoffMessage(responseText);
827
+ if (handoffMessage && attempt < maxAttempts) {
828
+ console.warn('[coordinator] run state DO deploy handoff retry', {
829
+ runId,
830
+ path,
831
+ attempt,
832
+ status: response.status,
833
+ error: handoffMessage,
834
+ });
835
+ await sleepForDurableObjectDeployHandoffRetry(attempt - 1);
836
+ continue;
837
+ }
838
+
798
839
  throw new Error(
799
- `run state ${path} failed ${response.status}: ${(
800
- await response.text().catch(() => '')
801
- ).slice(0, 400)}`,
840
+ `run state ${path} failed ${response.status}: ${responseText}`,
802
841
  );
803
842
  }
804
- return (await response.json()) as T;
843
+ throw new Error(`run state ${path} failed after deploy handoff retries`);
805
844
  }
806
845
 
807
846
  async function recordWorkflowInstanceId(input: {
@@ -0,0 +1,24 @@
1
+ export const DURABLE_OBJECT_DEPLOY_HANDOFF_STORAGE_ERROR =
2
+ "The Durable Object's code has been updated, this version can no longer access storage";
3
+
4
+ export const DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS = [50, 150, 350];
5
+
6
+ export function durableObjectDeployHandoffMessage(
7
+ value: unknown,
8
+ ): string | null {
9
+ const message = value instanceof Error ? value.message : String(value ?? '');
10
+ return message.includes(DURABLE_OBJECT_DEPLOY_HANDOFF_STORAGE_ERROR)
11
+ ? message
12
+ : null;
13
+ }
14
+
15
+ export async function sleepForDurableObjectDeployHandoffRetry(
16
+ attemptIndex: number,
17
+ ): Promise<void> {
18
+ const delayMs =
19
+ DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS[attemptIndex] ??
20
+ DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS[
21
+ DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS.length - 1
22
+ ];
23
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
24
+ }
@@ -76,6 +76,12 @@ import {
76
76
  type ToolExecuteResult,
77
77
  type ToolResultMetadataInput,
78
78
  } from '../../../shared_libs/play-runtime/tool-result';
79
+ import {
80
+ TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS,
81
+ TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
82
+ TOOL_EXECUTE_TRANSPORT_RETRY_DELAY_MS,
83
+ decideToolExecuteHttpRetry,
84
+ } from '../../../shared_libs/play-runtime/tool-execute-retry-policy';
79
85
  import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
80
86
  import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
81
87
  import type { PlayRuntimeManifestMap } from '../../../shared_libs/plays/compiler-manifest';
@@ -1223,12 +1229,11 @@ async function callToolDirect(
1223
1229
  ): Promise<ToolExecuteResult> {
1224
1230
  const { id, toolId, input } = args;
1225
1231
  const path = `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
1226
- const maxAttempts = 3;
1227
1232
  let lastError: Error | null = null;
1228
1233
 
1229
1234
  for (
1230
1235
  let attempt = 1;
1231
- attempt <= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS;
1236
+ attempt <= TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS;
1232
1237
  attempt += 1
1233
1238
  ) {
1234
1239
  let res: Response;
@@ -1247,18 +1252,18 @@ async function callToolDirect(
1247
1252
  } catch (error) {
1248
1253
  const message = error instanceof Error ? error.message : String(error);
1249
1254
  lastError = new Error(
1250
- `Tool ${toolId} transport failed calling ${path} for run ${req.runId} on attempt ${attempt}/${WORKER_TOOL_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
1255
+ `Tool ${toolId} transport failed calling ${path} for run ${req.runId} on attempt ${attempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
1251
1256
  );
1252
1257
  if (
1253
- attempt >= WORKER_TOOL_TRANSPORT_MAX_ATTEMPTS ||
1258
+ attempt >= TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS ||
1254
1259
  !isRetryableRuntimeApiError(error)
1255
1260
  ) {
1256
1261
  throw lastError;
1257
1262
  }
1258
1263
  onRetryAttempt?.();
1259
- const delayMs = WORKER_TOOL_TRANSPORT_RETRY_DELAY_MS * attempt;
1264
+ const delayMs = TOOL_EXECUTE_TRANSPORT_RETRY_DELAY_MS * attempt;
1260
1265
  console.warn(
1261
- `[deepline-run:${req.runId}] tool transport retry tool=${toolId} path=${path} attempt=${attempt}/${WORKER_TOOL_TRANSPORT_MAX_ATTEMPTS} retryAfterMs=${delayMs} error=${redactSecretsFromLogString(message)}`,
1266
+ `[deepline-run:${req.runId}] tool transport retry tool=${toolId} path=${path} attempt=${attempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS} retryAfterMs=${delayMs} error=${redactSecretsFromLogString(message)}`,
1262
1267
  );
1263
1268
  await sleepWorkerMs(delayMs);
1264
1269
  continue;
@@ -1276,18 +1281,25 @@ async function callToolDirect(
1276
1281
 
1277
1282
  const text = await res.text().catch(() => '');
1278
1283
  const isRateLimited = res.status === 429;
1279
- // Rate-limit pushback gets the larger 429-specific retry budget; every
1280
- // other failure keeps the generic 3-attempt budget.
1281
- const attemptCap = isRateLimited
1282
- ? WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS
1283
- : maxAttempts;
1284
+ const initialRetryDecision = decideToolExecuteHttpRetry({
1285
+ toolId,
1286
+ status: res.status,
1287
+ });
1284
1288
  lastError = normalizeToolHttpErrorMessage({
1285
1289
  toolId,
1286
1290
  status: res.status,
1287
1291
  attempt,
1288
- maxAttempts: attemptCap,
1292
+ maxAttempts: initialRetryDecision.attemptCap,
1289
1293
  bodyText: text,
1290
1294
  });
1295
+ // Rate-limit pushback gets the larger 429-specific retry budget, unless the
1296
+ // current response body is a hard Deepline billing denial.
1297
+ const retryDecision = decideToolExecuteHttpRetry({
1298
+ toolId,
1299
+ status: res.status,
1300
+ hardBillingFailure: isHardBillingToolHttpError(lastError),
1301
+ });
1302
+ const attemptCap = retryDecision.attemptCap;
1291
1303
  const retryAfterSeconds = Number(res.headers.get('retry-after'));
1292
1304
  const retryAfterMs =
1293
1305
  Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
@@ -1298,10 +1310,7 @@ async function callToolDirect(
1298
1310
  // final attempt so the (org, provider) bucket backs off across isolates.
1299
1311
  onProviderBackpressure?.(retryAfterMs > 0 ? retryAfterMs : 1_000);
1300
1312
  }
1301
- const retryable =
1302
- (isRateLimited && !isHardBillingToolHttpError(lastError)) ||
1303
- (res.status >= 500 && WORKER_RETRY_SAFE_5XX_TOOLS.has(toolId));
1304
- if (!retryable || attempt >= attemptCap) {
1313
+ if (!retryDecision.retryable || attempt >= attemptCap) {
1305
1314
  throw lastError;
1306
1315
  }
1307
1316
  // Charge the retry budget per attempt, matching the cjs runner's
@@ -1490,7 +1499,6 @@ const MAP_ROW_FAILURE_SAMPLE_LIMIT = 3;
1490
1499
  // their previous batching behavior; declared providers tighten via the
1491
1500
  // Governor's suggestedParallelism.
1492
1501
  const WORKER_TOOL_BATCH_DEFAULT_PARALLELISM = 4;
1493
- const WORKER_RETRY_SAFE_5XX_TOOLS = new Set(['test_transient_500']);
1494
1502
  /**
1495
1503
  * In-process retry budget for HTTP 429 tool responses. Rate-limit pushback is
1496
1504
  * throughput pacing (provider or Deepline limiter), not a tool defect, so it
@@ -1499,9 +1507,6 @@ const WORKER_RETRY_SAFE_5XX_TOOLS = new Set(['test_transient_500']);
1499
1507
  * throttling before the call fails. Every retry still charges the Governor's
1500
1508
  * retry budget, so a runaway storm stays bounded and loud.
1501
1509
  */
1502
- const WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS = 8;
1503
- const WORKER_TOOL_TRANSPORT_MAX_ATTEMPTS = 3;
1504
- const WORKER_TOOL_TRANSPORT_RETRY_DELAY_MS = 1_000;
1505
1510
 
1506
1511
  function sleepWorkerMs(ms: number): Promise<void> {
1507
1512
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2473,7 +2478,7 @@ async function postRuntimeEgressFetch(
2473
2478
  let lastError: Error | null = null;
2474
2479
  for (
2475
2480
  let attempt = 1;
2476
- attempt <= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS;
2481
+ attempt <= TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS;
2477
2482
  attempt += 1
2478
2483
  ) {
2479
2484
  const response = await fetchRuntimeApi(
@@ -2519,7 +2524,7 @@ async function postRuntimeEgressFetch(
2519
2524
  ? Math.ceil(retryAfterSeconds * 1000)
2520
2525
  : 1_000;
2521
2526
  onProviderBackpressure?.(retryAfterMs);
2522
- if (attempt >= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS) {
2527
+ if (attempt >= TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS) {
2523
2528
  throw lastError;
2524
2529
  }
2525
2530
  onRetryAttempt?.();
@@ -3943,6 +3948,13 @@ function createMinimalWorkerCtx(
3943
3948
  {
3944
3949
  completed: completedRowsForProgress(),
3945
3950
  total: progressTotalRows,
3951
+ startedRows: startedExecutedRows,
3952
+ activeRows: activeExecutedRows,
3953
+ waitingRows: Math.max(
3954
+ 0,
3955
+ rowsToExecute.length - startedExecutedRows,
3956
+ ),
3957
+ completedRows: completedExecutedRows,
3946
3958
  startedAt: mapStartedAt,
3947
3959
  message: formatMapExecutionHeartbeatMessage({
3948
3960
  rowsToExecute: rowsToExecute.length,
@@ -6147,6 +6159,18 @@ async function executeRunRequest(
6147
6159
  ...(typeof progress.failed === 'number'
6148
6160
  ? { failed: progress.failed }
6149
6161
  : {}),
6162
+ ...(typeof progress.startedRows === 'number'
6163
+ ? { startedRows: progress.startedRows }
6164
+ : {}),
6165
+ ...(typeof progress.activeRows === 'number'
6166
+ ? { activeRows: progress.activeRows }
6167
+ : {}),
6168
+ ...(typeof progress.waitingRows === 'number'
6169
+ ? { waitingRows: progress.waitingRows }
6170
+ : {}),
6171
+ ...(typeof progress.completedRows === 'number'
6172
+ ? { completedRows: progress.completedRows }
6173
+ : {}),
6150
6174
  ...(typeof progress.message === 'string' && progress.message
6151
6175
  ? { message: progress.message }
6152
6176
  : {}),
@@ -2,6 +2,10 @@ export type LiveNodeProgressSnapshot = {
2
2
  completed?: number;
3
3
  total?: number;
4
4
  failed?: number;
5
+ startedRows?: number;
6
+ activeRows?: number;
7
+ waitingRows?: number;
8
+ completedRows?: number;
5
9
  message?: string;
6
10
  updatedAt?: number;
7
11
  startedAt?: number;
@@ -1,262 +1,7 @@
1
- export class ToolHttpError extends Error {
2
- readonly billing: Record<string, unknown> | null;
3
- /** HTTP status of the failed tool-execute response (e.g. 429, 502). */
4
- readonly status: number;
5
-
6
- constructor(
7
- message: string,
8
- billing: Record<string, unknown> | null,
9
- status: number,
10
- ) {
11
- super(message);
12
- this.name = 'ToolHttpError';
13
- this.billing = billing;
14
- this.status = status;
15
- }
16
- }
17
-
18
- function formatCreditAmount(value: unknown): string {
19
- if (typeof value !== 'number' || !Number.isFinite(value)) {
20
- return String(value ?? '-');
21
- }
22
- return Number(value.toFixed(8)).toString();
23
- }
24
-
25
- function isRecord(value: unknown): value is Record<string, unknown> {
26
- return value !== null && typeof value === 'object' && !Array.isArray(value);
27
- }
28
-
29
- function getStringField(value: unknown, key: string): string | null {
30
- if (!isRecord(value)) return null;
31
- const field = value[key];
32
- return typeof field === 'string' && field.trim() ? field : null;
33
- }
34
-
35
- function getObjectField(
36
- value: unknown,
37
- key: string,
38
- ): Record<string, unknown> | null {
39
- if (!isRecord(value)) return null;
40
- const field = value[key];
41
- return isRecord(field) ? field : null;
42
- }
43
-
44
- function isInsufficientCreditsBilling(
45
- billing: Record<string, unknown> | null,
46
- ): billing is Record<string, unknown> {
47
- return billing?.kind === 'insufficient_credits';
48
- }
49
-
50
- function isHardBillingFailurePayload(
51
- payload: Record<string, unknown> | null,
52
- ): payload is Record<string, unknown> {
53
- if (!payload) return false;
54
- const category = String(
55
- payload.error_category ?? payload.errorCategory ?? '',
56
- ).toLowerCase();
57
- const code = String(payload.code ?? payload.error_code ?? '').toUpperCase();
58
- const message = String(
59
- payload.error ?? payload.message ?? payload.failure_description ?? '',
60
- ).toLowerCase();
61
- if (category === 'billing') return true;
62
- if (
63
- code === 'INSUFFICIENT_CREDITS' ||
64
- code === 'BILLING_CAP_EXCEEDED' ||
65
- code === 'MONTHLY_BILLING_LIMIT_EXCEEDED'
66
- ) {
67
- return true;
68
- }
69
- return (
70
- (message.includes('billing cap') ||
71
- message.includes('monthly billing limit') ||
72
- message.includes('rolling 30-day organization billing cap') ||
73
- message.includes('insufficient credits')) &&
74
- !message.includes('rate limit')
75
- );
76
- }
77
-
78
- function normalizeHardBillingPayload(
79
- payload: Record<string, unknown>,
80
- ): Record<string, unknown> {
81
- return {
82
- kind: 'billing_cap_exceeded',
83
- code:
84
- typeof payload.code === 'string' && payload.code.trim()
85
- ? payload.code
86
- : 'MONTHLY_BILLING_LIMIT_EXCEEDED',
87
- error_category: 'billing',
88
- failure_origin:
89
- typeof payload.failure_origin === 'string' &&
90
- payload.failure_origin.trim()
91
- ? payload.failure_origin
92
- : 'deepline_billing',
93
- message:
94
- typeof payload.error === 'string' && payload.error.trim()
95
- ? payload.error
96
- : typeof payload.message === 'string' && payload.message.trim()
97
- ? payload.message
98
- : 'Deepline billing cap exceeded.',
99
- ...payload,
100
- };
101
- }
102
-
103
- function formatHardBillingFailureMessage(input: {
104
- billing: Record<string, unknown>;
105
- toolId: string;
106
- status: number;
107
- attempt: number;
108
- maxAttempts: number;
109
- }): string {
110
- const code = getStringField(input.billing, 'code');
111
- const message =
112
- getStringField(input.billing, 'message') ??
113
- getStringField(input.billing, 'error') ??
114
- 'Deepline billing cap exceeded.';
115
- return [
116
- `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}:`,
117
- 'Deepline billing cap exceeded.',
118
- 'Run halted before marking remaining rows processed.',
119
- code ? `code=${code}.` : '',
120
- message,
121
- ]
122
- .filter(Boolean)
123
- .join(' ');
124
- }
125
-
126
- function formatInsufficientCreditsMessage(input: {
127
- billing: Record<string, unknown>;
128
- toolId: string;
129
- }): string {
130
- const operation =
131
- getStringField(input.billing, 'operation_id') ??
132
- getStringField(input.billing, 'operation') ??
133
- input.toolId;
134
- const balance = formatCreditAmount(input.billing.balance_credits);
135
- const required = formatCreditAmount(input.billing.required_credits);
136
- const recommended = formatCreditAmount(
137
- input.billing.recommended_add_credits ?? input.billing.needed_credits,
138
- );
139
- const billingUrl = getStringField(input.billing, 'billing_url');
140
- const addSuffix =
141
- billingUrl && recommended !== '-'
142
- ? ` Add >=${recommended} at ${billingUrl}.`
143
- : billingUrl
144
- ? ` Add credits at ${billingUrl}.`
145
- : '';
146
- return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
147
- }
148
-
149
- function formatPublicToolErrorPayload(input: {
150
- parsed: Record<string, unknown> | null;
151
- bodyText: string;
152
- }): string {
153
- if (!input.parsed) {
154
- return input.bodyText.slice(0, 500);
155
- }
156
-
157
- const selected: Record<string, unknown> = {};
158
- for (const key of [
159
- 'error',
160
- 'message',
161
- 'code',
162
- 'failure_origin',
163
- 'error_category',
164
- 'failure_description',
165
- 'operator_hint',
166
- 'failure_hint',
167
- 'details',
168
- 'provider',
169
- 'operation',
170
- 'request_id',
171
- 'requestId',
172
- 'credential_source',
173
- 'credential_owner',
174
- ]) {
175
- const value = input.parsed[key];
176
- if (typeof value === 'string' && value.trim()) {
177
- selected[key] = value;
178
- }
179
- }
180
-
181
- return JSON.stringify(
182
- Object.keys(selected).length > 0 ? selected : input.parsed,
183
- ).slice(0, 1_500);
184
- }
185
-
186
- export function normalizeToolHttpErrorMessage(input: {
187
- toolId: string;
188
- status: number;
189
- attempt: number;
190
- maxAttempts: number;
191
- bodyText: string;
192
- }): ToolHttpError {
193
- let parsed: Record<string, unknown> | null = null;
194
- try {
195
- const candidate = JSON.parse(input.bodyText);
196
- parsed = isRecord(candidate) ? candidate : null;
197
- } catch {
198
- parsed = null;
199
- }
200
- const billing = getObjectField(parsed, 'billing');
201
- if (isInsufficientCreditsBilling(billing)) {
202
- return new ToolHttpError(
203
- `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
204
- {
205
- billing,
206
- toolId: input.toolId,
207
- },
208
- )}`,
209
- billing,
210
- input.status,
211
- );
212
- }
213
- const hardBillingPayload = isHardBillingFailurePayload(billing)
214
- ? normalizeHardBillingPayload(billing)
215
- : isHardBillingFailurePayload(parsed)
216
- ? normalizeHardBillingPayload(parsed)
217
- : null;
218
- if (hardBillingPayload) {
219
- return new ToolHttpError(
220
- formatHardBillingFailureMessage({
221
- billing: hardBillingPayload,
222
- toolId: input.toolId,
223
- status: input.status,
224
- attempt: input.attempt,
225
- maxAttempts: input.maxAttempts,
226
- }),
227
- hardBillingPayload,
228
- input.status,
229
- );
230
- }
231
- return new ToolHttpError(
232
- `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatPublicToolErrorPayload(
233
- {
234
- parsed,
235
- bodyText: input.bodyText,
236
- },
237
- )}`,
238
- billing,
239
- input.status,
240
- );
241
- }
242
-
243
- export function extractErrorBilling(
244
- error: unknown,
245
- ): Record<string, unknown> | null {
246
- return error instanceof ToolHttpError ? error.billing : null;
247
- }
248
-
249
- export function isHardBillingToolHttpError(error: unknown): boolean {
250
- return (
251
- error instanceof ToolHttpError && isHardBillingFailurePayload(error.billing)
252
- );
253
- }
254
-
255
- /**
256
- * A tool call that ultimately failed with HTTP 429 — provider or
257
- * Deepline-internal rate-limit pushback that survived the in-process retry
258
- * budget. This is run-level throughput pressure, never a row-specific defect.
259
- */
260
- export function isRateLimitToolHttpError(error: unknown): boolean {
261
- return error instanceof ToolHttpError && error.status === 429;
262
- }
1
+ export {
2
+ ToolHttpError,
3
+ extractErrorBilling,
4
+ isHardBillingToolHttpError,
5
+ isRateLimitToolHttpError,
6
+ normalizeToolHttpErrorMessage,
7
+ } from '../../../../shared_libs/play-runtime/tool-http-errors';
@@ -75,6 +75,7 @@ import type {
75
75
  ToolSearchResult,
76
76
  ToolMetadata,
77
77
  CustomerDbQueryResult,
78
+ DeeplineAgentModelDescription,
78
79
  } from './types.js';
79
80
  import type { PlayStagedFileRef } from './plays/local-file-discovery.js';
80
81
  import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest.js';
@@ -1113,6 +1114,29 @@ export class DeeplineClient {
1113
1114
  );
1114
1115
  }
1115
1116
 
1117
+ /**
1118
+ * Describe a Deepline Agent model and its provider-specific option surface.
1119
+ *
1120
+ * Combines live AI Gateway model metadata with Deepline's generated AI SDK
1121
+ * provider option registry so agents can construct `providerOptions`
1122
+ * payloads before executing `deeplineagent`.
1123
+ *
1124
+ * The returned option schemas describe accepted provider option shapes, not
1125
+ * guaranteed support for every model. Runtime AI SDK/Gateway errors remain
1126
+ * authoritative for model-gated values.
1127
+ *
1128
+ * @param model - Gateway model id such as `"openai/gpt-5.5"`
1129
+ * @returns Model metadata, provider option shapes, and runnable examples
1130
+ */
1131
+ async describeModel(model: string): Promise<DeeplineAgentModelDescription> {
1132
+ return this.http.request<DeeplineAgentModelDescription>(
1133
+ `/api/v2/models/describe?model=${encodeURIComponent(model)}`,
1134
+ {
1135
+ method: 'GET',
1136
+ },
1137
+ );
1138
+ }
1139
+
1116
1140
  /**
1117
1141
  * Execute a tool and return the standard execution envelope.
1118
1142
  *
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
101
101
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
102
102
  // the SDK enrich generator's one-second stale policy.
103
103
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
104
- version: '0.1.140',
104
+ version: '0.1.142',
105
105
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
106
106
  supportPolicy: {
107
- latest: '0.1.140',
107
+ latest: '0.1.142',
108
108
  minimumSupported: '0.1.53',
109
109
  deprecatedBelow: '0.1.53',
110
110
  commandMinimumSupported: [
@@ -247,6 +247,38 @@ export interface ToolDefinition {
247
247
  }>;
248
248
  }
249
249
 
250
+ export interface ModelProviderOptionField {
251
+ name: string;
252
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
253
+ enumValues?: string[];
254
+ description: string;
255
+ caveat?: string;
256
+ }
257
+
258
+ export interface ModelProviderOptionNamespace {
259
+ provider: string;
260
+ sourcePackage: string;
261
+ sourceSymbol: string;
262
+ fields: ModelProviderOptionField[];
263
+ }
264
+
265
+ export interface DeeplineAgentModelDescription {
266
+ schemaVersion: 1;
267
+ model: string;
268
+ provider: string;
269
+ modelMetadata: Record<string, unknown> | null;
270
+ providerOptions: {
271
+ gateway: ModelProviderOptionNamespace;
272
+ selectedProvider?: ModelProviderOptionNamespace;
273
+ };
274
+ exampleInput: {
275
+ model: string;
276
+ providerOptions: Record<string, unknown>;
277
+ };
278
+ caveats: string[];
279
+ sources: string[];
280
+ }
281
+
250
282
  /**
251
283
  * Query options for ranked tool/provider discovery.
252
284
  */