deepline 0.1.78 → 0.1.80

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 (26) hide show
  1. package/dist/cli/index.js +69 -37
  2. package/dist/cli/index.mjs +69 -37
  3. package/dist/index.d.mts +32 -1
  4. package/dist/index.d.ts +32 -1
  5. package/dist/index.js +7 -4
  6. package/dist/index.mjs +7 -4
  7. package/dist/repo/apps/play-runner-workers/src/child-play-await.ts +192 -0
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +1320 -1644
  9. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +515 -648
  10. package/dist/repo/apps/play-runner-workers/src/entry.ts +896 -354
  11. package/dist/repo/apps/play-runner-workers/src/workflow-retry-state.ts +209 -0
  12. package/dist/repo/sdk/src/client.ts +9 -2
  13. package/dist/repo/sdk/src/release.ts +2 -2
  14. package/dist/repo/sdk/src/types.ts +5 -0
  15. package/dist/repo/shared_libs/play-runtime/governor/coordinator-rate-state-backend.ts +231 -0
  16. package/dist/repo/shared_libs/play-runtime/governor/governor.ts +376 -0
  17. package/dist/repo/shared_libs/play-runtime/governor/policy.ts +179 -0
  18. package/dist/repo/shared_libs/play-runtime/governor/rate-state-backend.ts +87 -0
  19. package/dist/repo/shared_libs/play-runtime/run-failure.ts +12 -0
  20. package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +24 -0
  21. package/dist/repo/shared_libs/play-runtime/submit-limits.ts +35 -0
  22. package/dist/repo/shared_libs/plays/bundling/index.ts +4 -12
  23. package/dist/repo/shared_libs/plays/bundling/limits.ts +29 -0
  24. package/dist/repo/shared_libs/plays/static-pipeline.ts +314 -1
  25. package/dist/repo/shared_libs/temporal/constants.ts +38 -0
  26. package/package.json +1 -1
