deepline 0.1.108 → 0.1.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2849 -1407
- package/dist/cli/index.mjs +2587 -1152
- package/dist/index.d.mts +81 -17
- package/dist/index.d.ts +81 -17
- package/dist/index.js +179 -51
- package/dist/index.mjs +179 -51
- package/dist/repo/apps/play-runner-workers/src/child-play-submit.ts +196 -0
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +42 -21
- package/dist/repo/apps/play-runner-workers/src/entry.ts +162 -320
- package/dist/repo/apps/play-runner-workers/src/runtime/csv-rows.ts +102 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/dataset-handles.ts +8 -3
- package/dist/repo/apps/play-runner-workers/src/runtime/receipts.ts +18 -27
- package/dist/repo/apps/play-runner-workers/src/workflow-instance-create.ts +44 -0
- package/dist/repo/apps/play-runner-workers/src/workflow-retry.ts +7 -11
- package/dist/repo/sdk/src/client.ts +35 -12
- package/dist/repo/sdk/src/errors.ts +2 -2
- package/dist/repo/sdk/src/http.ts +109 -9
- package/dist/repo/sdk/src/index.ts +4 -0
- package/dist/repo/sdk/src/play.ts +77 -7
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +5 -1
- package/dist/repo/sdk/src/release.ts +14 -11
- package/dist/repo/sdk/src/tool-output.ts +2 -2
- package/dist/repo/sdk/src/types.ts +9 -6
- package/dist/repo/shared_libs/play-data-plane/cell-policy.ts +76 -0
- package/dist/repo/shared_libs/play-data-plane/column-names.ts +17 -0
- package/dist/repo/shared_libs/play-data-plane/sheet-contract.ts +190 -0
- package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +2 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +4 -0
- package/dist/repo/shared_libs/play-runtime/fullenrich-batching.ts +229 -0
- package/dist/repo/shared_libs/play-runtime/governor/policy.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/play-runtime-batching-registry.ts +20 -0
- package/dist/repo/shared_libs/play-runtime/providers.ts +5 -24
- package/dist/repo/shared_libs/play-runtime/run-failure.ts +20 -12
- package/dist/repo/shared_libs/play-runtime/run-ledger.ts +115 -25
- package/dist/repo/shared_libs/play-runtime/run-snapshot-stream.ts +49 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +22 -9
- package/dist/repo/shared_libs/play-runtime/secret-redaction.ts +15 -0
- package/dist/repo/shared_libs/play-runtime/work-receipts.ts +1 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +69 -11
- package/dist/repo/shared_libs/plays/static-pipeline.ts +4 -14
- package/dist/repo/shared_libs/security/outbound-url-policy.ts +238 -0
- package/dist/repo/shared_libs/security/safe-fetch.ts +118 -0
- package/dist/viewer/viewer.css +617 -0
- package/dist/viewer/viewer.js +1496 -0
- package/package.json +5 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneCsvAliasedRow,
|
|
3
|
+
stripCsvProjectionMetadata,
|
|
4
|
+
} from '../../../../shared_libs/play-runtime/csv-rename';
|
|
5
|
+
|
|
6
|
+
const CSV_TEMPLATE_CONTEXT = '__deeplineCsvProjectedValues';
|
|
7
|
+
const RUNTIME_STORAGE_FIELDS = [
|
|
8
|
+
'__deeplineRowKey',
|
|
9
|
+
'__deeplineCellMetaPatch',
|
|
10
|
+
'__deeplineRowStatus',
|
|
11
|
+
'__deeplineRowError',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type CsvRuntimeRow = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
function copyVisibleFields(
|
|
17
|
+
row: CsvRuntimeRow,
|
|
18
|
+
keepTemplateContext: boolean,
|
|
19
|
+
): CsvRuntimeRow {
|
|
20
|
+
const publicRow: CsvRuntimeRow = {};
|
|
21
|
+
for (const fieldName of Reflect.ownKeys(row)) {
|
|
22
|
+
if (
|
|
23
|
+
typeof fieldName === 'string' &&
|
|
24
|
+
fieldName.startsWith('__deepline') &&
|
|
25
|
+
(!keepTemplateContext || fieldName !== CSV_TEMPLATE_CONTEXT)
|
|
26
|
+
) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const descriptor = Object.getOwnPropertyDescriptor(row, fieldName);
|
|
30
|
+
if (!descriptor) continue;
|
|
31
|
+
Object.defineProperty(publicRow, fieldName, descriptor);
|
|
32
|
+
}
|
|
33
|
+
return publicRow;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function withCtx(
|
|
37
|
+
row: CsvRuntimeRow,
|
|
38
|
+
contextSource: CsvRuntimeRow = row,
|
|
39
|
+
): CsvRuntimeRow {
|
|
40
|
+
const ctx = contextSource[CSV_TEMPLATE_CONTEXT];
|
|
41
|
+
if (ctx) {
|
|
42
|
+
const values = ctx as CsvRuntimeRow;
|
|
43
|
+
for (const fieldName in values) {
|
|
44
|
+
delete row[fieldName];
|
|
45
|
+
row[fieldName] = values[fieldName];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function publicCsvInputRow<T extends CsvRuntimeRow>(row: T): T {
|
|
52
|
+
return copyVisibleFields(stripCsvProjectionMetadata(row), false) as T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runtimeCsvInputRow<T extends CsvRuntimeRow>(row: T): T {
|
|
56
|
+
return withCtx(copyVisibleFields(cloneCsvAliasedRow(row), true)) as T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function runtimeCsvExecutionRow<T extends CsvRuntimeRow>(
|
|
60
|
+
sourceRow: T,
|
|
61
|
+
pendingRow?: CsvRuntimeRow | null,
|
|
62
|
+
): T {
|
|
63
|
+
const source = runtimeCsvInputRow(sourceRow) as CsvRuntimeRow;
|
|
64
|
+
if (!pendingRow) return source as T;
|
|
65
|
+
const pending = runtimeCsvInputRow(pendingRow);
|
|
66
|
+
const executionRow = { ...pending, ...source };
|
|
67
|
+
if ('__deeplineCellMeta' in pendingRow) {
|
|
68
|
+
executionRow.__deeplineCellMeta = pendingRow.__deeplineCellMeta;
|
|
69
|
+
}
|
|
70
|
+
return withCtx(executionRow) as T;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function publicCsvOutputRow<T extends CsvRuntimeRow>(row: T): T {
|
|
74
|
+
return publicCsvInputRow(row);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function copyRuntimeStorageFields(
|
|
78
|
+
target: CsvRuntimeRow,
|
|
79
|
+
row: CsvRuntimeRow,
|
|
80
|
+
): CsvRuntimeRow {
|
|
81
|
+
for (const runtimeField of RUNTIME_STORAGE_FIELDS) {
|
|
82
|
+
if (runtimeField in row) {
|
|
83
|
+
target[runtimeField] = row[runtimeField];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return target;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function publicCsvStorageRow<T extends CsvRuntimeRow>(row: T): T {
|
|
90
|
+
const publicRow = publicCsvInputRow(row) as CsvRuntimeRow;
|
|
91
|
+
const storageRow: CsvRuntimeRow = {};
|
|
92
|
+
for (const fieldName of Reflect.ownKeys(publicRow)) {
|
|
93
|
+
if (typeof fieldName !== 'string') continue;
|
|
94
|
+
storageRow[fieldName] = publicRow[fieldName];
|
|
95
|
+
}
|
|
96
|
+
return copyRuntimeStorageFields(storageRow, row) as T;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function runtimeCsvStorageRow<T extends CsvRuntimeRow>(row: T): T {
|
|
100
|
+
const storageRow = publicCsvStorageRow(row) as CsvRuntimeRow;
|
|
101
|
+
return withCtx(storageRow, row) as T;
|
|
102
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyCsvRenameProjection,
|
|
3
|
+
cloneCsvAliasedRow,
|
|
3
4
|
stripCsvProjectedFields,
|
|
4
|
-
stripCsvProjectionMetadata,
|
|
5
5
|
type CsvRenameOptions,
|
|
6
6
|
} from '../../../../shared_libs/play-runtime/csv-rename';
|
|
7
7
|
import {
|
|
@@ -53,10 +53,15 @@ function cloneRow<T extends DatasetRow>(row: T): T {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function internalDatasetRow<T extends DatasetRow>(row: T): T {
|
|
56
|
-
const stripped =
|
|
56
|
+
const stripped = cloneCsvAliasedRow(row);
|
|
57
57
|
const publicRow: DatasetRow = {};
|
|
58
58
|
for (const key of Reflect.ownKeys(stripped)) {
|
|
59
|
-
if (
|
|
59
|
+
if (
|
|
60
|
+
typeof key === 'string' &&
|
|
61
|
+
/^__deepline(?!CsvProjectedValues$)/.test(key)
|
|
62
|
+
) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
60
65
|
const descriptor = Object.getOwnPropertyDescriptor(stripped, key);
|
|
61
66
|
if (!descriptor) continue;
|
|
62
67
|
Object.defineProperty(publicRow, key, descriptor);
|
|
@@ -41,22 +41,13 @@ function errorMessage(error: unknown): string {
|
|
|
41
41
|
return error instanceof Error ? error.message : String(error);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function runningReceiptError(
|
|
45
|
-
key: string,
|
|
46
|
-
receipt: WorkerRuntimeReceipt,
|
|
47
|
-
): Error {
|
|
48
|
-
return new Error(
|
|
49
|
-
`Runtime receipt ${key} is already running for run ${receipt.runId ?? 'unknown'}.`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
44
|
async function executeAndPersistReceipt<T>(input: {
|
|
54
45
|
key: string;
|
|
55
46
|
playName: string;
|
|
56
47
|
runId: string;
|
|
57
48
|
execute: () => Promise<T> | T;
|
|
58
49
|
receiptStore: WorkerRuntimeReceiptStore;
|
|
59
|
-
ownership: 'claimed' | '
|
|
50
|
+
ownership: 'claimed' | 'reconciled';
|
|
60
51
|
}): Promise<T> {
|
|
61
52
|
let output: T;
|
|
62
53
|
try {
|
|
@@ -83,9 +74,13 @@ async function executeAndPersistReceipt<T>(input: {
|
|
|
83
74
|
output,
|
|
84
75
|
});
|
|
85
76
|
if (!completed) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
(completed.status === 'completed' || completed.status === 'skipped') &&
|
|
81
|
+
completed.output !== undefined
|
|
82
|
+
) {
|
|
83
|
+
return receiptOutput<T>(completed);
|
|
89
84
|
}
|
|
90
85
|
return output;
|
|
91
86
|
}
|
|
@@ -94,6 +89,7 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
94
89
|
input: RuntimeReceiptContext & {
|
|
95
90
|
execute: () => Promise<T> | T;
|
|
96
91
|
repairRunningReceiptForSameRun?: boolean;
|
|
92
|
+
reclaimRunning?: boolean;
|
|
97
93
|
},
|
|
98
94
|
): Promise<T> {
|
|
99
95
|
const key = scopedReceiptKey(input);
|
|
@@ -102,25 +98,20 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
102
98
|
playName: input.playName,
|
|
103
99
|
runId: input.runId,
|
|
104
100
|
key,
|
|
101
|
+
...(input.reclaimRunning === true ? { reclaimRunning: true } : {}),
|
|
105
102
|
});
|
|
106
103
|
if (claimed.disposition === 'reused') {
|
|
107
104
|
return receiptOutput<T>(claimed.receipt);
|
|
108
105
|
}
|
|
109
106
|
if (claimed.disposition === 'running') {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
execute: input.execute,
|
|
119
|
-
receiptStore,
|
|
120
|
-
ownership: 'workflow_replay',
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
throw runningReceiptError(key, claimed.receipt);
|
|
107
|
+
return executeAndPersistReceipt({
|
|
108
|
+
key,
|
|
109
|
+
playName: input.playName,
|
|
110
|
+
runId: input.runId,
|
|
111
|
+
execute: input.execute,
|
|
112
|
+
receiptStore,
|
|
113
|
+
ownership: 'reconciled',
|
|
114
|
+
});
|
|
124
115
|
}
|
|
125
116
|
if (claimed.disposition === 'failed') {
|
|
126
117
|
throw new Error(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type WorkflowInstanceLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type WorkflowInstanceCreateResult<T extends WorkflowInstanceLike> = {
|
|
6
|
+
instance: T;
|
|
7
|
+
startMode: 'created' | 'reattached_existing';
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function isWorkflowInstanceAlreadyExistsError(error: unknown): boolean {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
return /instance[._ -]?already[._ -]?exists|Instance already exists/i.test(
|
|
13
|
+
message,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createOrAttachWorkflowInstance<
|
|
18
|
+
T extends WorkflowInstanceLike,
|
|
19
|
+
>(input: {
|
|
20
|
+
create: () => Promise<T>;
|
|
21
|
+
getExisting: () => Promise<T>;
|
|
22
|
+
}): Promise<WorkflowInstanceCreateResult<T>> {
|
|
23
|
+
try {
|
|
24
|
+
return { instance: await input.create(), startMode: 'created' };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!isWorkflowInstanceAlreadyExistsError(error)) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return {
|
|
31
|
+
instance: await input.getExisting(),
|
|
32
|
+
startMode: 'reattached_existing',
|
|
33
|
+
};
|
|
34
|
+
} catch (attachError) {
|
|
35
|
+
const message =
|
|
36
|
+
attachError instanceof Error
|
|
37
|
+
? attachError.message
|
|
38
|
+
: String(attachError);
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Workflow instance already exists but could not be reattached: ${message}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isCloudflareDurableObjectCodeUpdatedError,
|
|
3
|
-
normalizePlayRunFailure,
|
|
4
|
-
} from '../../../shared_libs/play-runtime/run-failure';
|
|
1
|
+
import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
|
|
5
2
|
|
|
6
3
|
export const PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT = 1;
|
|
7
4
|
|
|
@@ -26,25 +23,24 @@ export function decideWorkflowPlatformRetry(input: {
|
|
|
26
23
|
if (!input.error) {
|
|
27
24
|
return { action: 'fail', reason: null, message: null };
|
|
28
25
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
);
|
|
26
|
+
const failure = normalizePlayRunFailure(input.error);
|
|
27
|
+
const retryablePlatformReset = failure.code === 'PLATFORM_DEPLOY_INTERRUPTED';
|
|
32
28
|
if (
|
|
33
29
|
retryablePlatformReset &&
|
|
34
30
|
input.retryAttempts < PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT
|
|
35
31
|
) {
|
|
36
32
|
return {
|
|
37
33
|
action: 'retry',
|
|
38
|
-
reason: '
|
|
34
|
+
reason: 'cloudflare_durable_object_reset',
|
|
39
35
|
nextAttempt: input.retryAttempts + 1,
|
|
40
|
-
message:
|
|
36
|
+
message: failure.message,
|
|
41
37
|
};
|
|
42
38
|
}
|
|
43
39
|
return {
|
|
44
40
|
action: 'fail',
|
|
45
41
|
reason: retryablePlatformReset
|
|
46
|
-
? '
|
|
42
|
+
? 'cloudflare_durable_object_reset_retry_exhausted'
|
|
47
43
|
: null,
|
|
48
|
-
message:
|
|
44
|
+
message: failure.message,
|
|
49
45
|
};
|
|
50
46
|
}
|
|
@@ -413,11 +413,14 @@ export type BillingPlanEntry = {
|
|
|
413
413
|
};
|
|
414
414
|
|
|
415
415
|
/** One usage metric published in the billing catalog. */
|
|
416
|
-
export type
|
|
416
|
+
export type BillingMeterEntry = {
|
|
417
417
|
id: string;
|
|
418
418
|
name: string;
|
|
419
419
|
};
|
|
420
420
|
|
|
421
|
+
/** @deprecated Use BillingMeterEntry. Retained for wire/API alias compatibility. */
|
|
422
|
+
export type BillingMetricEntry = BillingMeterEntry;
|
|
423
|
+
|
|
421
424
|
/** The caller's active plan as reported by the plans endpoint. */
|
|
422
425
|
export type BillingActivePlan = {
|
|
423
426
|
plan_family_id: string;
|
|
@@ -440,6 +443,8 @@ export type BillingPlansResult = {
|
|
|
440
443
|
catalog_version: string;
|
|
441
444
|
active_plan: BillingActivePlan;
|
|
442
445
|
plans: BillingPlanEntry[];
|
|
446
|
+
meters: BillingMeterEntry[];
|
|
447
|
+
/** @deprecated Use meters. Retained for installed SDK compatibility. */
|
|
443
448
|
metrics: BillingMetricEntry[];
|
|
444
449
|
};
|
|
445
450
|
|
|
@@ -1075,12 +1080,12 @@ export class DeeplineClient {
|
|
|
1075
1080
|
* Returns everything from {@link ToolDefinition} plus pricing info, sample
|
|
1076
1081
|
* inputs/outputs, failure modes, and cost estimates.
|
|
1077
1082
|
*
|
|
1078
|
-
* @param toolId - Tool identifier (e.g. `"
|
|
1083
|
+
* @param toolId - Tool identifier (e.g. `"dropleads_search_people"`)
|
|
1079
1084
|
* @returns Full tool metadata
|
|
1080
1085
|
*
|
|
1081
1086
|
* @example
|
|
1082
1087
|
* ```typescript
|
|
1083
|
-
* const meta = await client.getTool('
|
|
1088
|
+
* const meta = await client.getTool('dropleads_search_people');
|
|
1084
1089
|
* console.log(`Cost: ${meta.estimatedCreditsRange} credits`);
|
|
1085
1090
|
* console.log(`Input schema:`, meta.inputSchema);
|
|
1086
1091
|
* ```
|
|
@@ -1120,6 +1125,7 @@ export class DeeplineClient {
|
|
|
1120
1125
|
`/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
|
|
1121
1126
|
{ payload: input },
|
|
1122
1127
|
headers,
|
|
1128
|
+
{ forbiddenAsApiError: true },
|
|
1123
1129
|
);
|
|
1124
1130
|
}
|
|
1125
1131
|
|
|
@@ -1228,7 +1234,7 @@ export class DeeplineClient {
|
|
|
1228
1234
|
? { waitForCompletionMs: request.waitForCompletionMs }
|
|
1229
1235
|
: {}),
|
|
1230
1236
|
// Profile selection is the API's job, not the CLI's. The server
|
|
1231
|
-
//
|
|
1237
|
+
// defaults to workers_edge; tests and runtime probes that want a
|
|
1232
1238
|
// different profile pass `request.profile` explicitly.
|
|
1233
1239
|
...(request.profile ? { profile: request.profile } : {}),
|
|
1234
1240
|
},
|
|
@@ -1338,10 +1344,15 @@ export class DeeplineClient {
|
|
|
1338
1344
|
sourceFiles: input.sourceFiles,
|
|
1339
1345
|
artifact: input.artifact,
|
|
1340
1346
|
}));
|
|
1341
|
-
return this.http.post(
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1347
|
+
return this.http.post(
|
|
1348
|
+
'/api/v2/plays/artifacts',
|
|
1349
|
+
{
|
|
1350
|
+
...input,
|
|
1351
|
+
compilerManifest,
|
|
1352
|
+
},
|
|
1353
|
+
undefined,
|
|
1354
|
+
{ forbiddenAsApiError: true, retryApiErrors: true },
|
|
1355
|
+
);
|
|
1345
1356
|
}
|
|
1346
1357
|
|
|
1347
1358
|
/**
|
|
@@ -1379,7 +1390,12 @@ export class DeeplineClient {
|
|
|
1379
1390
|
}>;
|
|
1380
1391
|
}> {
|
|
1381
1392
|
if (artifacts.length === 0) {
|
|
1382
|
-
return this.http.post(
|
|
1393
|
+
return this.http.post(
|
|
1394
|
+
'/api/v2/plays/artifacts',
|
|
1395
|
+
{ artifacts },
|
|
1396
|
+
undefined,
|
|
1397
|
+
{ forbiddenAsApiError: true, retryApiErrors: true },
|
|
1398
|
+
);
|
|
1383
1399
|
}
|
|
1384
1400
|
const compiledArtifacts = await mapWithConcurrency(
|
|
1385
1401
|
artifacts,
|
|
@@ -1414,9 +1430,14 @@ export class DeeplineClient {
|
|
|
1414
1430
|
triggerMetadata?: unknown;
|
|
1415
1431
|
triggerBindings?: unknown;
|
|
1416
1432
|
}>;
|
|
1417
|
-
}>(
|
|
1418
|
-
artifacts
|
|
1419
|
-
|
|
1433
|
+
}>(
|
|
1434
|
+
'/api/v2/plays/artifacts',
|
|
1435
|
+
{
|
|
1436
|
+
artifacts: chunk,
|
|
1437
|
+
},
|
|
1438
|
+
undefined,
|
|
1439
|
+
{ forbiddenAsApiError: true, retryApiErrors: true },
|
|
1440
|
+
),
|
|
1420
1441
|
);
|
|
1421
1442
|
}
|
|
1422
1443
|
return {
|
|
@@ -2446,6 +2467,8 @@ export class DeeplineClient {
|
|
|
2446
2467
|
return this.http.post<PublishPlayVersionResult>(
|
|
2447
2468
|
`/api/v2/plays/${encodedName}/live`,
|
|
2448
2469
|
request,
|
|
2470
|
+
undefined,
|
|
2471
|
+
{ forbiddenAsApiError: true },
|
|
2449
2472
|
);
|
|
2450
2473
|
}
|
|
2451
2474
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* const client = new DeeplineClient();
|
|
12
12
|
* try {
|
|
13
|
-
* await client.executeTool('
|
|
13
|
+
* await client.executeTool('dropleads_search_people', { query: 'cto' });
|
|
14
14
|
* } catch (err) {
|
|
15
15
|
* if (err instanceof AuthError) {
|
|
16
16
|
* console.error('Bad API key — run: deepline auth register');
|
|
@@ -77,7 +77,7 @@ export class AuthError extends DeeplineError {
|
|
|
77
77
|
* import { RateLimitError } from 'deepline';
|
|
78
78
|
*
|
|
79
79
|
* try {
|
|
80
|
-
* await client.executeTool('
|
|
80
|
+
* await client.executeTool('dropleads_search_people', { query: 'cto' });
|
|
81
81
|
* } catch (err) {
|
|
82
82
|
* if (err instanceof RateLimitError) {
|
|
83
83
|
* console.log(`Retry after ${err.retryAfterMs}ms`);
|
|
@@ -29,11 +29,14 @@ import { baseUrlSlug } from './config.js';
|
|
|
29
29
|
import {
|
|
30
30
|
COORDINATOR_INTERNAL_TOKEN_HEADER,
|
|
31
31
|
COORDINATOR_URL_OVERRIDE_HEADER,
|
|
32
|
+
RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER,
|
|
32
33
|
SYNTHETIC_RUN_HEADER,
|
|
33
34
|
WORKER_CALLBACK_URL_OVERRIDE_HEADER,
|
|
34
35
|
} from '../../shared_libs/play-runtime/coordinator-headers.js';
|
|
35
36
|
|
|
36
37
|
const MAX_DIAGNOSTIC_HEADER_LENGTH = 120;
|
|
38
|
+
const COWORK_NETWORK_HINT =
|
|
39
|
+
'Claude Cowork appears to be running Deepline in a network-restricted sandbox. In Claude Desktop, open Settings > Capabilities, turn on Allow network egress, and set Domain allowlist to All domains for the Cowork session.';
|
|
37
40
|
|
|
38
41
|
interface RequestOptions {
|
|
39
42
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
@@ -50,6 +53,11 @@ interface RequestOptions {
|
|
|
50
53
|
* {@link AuthError}.
|
|
51
54
|
*/
|
|
52
55
|
forbiddenAsApiError?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Retry HTTP 408/425/5xx JSON API responses. Use only for idempotent requests;
|
|
58
|
+
* most mutating API calls must fail loudly after the first server response.
|
|
59
|
+
*/
|
|
60
|
+
retryApiErrors?: boolean;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
interface StreamOptions {
|
|
@@ -113,6 +121,7 @@ export class HttpClient {
|
|
|
113
121
|
'User-Agent': `deepline-ts-sdk/${SDK_VERSION}`,
|
|
114
122
|
'X-Deepline-Client-Family': 'sdk',
|
|
115
123
|
'X-Deepline-CLI-Family': 'sdk',
|
|
124
|
+
'X-Deepline-Agent-Runtime': detectAgentRuntime(),
|
|
116
125
|
'X-Deepline-CLI-Version': SDK_VERSION,
|
|
117
126
|
'X-Deepline-SDK-Version': SDK_VERSION,
|
|
118
127
|
'X-Deepline-API-Contract': SDK_API_CONTRACT,
|
|
@@ -158,6 +167,14 @@ export class HttpClient {
|
|
|
158
167
|
if (workerCallbackUrl?.trim()) {
|
|
159
168
|
headers[WORKER_CALLBACK_URL_OVERRIDE_HEADER] = workerCallbackUrl.trim();
|
|
160
169
|
}
|
|
170
|
+
const runtimeSchedulerSchema =
|
|
171
|
+
typeof process !== 'undefined'
|
|
172
|
+
? process.env?.DEEPLINE_RUNTIME_SCHEDULER_SCHEMA
|
|
173
|
+
: undefined;
|
|
174
|
+
if (runtimeSchedulerSchema?.trim()) {
|
|
175
|
+
headers[RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER] =
|
|
176
|
+
runtimeSchedulerSchema.trim();
|
|
177
|
+
}
|
|
161
178
|
// Automated test harnesses (e.g. tests/v2-plays) set DEEPLINE_SYNTHETIC_RUN
|
|
162
179
|
// so their intentionally failing plays do not page the SDK CLI error
|
|
163
180
|
// channel. Honored server-side only in non-prod (see plays/run route).
|
|
@@ -261,6 +278,9 @@ export class HttpClient {
|
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
if (!response.ok) {
|
|
281
|
+
const retryableApiError =
|
|
282
|
+
options?.retryApiErrors === true &&
|
|
283
|
+
isRetryableApiErrorResponse(response.status);
|
|
264
284
|
// A 5xx that escaped a Worker can return Cloudflare's default HTML
|
|
265
285
|
// error page. Never surface raw HTML to CLI users: replace the
|
|
266
286
|
// message with a structured summary and store a short flag in
|
|
@@ -271,7 +291,7 @@ export class HttpClient {
|
|
|
271
291
|
response.headers.get('content-type'),
|
|
272
292
|
);
|
|
273
293
|
if (htmlError) {
|
|
274
|
-
|
|
294
|
+
lastError = new DeeplineError(
|
|
275
295
|
htmlError.message(response.status),
|
|
276
296
|
response.status,
|
|
277
297
|
'API_ERROR',
|
|
@@ -283,6 +303,11 @@ export class HttpClient {
|
|
|
283
303
|
: {}),
|
|
284
304
|
},
|
|
285
305
|
);
|
|
306
|
+
if (retryableApiError && attempt < this.config.maxRetries) {
|
|
307
|
+
retryAfterDelayMs = parseOptionalRetryAfter(response);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
throw lastError;
|
|
286
311
|
}
|
|
287
312
|
const errorValue =
|
|
288
313
|
typeof parsed === 'object' && parsed && 'error' in parsed
|
|
@@ -304,9 +329,20 @@ export class HttpClient {
|
|
|
304
329
|
'string'
|
|
305
330
|
? (parsed as Record<string, string>).message
|
|
306
331
|
: `HTTP ${response.status}`;
|
|
307
|
-
|
|
332
|
+
const apiErrorCode =
|
|
333
|
+
errorValue &&
|
|
334
|
+
typeof errorValue === 'object' &&
|
|
335
|
+
typeof (errorValue as Record<string, unknown>).code === 'string'
|
|
336
|
+
? (errorValue as Record<string, string>).code
|
|
337
|
+
: 'API_ERROR';
|
|
338
|
+
lastError = new DeeplineError(msg, response.status, apiErrorCode, {
|
|
308
339
|
response: parsed,
|
|
309
340
|
});
|
|
341
|
+
if (retryableApiError && attempt < this.config.maxRetries) {
|
|
342
|
+
retryAfterDelayMs = parseOptionalRetryAfter(response);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
throw lastError;
|
|
310
346
|
}
|
|
311
347
|
|
|
312
348
|
return parsed as T;
|
|
@@ -332,7 +368,7 @@ export class HttpClient {
|
|
|
332
368
|
const errorMessage = lastError?.message
|
|
333
369
|
? `Unable to connect to ${baseUrl}. ${lastError.message}`
|
|
334
370
|
: `Unable to connect to ${baseUrl}. Is the computer able to access the url?`;
|
|
335
|
-
throw new DeeplineError(errorMessage);
|
|
371
|
+
throw new DeeplineError(withCoworkNetworkHint(errorMessage));
|
|
336
372
|
}
|
|
337
373
|
|
|
338
374
|
/**
|
|
@@ -418,9 +454,11 @@ export class HttpClient {
|
|
|
418
454
|
}
|
|
419
455
|
|
|
420
456
|
throw new DeeplineError(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
457
|
+
withCoworkNetworkHint(
|
|
458
|
+
lastError?.message
|
|
459
|
+
? `Unable to stream from ${this.config.baseUrl}. ${lastError.message}`
|
|
460
|
+
: `Unable to stream from ${this.config.baseUrl}.`,
|
|
461
|
+
),
|
|
424
462
|
);
|
|
425
463
|
}
|
|
426
464
|
|
|
@@ -435,11 +473,16 @@ export class HttpClient {
|
|
|
435
473
|
path: string,
|
|
436
474
|
body: unknown,
|
|
437
475
|
headers?: Record<string, string>,
|
|
476
|
+
options?: Pick<
|
|
477
|
+
RequestOptions,
|
|
478
|
+
'forbiddenAsApiError' | 'retryApiErrors' | 'timeout'
|
|
479
|
+
>,
|
|
438
480
|
): Promise<T> {
|
|
439
481
|
return this.request<T>(path, {
|
|
440
482
|
method: 'POST',
|
|
441
483
|
body,
|
|
442
484
|
headers,
|
|
485
|
+
...options,
|
|
443
486
|
});
|
|
444
487
|
}
|
|
445
488
|
|
|
@@ -455,12 +498,23 @@ export class HttpClient {
|
|
|
455
498
|
});
|
|
456
499
|
}
|
|
457
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Send a PATCH request with a JSON body.
|
|
503
|
+
*
|
|
504
|
+
* @typeParam T - Expected response body type
|
|
505
|
+
* @param path - API path
|
|
506
|
+
* @param body - JSON-serializable request body
|
|
507
|
+
*/
|
|
458
508
|
async patch<T = unknown>(
|
|
459
509
|
path: string,
|
|
460
|
-
body
|
|
510
|
+
body?: unknown,
|
|
461
511
|
headers?: Record<string, string>,
|
|
462
512
|
): Promise<T> {
|
|
463
|
-
return this.request<T>(path, {
|
|
513
|
+
return this.request<T>(path, {
|
|
514
|
+
method: 'PATCH',
|
|
515
|
+
body,
|
|
516
|
+
headers,
|
|
517
|
+
});
|
|
464
518
|
}
|
|
465
519
|
|
|
466
520
|
/**
|
|
@@ -482,6 +536,10 @@ function parseResponseBody(body: string): unknown {
|
|
|
482
536
|
}
|
|
483
537
|
}
|
|
484
538
|
|
|
539
|
+
function isRetryableApiErrorResponse(status: number): boolean {
|
|
540
|
+
return status === 408 || status === 425 || (status >= 500 && status < 600);
|
|
541
|
+
}
|
|
542
|
+
|
|
485
543
|
/**
|
|
486
544
|
* Detect a raw Cloudflare HTML error page in an error response body.
|
|
487
545
|
*
|
|
@@ -559,6 +617,10 @@ function apiErrorMessage(parsed: unknown, status: number): string {
|
|
|
559
617
|
|
|
560
618
|
/** Parse the `Retry-After` header as milliseconds. Falls back to 5000ms. */
|
|
561
619
|
function parseRetryAfter(response: Response): number {
|
|
620
|
+
return parseOptionalRetryAfter(response) ?? 5000;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function parseOptionalRetryAfter(response: Response): number | null {
|
|
562
624
|
const header = response.headers.get('retry-after');
|
|
563
625
|
if (header) {
|
|
564
626
|
const seconds = Number(header);
|
|
@@ -566,7 +628,7 @@ function parseRetryAfter(response: Response): number {
|
|
|
566
628
|
return seconds * 1000;
|
|
567
629
|
}
|
|
568
630
|
}
|
|
569
|
-
return
|
|
631
|
+
return null;
|
|
570
632
|
}
|
|
571
633
|
|
|
572
634
|
/**
|
|
@@ -650,3 +712,41 @@ function decodeSseFrame<TEvent extends LiveEventEnvelope>(
|
|
|
650
712
|
function sleep(ms: number): Promise<void> {
|
|
651
713
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
652
714
|
}
|
|
715
|
+
|
|
716
|
+
function isTruthyEnv(name: string): boolean {
|
|
717
|
+
const normalized = process.env[name]?.trim().toLowerCase();
|
|
718
|
+
return ['1', 'true', 'yes', 'on'].includes(normalized ?? '');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function isCoworkLikeSandbox(): boolean {
|
|
722
|
+
const pluginMode = isTruthyEnv('DEEPLINE_PLUGIN_MODE');
|
|
723
|
+
const claudeRemote = isTruthyEnv('CLAUDE_CODE_REMOTE');
|
|
724
|
+
const projectDir = Boolean(process.env.CLAUDE_PROJECT_DIR?.trim());
|
|
725
|
+
const pluginRoot = Boolean(process.env.DEEPLINE_PLUGIN_ROOT?.trim());
|
|
726
|
+
const home = process.env.HOME?.trim() ?? '';
|
|
727
|
+
const sessionHome = home.startsWith('/sessions/');
|
|
728
|
+
return (
|
|
729
|
+
(pluginMode || pluginRoot) && (claudeRemote || projectDir || sessionHome)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function detectAgentRuntime(): string {
|
|
734
|
+
if (process.env.CODEX_THREAD_ID?.trim()) return 'codex';
|
|
735
|
+
if (isCoworkLikeSandbox()) return 'claude_cowork';
|
|
736
|
+
if (process.env.CLAUDECODE?.trim() === '1') return 'claude_code';
|
|
737
|
+
if (process.env.CLINE_ACTIVE?.trim().toLowerCase() === 'true') return 'cline';
|
|
738
|
+
if (process.env.CURSOR_TRACE_ID?.trim() || process.env.CURSOR_AGENT?.trim()) {
|
|
739
|
+
return 'cursor';
|
|
740
|
+
}
|
|
741
|
+
if (process.env.WINDSURF?.trim() || process.env.CASCADE?.trim()) {
|
|
742
|
+
return 'windsurf';
|
|
743
|
+
}
|
|
744
|
+
return 'unknown';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function withCoworkNetworkHint(message: string): string {
|
|
748
|
+
if (!isCoworkLikeSandbox() || message.includes(COWORK_NETWORK_HINT)) {
|
|
749
|
+
return message;
|
|
750
|
+
}
|
|
751
|
+
return `${message}\n${COWORK_NETWORK_HINT}`;
|
|
752
|
+
}
|