@@ -0,0 +1,209 @@
1
+ import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
2
+ import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
3
+ import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
4
+ import type {
5
+ PlayRuntimeManifest,
6
+ PlayRuntimeManifestMap,
7
+ } from '../../../shared_libs/plays/compiler-manifest';
8
+
9
+ import {
10
+ PLAY_SUBMIT_INPUT_INLINE_MAX_BYTES,
11
+ PLAY_SUBMIT_INPUT_MAX_BYTES,
12
+ } from '../../../shared_libs/play-runtime/submit-limits';
13
+
14
+ export const WORKFLOW_RETRY_STATE_TARGET_BYTES =
15
+ PLAY_SUBMIT_INPUT_INLINE_MAX_BYTES;
16
+ export const WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES =
17
+ WORKFLOW_RETRY_STATE_TARGET_BYTES;
18
+ export const WORKFLOW_RETRY_PARAMS_MAX_BYTES = PLAY_SUBMIT_INPUT_MAX_BYTES;
19
+
20
+ export type WorkflowRetryParamsRef = {
21
+ storageKind: 'r2';
22
+ storageKey: string;
23
+ bytes: number;
24
+ hash: string;
25
+ expiresAt: number;
26
+ };
27
+
28
+ export type WorkflowRetryPlayParams = {
29
+ runId: string;
30
+ playId: string;
31
+ playName: string;
32
+ artifactStorageKey: string;
33
+ artifactHash: string;
34
+ graphHash: string;
35
+ input: Record<string, unknown>;
36
+ inputFile?: {
37
+ name?: string;
38
+ r2Key?: string;
39
+ storageKey?: string;
40
+ path?: string;
41
+ fileName?: string;
42
+ logicalPath?: string;
43
+ contentType?: string;
44
+ bytes?: number;
45
+ } | null;
46
+ inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
47
+ packagedFiles?: Array<{
48
+ playPath: string;
49
+ storageKey: string;
50
+ contentType?: string;
51
+ bytes?: number;
52
+ inlineText?: string;
53
+ }> | null;
54
+ contractSnapshot?: unknown;
55
+ executionPlan?: ExecutionPlan | null;
56
+ childPlayManifests?: PlayRuntimeManifestMap | null;
57
+ playCallGovernance?: PlayCallGovernanceSnapshot | null;
58
+ preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
59
+ preloadedDbSessionRef?: {
60
+ runId: string;
61
+ sessionCount: number;
62
+ expiresAt: number;
63
+ } | null;
64
+ dynamicWorkerCode?: string | null;
65
+ executorToken: string;
66
+ baseUrl: string;
67
+ orgId: string;
68
+ userEmail: string;
69
+ userId?: string | null;
70
+ runtimeBackend: string;
71
+ dedupBackend: string;
72
+ totalRows?: number;
73
+ coordinatorUrl?: string | null;
74
+ coordinatorInternalToken?: string | null;
75
+ };
76
+
77
+ export type WorkflowRetryStatePayload<TParams = WorkflowRetryPlayParams> =
78
+ | {
79
+ params: TParams;
80
+ paramsRef?: null;
81
+ paramsBytes: number;
82
+ }
83
+ | {
84
+ params: null;
85
+ paramsRef: WorkflowRetryParamsRef;
86
+ paramsBytes: number;
87
+ };
88
+
89
+ export function buildWorkflowRetryParams<
90
+ TParams extends WorkflowRetryPlayParams,
91
+ >(params: TParams): TParams {
92
+ const retryParams = {
93
+ ...params,
94
+ dynamicWorkerCode: null,
95
+ contractSnapshot: stripRetrySourceSnapshot(params.contractSnapshot),
96
+ childPlayManifests: stripRetryChildManifestCode(params.childPlayManifests),
97
+ packagedFiles: stripRetryPackagedFiles(params.packagedFiles),
98
+ } satisfies WorkflowRetryPlayParams as TParams;
99
+ if (jsonByteLength(retryParams) <= WORKFLOW_RETRY_STATE_TARGET_BYTES) {
100
+ return retryParams;
101
+ }
102
+ return {
103
+ ...retryParams,
104
+ contractSnapshot: stripRetryContractSnapshotToArtifact(
105
+ retryParams.contractSnapshot,
106
+ params,
107
+ ),
108
+ childPlayManifests: stripRetryChildManifestToArtifact(
109
+ retryParams.childPlayManifests,
110
+ ),
111
+ } satisfies WorkflowRetryPlayParams as TParams;
112
+ }
113
+
114
+ export function jsonByteLength(value: unknown): number {
115
+ return new TextEncoder().encode(JSON.stringify(value)).length;
116
+ }
117
+
118
+ export function workflowRetryParamsStorageKey(input: {
119
+ runId: string;
120
+ hash: string;
121
+ }): string {
122
+ const safeRunId = input.runId
123
+ .toLowerCase()
124
+ .replace(/[^a-z0-9_-]+/g, '-')
125
+ .slice(0, 140);
126
+ return `plays/workflow-retry-params/${safeRunId || 'run'}/${input.hash}.json`;
127
+ }
128
+
129
+ function stripRetryPackagedFiles(
130
+ files: WorkflowRetryPlayParams['packagedFiles'],
131
+ ): WorkflowRetryPlayParams['packagedFiles'] {
132
+ return (
133
+ files?.map((file) => ({
134
+ playPath: file.playPath,
135
+ storageKey: file.storageKey,
136
+ contentType: file.contentType,
137
+ bytes: file.bytes,
138
+ })) ?? null
139
+ );
140
+ }
141
+
142
+ function stripRetrySourceSnapshot(snapshot: unknown): unknown {
143
+ if (!isRecord(snapshot)) return snapshot;
144
+ const rest = { ...snapshot };
145
+ delete rest.sourceCode;
146
+ delete rest.sourceFiles;
147
+ delete rest.bundledCode;
148
+ return rest;
149
+ }
150
+
151
+ function stripRetryContractSnapshotToArtifact(
152
+ snapshot: unknown,
153
+ params: WorkflowRetryPlayParams,
154
+ ): unknown {
155
+ if (!isRecord(snapshot)) return snapshot;
156
+ return {
157
+ source: snapshot.source ?? 'artifact',
158
+ revisionVersion: snapshot.revisionVersion ?? null,
159
+ staticPipeline: snapshot.staticPipeline ?? null,
160
+ billingLimit: snapshot.billingLimit ?? null,
161
+ artifactMetadata: {
162
+ storageKey: params.artifactStorageKey,
163
+ artifactHash: params.artifactHash,
164
+ graphHash: params.graphHash,
165
+ },
166
+ codeFormat: snapshot.codeFormat ?? null,
167
+ compatibility: snapshot.compatibility ?? null,
168
+ };
169
+ }
170
+
171
+ function stripRetryChildManifestCode(
172
+ manifests: PlayRuntimeManifestMap | null | undefined,
173
+ ): PlayRuntimeManifestMap | null {
174
+ if (!manifests) return null;
175
+ const stripped: PlayRuntimeManifestMap = {};
176
+ for (const [key, manifest] of Object.entries(manifests)) {
177
+ const rest = { ...manifest };
178
+ delete rest.bundledCode;
179
+ delete rest.sourceCode;
180
+ delete (rest as Record<string, unknown>).sourceMap;
181
+ stripped[key] = rest;
182
+ }
183
+ return stripped;
184
+ }
185
+
186
+ function stripRetryChildManifestToArtifact(
187
+ manifests: PlayRuntimeManifestMap | null | undefined,
188
+ ): PlayRuntimeManifestMap | null {
189
+ if (!manifests) return null;
190
+ const stripped: PlayRuntimeManifestMap = {};
191
+ for (const [key, manifest] of Object.entries(manifests)) {
192
+ stripped[key] = {
193
+ playName: manifest.playName,
194
+ graphHash: manifest.graphHash,
195
+ artifactStorageKey: manifest.artifactStorageKey,
196
+ artifactHash: manifest.artifactHash,
197
+ staticPipelineHash: manifest.staticPipelineHash,
198
+ staticPipeline: manifest.staticPipeline,
199
+ compiledAt: manifest.compiledAt,
200
+ compilerVersion: manifest.compilerVersion,
201
+ maxCreditsPerRun: manifest.maxCreditsPerRun,
202
+ };
203
+ }
204
+ return stripped;
205
+ }
206
+
207
+ function isRecord(value: unknown): value is Record<string, unknown> {
208
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
209
+ }
@@ -642,6 +642,7 @@ export class DeeplineClient {
642
642
  categories?: string;
643
643
  grep?: string;
644
644
  grepMode?: 'all' | 'any' | 'phrase';
645
+ compact?: boolean;
645
646
  }): Promise<ToolDefinition[]> {
646
647
  const params = new URLSearchParams();
647
648
  if (options?.categories?.trim()) {
@@ -651,6 +652,7 @@ export class DeeplineClient {
651
652
  params.set('grep', options.grep.trim());
652
653
  params.set('grep_mode', options.grepMode ?? 'all');
653
654
  }
655
+ params.set('compact', options?.compact === true ? 'true' : 'false');
654
656
  const suffix = params.toString() ? `?${params.toString()}` : '';
655
657
  const res = await this.http.get<{ tools: ToolDefinition[] }>(
656
658
  `/api/v2/tools${suffix}`,
@@ -1433,6 +1435,7 @@ export class DeeplineClient {
1433
1435
  if (status) {
1434
1436
  params.set('status', status);
1435
1437
  }
1438
+ params.set('compact', 'true');
1436
1439
  const response = await this.http.get<{ runs: PlayRunListItem[] }>(
1437
1440
  `/api/v2/runs?${params.toString()}`,
1438
1441
  );
@@ -1636,10 +1639,14 @@ export class DeeplineClient {
1636
1639
  * @param name - Play name
1637
1640
  * @returns Version list (newest first)
1638
1641
  */
1639
- async listPlayVersions(name: string): Promise<PlayRevisionSummary[]> {
1642
+ async listPlayVersions(
1643
+ name: string,
1644
+ options?: { full?: boolean },
1645
+ ): Promise<PlayRevisionSummary[]> {
1640
1646
  const encodedName = encodeURIComponent(name);
1647
+ const suffix = options?.full ? '?full=true' : '';
1641
1648
  const response = await this.http.get<{ versions: PlayRevisionSummary[] }>(
1642
- `/api/v2/plays/${encodedName}/versions`,
1649
+ `/api/v2/plays/${encodedName}/versions${suffix}`,
1643
1650
  );
1644
1651
  return response.versions ?? [];
1645
1652
  }
@@ -50,10 +50,10 @@ export type SdkRelease = {
50
50
  };
51
51
 
52
52
  export const SDK_RELEASE = {
53
- version: '0.1.78',
53
+ version: '0.1.80',
54
54
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
55
55
  supportPolicy: {
56
- latest: '0.1.78',
56
+ latest: '0.1.80',
57
57
  minimumSupported: '0.1.53',
58
58
  deprecatedBelow: '0.1.53',
59
59
  },
@@ -131,6 +131,10 @@ export interface ToolDefinition {
131
131
  operationId?: string;
132
132
  /** Alternative names that resolve to this tool. */
133
133
  operationAliases?: string[];
134
+ /** Whether detailed input schema is available from `tools describe`. */
135
+ hasInputSchema?: boolean;
136
+ /** Whether detailed output schema is available from `tools describe`. */
137
+ hasOutputSchema?: boolean;
134
138
  /** JSON Schema describing the tool's input parameters. */
135
139
  inputSchema?: Record<string, unknown>;
136
140
  /** JSON Schema describing the tool's output shape. */
@@ -661,6 +665,7 @@ export interface PlayListItem {
661
665
  currentPublishedVersion?: number | null;
662
666
  tableNamespace?: string | null;
663
667
  isDraftDirty?: boolean;
668
+ hasInputSchema?: boolean;
664
669
  inputSchema?: Record<string, unknown> | null;
665
670
  outputSchema?: Record<string, unknown> | null;
666
671
  staticPipeline?: unknown;
@@ -0,0 +1,231 @@
1
+ import {
2
+ noopPacingPermit,
3
+ type PacingPermit,
4
+ type PacingRule,
5
+ type RateStateBackend,
6
+ } from './rate-state-backend';
7
+
8
+ /**
9
+ * Distributed Rate State Backend for the `esm_workers` substrate.
10
+ *
11
+ * On Cloudflare a single play run fans child plays and map rows across many
12
+ * V8 isolates, so the per-`(org, provider)` request window cannot be
13
+ * process-local — each isolate would otherwise pace against its own private
14
+ * counter and the org could blow past a provider's real limit by the number of
15
+ * isolates. This backend makes the window GLOBAL by RPCing the coordinator
16
+ * Durable Object addressed per bucket (`idFromName('rate:<orgId>:<provider>')`).
17
+ * The DO is single-threaded, so it runs the same sliding-window algorithm as
18
+ * `InMemoryRateStateBackend` correctly for all isolates at once.
19
+ *
20
+ * Latency: a full DO round-trip on every outbound tool call would tax the
21
+ * hello-world latency baseline. Instead the backend LEASES SMALL PERMIT BLOCKS:
22
+ * one `/rate-acquire` round-trip debits up to {@link LEASE_BLOCK_SIZE} permits
23
+ * from the global window, and subsequent acquires draw from the local block
24
+ * until it is exhausted or its short TTL expires. This bounds round-trips to
25
+ * roughly `calls / LEASE_BLOCK_SIZE` while keeping over-issuance bounded by one
26
+ * block per isolate per window.
27
+ *
28
+ * Fail-open: if the coordinator is unreachable the backend logs once and
29
+ * PROCEEDS (grants the permit) rather than stalling the run, matching the
30
+ * semantics of `src/lib/redis/customer-rate-limiter.ts` — a degraded limiter
31
+ * must never become an availability outage. The Governor's global
32
+ * tool-concurrency semaphore remains the unconditional backstop.
33
+ */
34
+
35
+ /** Permits leased per round-trip. Tuned to amortize the DO hop, not to batch. */
36
+ const LEASE_BLOCK_SIZE = 16;
37
+ /**
38
+ * Max age of a leased block. A leased permit debited the global window already,
39
+ * but if it is held past roughly one window it could let an isolate run ahead
40
+ * of a rolled-over window. Discarding stale blocks bounds that to sub-window.
41
+ */
42
+ const LEASE_BLOCK_TTL_MS = 250;
43
+ /** Cap on how long the backend will park waiting on a saturated window. */
44
+ const MAX_ACQUIRE_WAIT_MS = 5_000;
45
+
46
+ export interface CoordinatorRatePort {
47
+ /**
48
+ * Lease up to `requested` request-window permits for `bucketId` under all
49
+ * `rules` from the coordinator DO. Returns how many were `granted` (0 when the
50
+ * window is saturated) and a `waitMs` hint before retrying.
51
+ */
52
+ rateAcquire(input: {
53
+ bucketId: string;
54
+ rules: PacingRule[];
55
+ requested: number;
56
+ }): Promise<{ granted: number; waitMs: number }>;
57
+ /** Feed a Retry-After cooldown back into the global bucket. */
58
+ ratePenalize(input: { bucketId: string; cooldownMs: number }): Promise<void>;
59
+ }
60
+
61
+ interface LeasedBlock {
62
+ remaining: number;
63
+ expiresAt: number;
64
+ /** Stable signature of the rules this block was leased under. */
65
+ rulesKey: string;
66
+ }
67
+
68
+ interface Options {
69
+ now?: () => number;
70
+ sleep?: (ms: number) => Promise<void>;
71
+ onDegraded?: (info: { bucketId: string; error: string }) => void;
72
+ }
73
+
74
+ function rulesSignature(rules: readonly PacingRule[]): string {
75
+ return [...rules]
76
+ .map(
77
+ (rule) =>
78
+ `${rule.ruleId}:${rule.requestsPerWindow}:${rule.windowMs}:${rule.maxConcurrency ?? ''}`,
79
+ )
80
+ .sort()
81
+ .join('|');
82
+ }
83
+
84
+ export class CoordinatorRateStateBackend implements RateStateBackend {
85
+ private readonly port: CoordinatorRatePort;
86
+ private readonly now: () => number;
87
+ private readonly sleep: (ms: number) => Promise<void>;
88
+ private readonly onDegraded: (info: {
89
+ bucketId: string;
90
+ error: string;
91
+ }) => void;
92
+ private readonly blocks = new Map<string, LeasedBlock>();
93
+ private degradedLogged = false;
94
+
95
+ constructor(port: CoordinatorRatePort, options: Options = {}) {
96
+ this.port = port;
97
+ this.now = options.now ?? (() => Date.now());
98
+ this.sleep =
99
+ options.sleep ??
100
+ ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
101
+ this.onDegraded =
102
+ options.onDegraded ??
103
+ ((info) => {
104
+ if (this.degradedLogged) return;
105
+ this.degradedLogged = true;
106
+ console.warn('[coordinator-rate-state] acquire failed open', info);
107
+ });
108
+ }
109
+
110
+ async acquire(input: {
111
+ bucketId: string;
112
+ rules: readonly PacingRule[];
113
+ signal?: AbortSignal;
114
+ }): Promise<PacingPermit> {
115
+ const { bucketId, rules, signal } = input;
116
+ if (rules.length === 0) {
117
+ return noopPacingPermit();
118
+ }
119
+ const rulesKey = rulesSignature(rules);
120
+
121
+ // Draw from a still-valid local block first — no round-trip.
122
+ if (this.drawFromBlock(bucketId, rulesKey)) {
123
+ return noopPacingPermit();
124
+ }
125
+
126
+ const waitStartedAt = this.now();
127
+ while (true) {
128
+ if (signal?.aborted) {
129
+ throw signal.reason instanceof Error
130
+ ? signal.reason
131
+ : new Error('Rate-state acquire aborted.');
132
+ }
133
+ let response: { granted: number; waitMs: number };
134
+ try {
135
+ response = await this.port.rateAcquire({
136
+ bucketId,
137
+ rules: [...rules],
138
+ requested: LEASE_BLOCK_SIZE,
139
+ });
140
+ } catch (error) {
141
+ // Fail open: a degraded coordinator must not stall the run.
142
+ this.onDegraded({
143
+ bucketId,
144
+ error: error instanceof Error ? error.message : String(error),
145
+ });
146
+ return noopPacingPermit();
147
+ }
148
+ if (response.granted > 0) {
149
+ // Consume one for this call; cache the rest as a short-lived block.
150
+ const remaining = response.granted - 1;
151
+ if (remaining > 0) {
152
+ this.mergeBlock(bucketId, remaining, rulesKey);
153
+ }
154
+ return noopPacingPermit();
155
+ }
156
+ // Window saturated. Park for the hint, then re-acquire. Cap total wait so
157
+ // a stuck bucket surfaces through the Governor's wall-clock guard instead
158
+ // of hanging forever.
159
+ if (this.now() - waitStartedAt >= MAX_ACQUIRE_WAIT_MS) {
160
+ return noopPacingPermit();
161
+ }
162
+ const waitMs = Math.max(1, Math.min(response.waitMs, MAX_ACQUIRE_WAIT_MS));
163
+ await this.sleep(waitMs);
164
+ }
165
+ }
166
+
167
+ penalize(input: { bucketId: string; cooldownMs: number }): void {
168
+ if (input.cooldownMs <= 0) return;
169
+ // Drop any cached block for this bucket so the cooldown takes effect on the
170
+ // very next acquire instead of being masked by already-leased permits.
171
+ this.blocks.delete(input.bucketId);
172
+ void this.port
173
+ .ratePenalize({
174
+ bucketId: input.bucketId,
175
+ cooldownMs: input.cooldownMs,
176
+ })
177
+ .catch((error) => {
178
+ this.onDegraded({
179
+ bucketId: input.bucketId,
180
+ error: error instanceof Error ? error.message : String(error),
181
+ });
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Add freshly-leased permits to the bucket's block instead of overwriting it.
187
+ * Two concurrent acquires can both miss the local block and both round-trip;
188
+ * each debited the global window, so the DO already issued both blocks'
189
+ * permits. Overwriting would drop one set — under-issuance that wastes window
190
+ * capacity and over-throttles. Merging preserves every debited permit:
191
+ * - same rulesKey + still valid → sum remaining, keep the earlier expiry so
192
+ * the merged block never outlives the older lease's sub-window bound.
193
+ * - missing / stale / different rules → start fresh from this lease.
194
+ */
195
+ private mergeBlock(
196
+ bucketId: string,
197
+ remaining: number,
198
+ rulesKey: string,
199
+ ): void {
200
+ const freshExpiresAt = this.now() + LEASE_BLOCK_TTL_MS;
201
+ const existing = this.blocks.get(bucketId);
202
+ if (
203
+ existing &&
204
+ existing.rulesKey === rulesKey &&
205
+ existing.expiresAt > this.now()
206
+ ) {
207
+ existing.remaining += remaining;
208
+ existing.expiresAt = Math.min(existing.expiresAt, freshExpiresAt);
209
+ return;
210
+ }
211
+ this.blocks.set(bucketId, {
212
+ remaining,
213
+ expiresAt: freshExpiresAt,
214
+ rulesKey,
215
+ });
216
+ }
217
+
218
+ private drawFromBlock(bucketId: string, rulesKey: string): boolean {
219
+ const block = this.blocks.get(bucketId);
220
+ if (!block) return false;
221
+ if (block.rulesKey !== rulesKey || block.expiresAt <= this.now()) {
222
+ this.blocks.delete(bucketId);
223
+ return false;
224
+ }
225
+ block.remaining -= 1;
226
+ if (block.remaining <= 0) {
227
+ this.blocks.delete(bucketId);
228
+ }
229
+ return true;
230
+ }
231
+ }