deepline 0.1.0 → 0.1.2
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 +212 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +198 -40
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
- package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
- package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
- package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
- package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
- package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
- package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
- package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
- package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
- package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
- package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
- package/dist/repo/sdk/src/cli/index.ts +138 -0
- package/dist/repo/sdk/src/cli/progress.ts +135 -0
- package/dist/repo/sdk/src/cli/trace.ts +61 -0
- package/dist/repo/sdk/src/cli/utils.ts +145 -0
- package/dist/repo/sdk/src/client.ts +1188 -0
- package/dist/repo/sdk/src/compat.ts +77 -0
- package/dist/repo/sdk/src/config.ts +285 -0
- package/dist/repo/sdk/src/errors.ts +125 -0
- package/dist/repo/sdk/src/http.ts +391 -0
- package/dist/repo/sdk/src/index.ts +139 -0
- package/dist/repo/sdk/src/play.ts +1330 -0
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
- package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
- package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
- package/dist/repo/sdk/src/tool-output.ts +489 -0
- package/dist/repo/sdk/src/types.ts +669 -0
- package/dist/repo/sdk/src/version.ts +2 -0
- package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
- package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
- package/dist/repo/shared_libs/observability/tracing.ts +98 -0
- package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
- package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
- package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
- package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
- package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
- package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
- package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
- package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
- package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
- package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
- package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
- package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
- package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
- package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
- package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
- package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
- package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
- package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
- package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
- package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
- package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
- package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
- package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
- package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
- package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
- package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
- package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
- package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
- package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
- package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
- package/dist/repo/shared_libs/plays/contracts.ts +51 -0
- package/dist/repo/shared_libs/plays/dataset.ts +308 -0
- package/dist/repo/shared_libs/plays/definition.ts +264 -0
- package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
- package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
- package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
- package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
- package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
- package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
- package/dist/repo/shared_libs/temporal/constants.ts +39 -0
- package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
- package/package.json +4 -4
|
@@ -0,0 +1,3999 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlayContextImpl — the cloud execution engine.
|
|
3
|
+
*
|
|
4
|
+
* Batching model:
|
|
5
|
+
* 1. ctx.map("table_key", rows, { key: "row_id" }).step("field", resolver) starts all row field resolvers concurrently
|
|
6
|
+
* 2. ctx.waterfall() calls inside field resolvers QUEUE requests (don't execute)
|
|
7
|
+
* 3. ctx.tools.execute({ id, tool, input }) calls inside field resolvers also QUEUE
|
|
8
|
+
* 4. After all rows have queued, executeBatchedWaterfalls() runs provider-by-provider
|
|
9
|
+
* 5. Each provider batch = real HTTP call to /api/v2/integrations/{toolId}/execute
|
|
10
|
+
* 6. Results resolve suspended row promises, rows complete
|
|
11
|
+
*
|
|
12
|
+
* Temporal integration:
|
|
13
|
+
* - checkpoint: recovered from heartbeatDetails on retry (skip completed batches)
|
|
14
|
+
* - onBatchComplete: called after each provider batch (heartbeat checkpoint to Temporal)
|
|
15
|
+
*/
|
|
16
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
17
|
+
import {
|
|
18
|
+
createDeferredPlayDataset,
|
|
19
|
+
materializePlayDatasetInput,
|
|
20
|
+
} from '@shared_libs/plays/dataset';
|
|
21
|
+
import type { PlayDataset, PlayDatasetInput } from '@shared_libs/plays/dataset';
|
|
22
|
+
import {
|
|
23
|
+
compileRequestsWithStrategy,
|
|
24
|
+
executeChunkedRequests,
|
|
25
|
+
executeWaterfallProviders,
|
|
26
|
+
resolveWaterfallToolId,
|
|
27
|
+
} from './batch-runtime';
|
|
28
|
+
import { PlayRateLimitScheduler } from '@shared_libs/plays/rate-limit-scheduler';
|
|
29
|
+
import { normalizePlayToolResult } from './result-normalization';
|
|
30
|
+
import {
|
|
31
|
+
cloneToolExecuteResultWithExecution,
|
|
32
|
+
createToolExecuteResult,
|
|
33
|
+
isToolExecuteResult,
|
|
34
|
+
type ToolExecuteResult,
|
|
35
|
+
type ToolResultMetadataInput,
|
|
36
|
+
} from './tool-result';
|
|
37
|
+
import { sqlSafePlayColumnName } from '@shared_libs/plays/static-pipeline';
|
|
38
|
+
import { createRuntimeDatasetId } from './dataset-id';
|
|
39
|
+
import {
|
|
40
|
+
derivePlayRowIdentity,
|
|
41
|
+
MAP_KEY_NAMESPACE_MAX_LENGTH,
|
|
42
|
+
normalizeTableNamespace,
|
|
43
|
+
resolveStaleMapTableNamespace,
|
|
44
|
+
} from '@shared_libs/plays/row-identity';
|
|
45
|
+
import {
|
|
46
|
+
assertUniqueExplicitMapKeys,
|
|
47
|
+
createExplicitMapKeyResolver,
|
|
48
|
+
deriveMapRowIdentity,
|
|
49
|
+
MapRowIdentity,
|
|
50
|
+
} from './map-row-identity';
|
|
51
|
+
import { MapExecutionFrameStore } from './map-execution-frame';
|
|
52
|
+
import { PlayProgressEmitter } from './progress-emitter';
|
|
53
|
+
import { WaterfallReplayStore } from './waterfall-replay';
|
|
54
|
+
import { setSpanAttributes, withActiveSpan } from './tracing';
|
|
55
|
+
import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from './runtime-constraints';
|
|
56
|
+
import {
|
|
57
|
+
PlayExecutionSuspendedError,
|
|
58
|
+
PlayRowExecutionSuspendedError,
|
|
59
|
+
isPlayRowExecutionSuspendedError,
|
|
60
|
+
} from './suspension';
|
|
61
|
+
import type {
|
|
62
|
+
CsvOptions,
|
|
63
|
+
RowState,
|
|
64
|
+
WaterfallRequest,
|
|
65
|
+
WaterfallOptions,
|
|
66
|
+
InlineWaterfallSpec,
|
|
67
|
+
MapDefinitionOptions,
|
|
68
|
+
MapOptions,
|
|
69
|
+
MapRunOptions,
|
|
70
|
+
ToolCallRequest,
|
|
71
|
+
ToolBatchResult,
|
|
72
|
+
ToolExecutionRequest,
|
|
73
|
+
ContextOptions,
|
|
74
|
+
PlayCallOptions,
|
|
75
|
+
PlayCheckpoint,
|
|
76
|
+
PlayStep,
|
|
77
|
+
PlayStepRowResult,
|
|
78
|
+
PlayRowUpdate,
|
|
79
|
+
MapFieldDefinition,
|
|
80
|
+
MapFieldResolver,
|
|
81
|
+
PlayExecutionGovernanceLimits,
|
|
82
|
+
PlayExecutionGovernanceState,
|
|
83
|
+
ResolvedPlayExecution,
|
|
84
|
+
PlayFetchResponse,
|
|
85
|
+
MapExecutionScope,
|
|
86
|
+
MapExecutionFrame,
|
|
87
|
+
PlayExecutionEvent,
|
|
88
|
+
IntegrationEventWaitHandler,
|
|
89
|
+
RuntimeStepReceipt,
|
|
90
|
+
RuntimeStepProgram,
|
|
91
|
+
RuntimeStepProgramStep,
|
|
92
|
+
RuntimeConditionalStepResolver,
|
|
93
|
+
} from './ctx-types';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* SECURITY: AsyncLocalStorage is per async execution, not a cross-run cache.
|
|
97
|
+
*
|
|
98
|
+
* Keep only row-scoped metadata here. Do not expand this store with credentials,
|
|
99
|
+
* provider responses, org-wide mutable caches, or anything that must not cross
|
|
100
|
+
* workflow boundaries if worker scheduling/interleaving changes.
|
|
101
|
+
*/
|
|
102
|
+
const rowContext = new AsyncLocalStorage<{
|
|
103
|
+
rowId: number;
|
|
104
|
+
fieldName?: string;
|
|
105
|
+
tableNamespace?: string;
|
|
106
|
+
rowKey?: string;
|
|
107
|
+
mapScope?: MapExecutionScope;
|
|
108
|
+
}>();
|
|
109
|
+
const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
|
|
110
|
+
const PURE_JS_HEARTBEAT_ROW_INTERVAL = 250;
|
|
111
|
+
const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
|
|
112
|
+
const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
113
|
+
const TOOL_TRANSIENT_HTTP_MAX_ATTEMPTS = 3;
|
|
114
|
+
const TRANSIENT_HTTP_RETRY_SAFE_TOOL_IDS = new Set([
|
|
115
|
+
'test_company_search',
|
|
116
|
+
'test_transient_500',
|
|
117
|
+
]);
|
|
118
|
+
const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
|
|
119
|
+
const IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT = 25;
|
|
120
|
+
const WATERFALL_ROW_MATCH_LOG_SAMPLE_LIMIT = 10;
|
|
121
|
+
const WATERFALL_ROW_MATCH_LOG_INTERVAL = 1_000;
|
|
122
|
+
const BATCH_SIZE_LOG_SAMPLE_LIMIT = 10;
|
|
123
|
+
const DEFAULT_GOVERNANCE_LIMITS: PlayExecutionGovernanceLimits = {
|
|
124
|
+
maxPlayCallDepth: 6,
|
|
125
|
+
maxPlayCallCount: 32,
|
|
126
|
+
maxToolCallCount: 500,
|
|
127
|
+
maxWaterfallStepExecutions: null,
|
|
128
|
+
maxRetryCount: 200,
|
|
129
|
+
maxDescendants: 64,
|
|
130
|
+
maxChildPlayCallsPerParent: 16,
|
|
131
|
+
maxConcurrentPlayCalls: 16,
|
|
132
|
+
maxConcurrentToolCalls: 32,
|
|
133
|
+
};
|
|
134
|
+
const STEP_PROGRAM_MAP_DEFINITION = Symbol('deepline.stepProgramMapDefinition');
|
|
135
|
+
|
|
136
|
+
function stableFetchHash(value: string): string {
|
|
137
|
+
let hash = 0;
|
|
138
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
139
|
+
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
|
|
140
|
+
}
|
|
141
|
+
return hash.toString(36);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function compactRowResultsPreview<T>(rows: T[]): T[] {
|
|
145
|
+
if (rows.length <= IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT) {
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
return rows.slice(0, IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isRuntimeStepProgram(value: unknown): value is RuntimeStepProgram {
|
|
152
|
+
return (
|
|
153
|
+
!!value &&
|
|
154
|
+
typeof value === 'object' &&
|
|
155
|
+
(value as { kind?: unknown }).kind === 'steps' &&
|
|
156
|
+
Array.isArray((value as { steps?: unknown }).steps)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isRuntimeConditionalStepResolver(
|
|
161
|
+
value: unknown,
|
|
162
|
+
): value is RuntimeConditionalStepResolver {
|
|
163
|
+
return (
|
|
164
|
+
!!value &&
|
|
165
|
+
typeof value === 'object' &&
|
|
166
|
+
(value as { kind?: unknown }).kind === 'conditional' &&
|
|
167
|
+
typeof (value as { when?: unknown }).when === 'function' &&
|
|
168
|
+
typeof (value as { run?: unknown }).run === 'function'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isMapDefinitionOptions(
|
|
173
|
+
value: unknown,
|
|
174
|
+
): value is Omit<MapOptions, 'description'> {
|
|
175
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const keys = Object.keys(value);
|
|
179
|
+
return keys.every((key) => key === 'key' || key === 'staleAfterSeconds');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class RuntimeMapBuilder<T extends Record<string, unknown>> {
|
|
183
|
+
private readonly program: RuntimeStepProgram = {
|
|
184
|
+
kind: 'steps',
|
|
185
|
+
steps: [],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
constructor(
|
|
189
|
+
private readonly ctx: PlayContextImpl,
|
|
190
|
+
private readonly key: string,
|
|
191
|
+
private readonly items: PlayDatasetInput<T>,
|
|
192
|
+
private readonly mapOptions?: MapDefinitionOptions<T>,
|
|
193
|
+
) {}
|
|
194
|
+
|
|
195
|
+
step(name: string, resolver: RuntimeStepProgramStep['resolver']): this {
|
|
196
|
+
if (!name.trim()) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'ctx.map(...).step(name, ...) requires a non-empty step name.',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
this.program.steps = [...this.program.steps, { name, resolver }];
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
run(options?: MapRunOptions): Promise<PlayDataset<Record<string, unknown>>> {
|
|
206
|
+
if (
|
|
207
|
+
options &&
|
|
208
|
+
Object.keys(options).some((optionKey) => optionKey !== 'description')
|
|
209
|
+
) {
|
|
210
|
+
throw new Error('ctx.map(...).run() only accepts description.');
|
|
211
|
+
}
|
|
212
|
+
return this.ctx.runStepProgramMap(this.key, this.items, this.program, {
|
|
213
|
+
...this.mapOptions,
|
|
214
|
+
...options,
|
|
215
|
+
} as MapOptions<T>);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const WAITING_ROW = Symbol('deepline.waiting_row');
|
|
220
|
+
|
|
221
|
+
function normalizeFetchHeaders(
|
|
222
|
+
headers: RequestInit['headers'],
|
|
223
|
+
): Record<string, string> {
|
|
224
|
+
if (!headers) return {};
|
|
225
|
+
if (headers instanceof Headers) {
|
|
226
|
+
return Object.fromEntries(
|
|
227
|
+
[...headers.entries()].map(([key, value]) => [key.toLowerCase(), value]),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(headers)) {
|
|
231
|
+
return Object.fromEntries(
|
|
232
|
+
headers.map(([key, value]) => [key.toLowerCase(), value]),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return Object.fromEntries(
|
|
236
|
+
Object.entries(headers).map(([key, value]) => [
|
|
237
|
+
key.toLowerCase(),
|
|
238
|
+
String(value),
|
|
239
|
+
]),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseRetryAfterMs(header: string | null): number {
|
|
244
|
+
if (!header) {
|
|
245
|
+
return TOOL_RETRY_AFTER_FALLBACK_MS;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const seconds = Number(header);
|
|
249
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
250
|
+
return Math.ceil(seconds * 1000);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const retryAt = Date.parse(header);
|
|
254
|
+
if (Number.isFinite(retryAt)) {
|
|
255
|
+
return Math.max(1, retryAt - Date.now());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return TOOL_RETRY_AFTER_FALLBACK_MS;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isRetryableTransientToolHttpStatus(
|
|
262
|
+
toolId: string,
|
|
263
|
+
status: number,
|
|
264
|
+
): boolean {
|
|
265
|
+
return (
|
|
266
|
+
status >= 500 &&
|
|
267
|
+
status < 600 &&
|
|
268
|
+
TRANSIENT_HTTP_RETRY_SAFE_TOOL_IDS.has(toolId)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseJsonOrNull(bodyText: string): unknown | null {
|
|
273
|
+
if (!bodyText.trim()) return null;
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(bodyText) as unknown;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
type ToolExecutionResponse = {
|
|
282
|
+
status: string;
|
|
283
|
+
result: unknown;
|
|
284
|
+
metadata: ToolResultMetadataInput | null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
function parseExecuteToolMetadata(
|
|
288
|
+
toolId: string,
|
|
289
|
+
data: Record<string, unknown>,
|
|
290
|
+
): ToolResultMetadataInput | null {
|
|
291
|
+
const metadata = data._metadata;
|
|
292
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const tool = (metadata as Record<string, unknown>).tool;
|
|
296
|
+
if (!tool || typeof tool !== 'object' || Array.isArray(tool)) return null;
|
|
297
|
+
const record = tool as Record<string, unknown>;
|
|
298
|
+
const metadataToolId =
|
|
299
|
+
typeof record.toolId === 'string' && record.toolId.trim()
|
|
300
|
+
? record.toolId
|
|
301
|
+
: toolId;
|
|
302
|
+
const readGetters = (value: unknown): Record<string, readonly string[]> => {
|
|
303
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
304
|
+
return Object.fromEntries(
|
|
305
|
+
Object.entries(value as Record<string, unknown>).flatMap(
|
|
306
|
+
([key, paths]) => {
|
|
307
|
+
if (!Array.isArray(paths)) return [];
|
|
308
|
+
const normalized = paths.filter(
|
|
309
|
+
(path): path is string =>
|
|
310
|
+
typeof path === 'string' && path.trim().length > 0,
|
|
311
|
+
);
|
|
312
|
+
return normalized.length > 0 ? [[key, normalized]] : [];
|
|
313
|
+
},
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
const listExtractorPaths = Array.isArray(record.listExtractorPaths)
|
|
318
|
+
? record.listExtractorPaths.filter(
|
|
319
|
+
(path): path is string =>
|
|
320
|
+
typeof path === 'string' && path.trim().length > 0,
|
|
321
|
+
)
|
|
322
|
+
: [];
|
|
323
|
+
return {
|
|
324
|
+
toolId: metadataToolId,
|
|
325
|
+
resultIdentityGetters: readGetters(record.resultIdentityGetters),
|
|
326
|
+
listExtractorPaths,
|
|
327
|
+
listIdentityGetters: readGetters(record.listIdentityGetters),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function assertJsonSerializableStepOutput(
|
|
332
|
+
stepId: string,
|
|
333
|
+
output: unknown,
|
|
334
|
+
): void {
|
|
335
|
+
try {
|
|
336
|
+
JSON.stringify(output);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`ctx.step(${stepId}) returned a value that cannot be checkpointed as JSON: ${
|
|
340
|
+
error instanceof Error ? error.message : String(error)
|
|
341
|
+
}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeStepDescription(
|
|
347
|
+
value: string | undefined,
|
|
348
|
+
): string | undefined {
|
|
349
|
+
const trimmed = value?.trim();
|
|
350
|
+
return trimmed ? trimmed : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function emptyCheckpoint(): PlayCheckpoint {
|
|
354
|
+
return {
|
|
355
|
+
completedBatches: {},
|
|
356
|
+
completedToolBatches: {},
|
|
357
|
+
resolvedWaterfalls: {},
|
|
358
|
+
mapFrames: {},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createDefaultGovernanceState(
|
|
363
|
+
options: ContextOptions,
|
|
364
|
+
): PlayExecutionGovernanceState {
|
|
365
|
+
const playId = options.playId ?? options.workflowId ?? 'anonymous-play';
|
|
366
|
+
const runId = options.runId ?? options.workflowId ?? 'anonymous-run';
|
|
367
|
+
return {
|
|
368
|
+
rootPlayId: playId,
|
|
369
|
+
rootRunId: runId,
|
|
370
|
+
currentPlayId: playId,
|
|
371
|
+
currentRunId: runId,
|
|
372
|
+
ancestryPlayIds: [playId],
|
|
373
|
+
ancestryRunIds: [runId],
|
|
374
|
+
callDepth: 0,
|
|
375
|
+
playCallCount: 0,
|
|
376
|
+
toolCallCount: 0,
|
|
377
|
+
waterfallStepExecutions: 0,
|
|
378
|
+
retryCount: 0,
|
|
379
|
+
descendantCount: 0,
|
|
380
|
+
parentChildCalls: {},
|
|
381
|
+
inFlightPlayCalls: 0,
|
|
382
|
+
inFlightPlayCallsByPlayId: {},
|
|
383
|
+
inFlightToolCalls: 0,
|
|
384
|
+
limits: DEFAULT_GOVERNANCE_LIMITS,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isInlineWaterfallSpec(value: unknown): value is InlineWaterfallSpec {
|
|
389
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
const candidate = value as InlineWaterfallSpec;
|
|
393
|
+
return (
|
|
394
|
+
typeof candidate.id === 'string' &&
|
|
395
|
+
typeof candidate.output === 'string' &&
|
|
396
|
+
typeof candidate.minResults === 'number' &&
|
|
397
|
+
candidate.minResults >= 1 &&
|
|
398
|
+
Array.isArray(candidate.steps) &&
|
|
399
|
+
candidate.steps.length > 0
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isInlineWaterfallCodeStep(
|
|
404
|
+
step: InlineWaterfallSpec['steps'][number],
|
|
405
|
+
): step is Extract<InlineWaterfallSpec['steps'][number], { kind: 'code' }> {
|
|
406
|
+
return step.kind === 'code';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isInlineWaterfallToolStep(
|
|
410
|
+
step: InlineWaterfallSpec['steps'][number],
|
|
411
|
+
): step is Extract<InlineWaterfallSpec['steps'][number], { toolId: string }> {
|
|
412
|
+
return !isInlineWaterfallCodeStep(step);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractInlineWaterfallCodeStepValue(
|
|
416
|
+
output: string,
|
|
417
|
+
result: unknown,
|
|
418
|
+
): unknown {
|
|
419
|
+
if (
|
|
420
|
+
result &&
|
|
421
|
+
typeof result === 'object' &&
|
|
422
|
+
!Array.isArray(result) &&
|
|
423
|
+
output in result
|
|
424
|
+
) {
|
|
425
|
+
return (result as Record<string, unknown>)[output] ?? null;
|
|
426
|
+
}
|
|
427
|
+
return result ?? null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isMeaningfulValue(value: unknown): boolean {
|
|
431
|
+
if (value == null) return false;
|
|
432
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
433
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
434
|
+
if (typeof value === 'object') return Object.keys(value).length > 0;
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getValueAtPath(value: unknown, path: string): unknown {
|
|
439
|
+
let current = value;
|
|
440
|
+
for (const part of path.split('.').filter(Boolean)) {
|
|
441
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
442
|
+
current = (current as Record<string, unknown>)[part];
|
|
443
|
+
}
|
|
444
|
+
return current;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function extractWaterfallOutputValue(
|
|
448
|
+
toolId: string,
|
|
449
|
+
output: string,
|
|
450
|
+
result: unknown,
|
|
451
|
+
getToolResultIdentityGetters?: (
|
|
452
|
+
toolId: string,
|
|
453
|
+
output: string,
|
|
454
|
+
) => Promise<readonly string[]>,
|
|
455
|
+
): Promise<unknown> {
|
|
456
|
+
const getters = getToolResultIdentityGetters
|
|
457
|
+
? await getToolResultIdentityGetters(toolId, output)
|
|
458
|
+
: [];
|
|
459
|
+
for (const getter of getters) {
|
|
460
|
+
const value = getValueAtPath(result, getter);
|
|
461
|
+
if (isMeaningfulValue(value)) {
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const directOutputValue = getValueAtPath(result, output);
|
|
467
|
+
if (directOutputValue !== undefined) {
|
|
468
|
+
return isMeaningfulValue(directOutputValue) ? directOutputValue : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (output === 'people' || output === 'companies') {
|
|
472
|
+
if (Array.isArray(result)) return result;
|
|
473
|
+
const direct = getValueAtPath(result, output);
|
|
474
|
+
if (Array.isArray(direct)) return direct;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return isMeaningfulValue(result) ? result : null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export class PlayContextImpl {
|
|
481
|
+
private rowStates = new Map<number, RowState>();
|
|
482
|
+
private resolvers = new Map<string, (value: unknown) => void>();
|
|
483
|
+
private waterfallQueue = new Map<string, WaterfallRequest[]>();
|
|
484
|
+
private toolCallQueue: ToolCallRequest[] = [];
|
|
485
|
+
private toolCallResolvers = new Map<string, Array<(value: unknown) => void>>();
|
|
486
|
+
private options: ContextOptions;
|
|
487
|
+
private logBuffer: string[] = [];
|
|
488
|
+
private checkpoint: PlayCheckpoint;
|
|
489
|
+
private steps: PlayStep[] = [];
|
|
490
|
+
private explicitMapInvocationKeys = new Set<string>();
|
|
491
|
+
/** The map step currently being built — substeps go here instead of top-level. */
|
|
492
|
+
private activeMapStep: Extract<PlayStep, { type: 'map' }> | null = null;
|
|
493
|
+
/** Last completed map step — for post-map recordStep calls (e.g. run_javascript). */
|
|
494
|
+
private lastMapStep: Extract<PlayStep, { type: 'map' }> | null = null;
|
|
495
|
+
private pureMapExecutionActive = false;
|
|
496
|
+
private lastProgressHeartbeatAt = 0;
|
|
497
|
+
private pendingRowEventBoundaries: Array<{
|
|
498
|
+
boundaryId: string;
|
|
499
|
+
eventKey: string;
|
|
500
|
+
timeoutMs: number;
|
|
501
|
+
}> = [];
|
|
502
|
+
private processedRowCount = 0;
|
|
503
|
+
private directToolCallIndex = 0;
|
|
504
|
+
private sleepBoundaryIndex = 0;
|
|
505
|
+
private fetchCallIndex = 0;
|
|
506
|
+
private readonly stepCallIndexByKey = new Map<string, number>();
|
|
507
|
+
private readonly waterfallMatchLogCounts = new Map<string, number>();
|
|
508
|
+
private readonly rateLimitScheduler: PlayRateLimitScheduler;
|
|
509
|
+
private readonly governance: PlayExecutionGovernanceState;
|
|
510
|
+
private readonly mapRowIdentity = new MapRowIdentity();
|
|
511
|
+
private readonly mapFrames: MapExecutionFrameStore;
|
|
512
|
+
private readonly progress: PlayProgressEmitter;
|
|
513
|
+
private readonly waterfallReplay: WaterfallReplayStore;
|
|
514
|
+
readonly tools = {
|
|
515
|
+
execute: <TOutput = unknown>(
|
|
516
|
+
request: ToolExecutionRequest,
|
|
517
|
+
): Promise<TOutput> => this.executeTool(request) as Promise<TOutput>,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
tool<TOutput = unknown>(request: ToolExecutionRequest): Promise<TOutput> {
|
|
521
|
+
return this.tools.execute<TOutput>(request);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
constructor(options: ContextOptions) {
|
|
525
|
+
this.options = options;
|
|
526
|
+
this.rateLimitScheduler = new PlayRateLimitScheduler({
|
|
527
|
+
getQueueHints: options.getToolQueueHints,
|
|
528
|
+
});
|
|
529
|
+
this.checkpoint = options.checkpoint ?? emptyCheckpoint();
|
|
530
|
+
this.mapFrames = new MapExecutionFrameStore(this.checkpoint, (event) =>
|
|
531
|
+
this.emitExecutionEvent(event),
|
|
532
|
+
);
|
|
533
|
+
this.progress = new PlayProgressEmitter(
|
|
534
|
+
options.onRowUpdate,
|
|
535
|
+
options.onExecutionEvent,
|
|
536
|
+
() => rowContext.getStore() ?? null,
|
|
537
|
+
isInlineWaterfallToolStep,
|
|
538
|
+
);
|
|
539
|
+
this.waterfallReplay = new WaterfallReplayStore(this.checkpoint);
|
|
540
|
+
this.governance = {
|
|
541
|
+
...(options.governance ?? createDefaultGovernanceState(options)),
|
|
542
|
+
inFlightPlayCallsByPlayId:
|
|
543
|
+
options.governance?.inFlightPlayCallsByPlayId ?? {},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private durableBoundaryId(localId: string): string {
|
|
548
|
+
// Durable boundaries live in one checkpoint for the whole root execution.
|
|
549
|
+
// Nested plays and concurrent child calls therefore need a stable run scope
|
|
550
|
+
// in the key, otherwise two children can both produce e.g. "sleep-0-25"
|
|
551
|
+
// and replay the wrong boundary or never observe completion.
|
|
552
|
+
return `${this.governance.currentRunId}:${localId}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private emitScopedRowUpdate(
|
|
556
|
+
key: string | null,
|
|
557
|
+
tableNamespace: string | null,
|
|
558
|
+
update: Omit<PlayRowUpdate, 'key'>,
|
|
559
|
+
): void {
|
|
560
|
+
this.progress.rowUpdate(key, tableNamespace, update);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private emitExecutionEvent(event: PlayExecutionEvent): void {
|
|
564
|
+
if (!this.options.onExecutionEvent) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.progress.executionEvent(event);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private normalizeContextKey(key: string, operation: string): string {
|
|
571
|
+
const normalizedKey = key.trim();
|
|
572
|
+
if (!normalizedKey) {
|
|
573
|
+
throw new Error(`ctx.${operation} requires a non-empty key.`);
|
|
574
|
+
}
|
|
575
|
+
return normalizedKey;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private async getRuntimeStepReceipt(
|
|
579
|
+
key: string,
|
|
580
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
581
|
+
if (!this.options.getRuntimeStepReceipt) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const receipt = await this.options.getRuntimeStepReceipt({ key });
|
|
585
|
+
if (receipt && typeof receipt.key === 'string' && receipt.key.trim()) {
|
|
586
|
+
return {
|
|
587
|
+
...receipt,
|
|
588
|
+
key: receipt.key.trim(),
|
|
589
|
+
runId: receipt.runId ?? null,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return receipt
|
|
593
|
+
? {
|
|
594
|
+
key: key.trim(),
|
|
595
|
+
status: 'pending',
|
|
596
|
+
}
|
|
597
|
+
: null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private async claimRuntimeStepReceipt(
|
|
601
|
+
key: string,
|
|
602
|
+
runId: string,
|
|
603
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
604
|
+
if (!this.options.claimRuntimeStepReceipt) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const claimed = await this.options.claimRuntimeStepReceipt({
|
|
608
|
+
key,
|
|
609
|
+
runId,
|
|
610
|
+
});
|
|
611
|
+
if (!claimed || typeof claimed.key !== 'string' || !claimed.key.trim()) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
...claimed,
|
|
616
|
+
key: claimed.key.trim(),
|
|
617
|
+
runId: claimed.runId ?? null,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private async completeRuntimeStepReceipt(
|
|
622
|
+
key: string,
|
|
623
|
+
runId: string,
|
|
624
|
+
output: unknown | null,
|
|
625
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
626
|
+
if (!this.options.completeRuntimeStepReceipt) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
const completed = await this.options.completeRuntimeStepReceipt({
|
|
630
|
+
key,
|
|
631
|
+
runId,
|
|
632
|
+
output,
|
|
633
|
+
});
|
|
634
|
+
if (
|
|
635
|
+
!completed ||
|
|
636
|
+
typeof completed.key !== 'string' ||
|
|
637
|
+
!completed.key.trim()
|
|
638
|
+
) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
...completed,
|
|
643
|
+
key: completed.key.trim(),
|
|
644
|
+
runId: completed.runId ?? null,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async failRuntimeStepReceipt(
|
|
649
|
+
key: string,
|
|
650
|
+
runId: string,
|
|
651
|
+
error: string,
|
|
652
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
653
|
+
if (!this.options.failRuntimeStepReceipt) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
const failed = await this.options.failRuntimeStepReceipt({
|
|
657
|
+
key,
|
|
658
|
+
runId,
|
|
659
|
+
error,
|
|
660
|
+
});
|
|
661
|
+
if (!failed || typeof failed.key !== 'string' || !failed.key.trim()) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
...failed,
|
|
666
|
+
key: failed.key.trim(),
|
|
667
|
+
runId: failed.runId ?? null,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private async executeWithRuntimeReceipt<T>(
|
|
672
|
+
operation: string,
|
|
673
|
+
key: string,
|
|
674
|
+
runId: string,
|
|
675
|
+
opts: {
|
|
676
|
+
markSkipped?: (output: T) => Promise<void> | void;
|
|
677
|
+
execute: () => Promise<T>;
|
|
678
|
+
},
|
|
679
|
+
): Promise<T> {
|
|
680
|
+
const existing = await this.getRuntimeStepReceipt(key);
|
|
681
|
+
if (existing?.status === 'completed' || existing?.status === 'skipped') {
|
|
682
|
+
this.log(`ctx.${operation}(${key}): recovered result from receipt`);
|
|
683
|
+
if (existing.output === undefined) {
|
|
684
|
+
return existing.output as T;
|
|
685
|
+
}
|
|
686
|
+
if (opts.markSkipped) {
|
|
687
|
+
await opts.markSkipped(existing.output as T);
|
|
688
|
+
}
|
|
689
|
+
return existing.output as T;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const claimed = await this.claimRuntimeStepReceipt(key, runId);
|
|
693
|
+
if (!claimed) {
|
|
694
|
+
if (existing?.status === 'running' || existing?.status === 'failed') {
|
|
695
|
+
const runningRun = existing.runId?.trim() ?? 'unknown';
|
|
696
|
+
if (existing.status === 'running' && existing.runId === runId) {
|
|
697
|
+
// Fall through and attempt execution; likely a replay of the same run
|
|
698
|
+
// where row-level checkpoints already recorded but external receipt
|
|
699
|
+
// state was not visible.
|
|
700
|
+
} else {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`ctx.${operation}(${key}): another run is already executing this key (${runningRun}).`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
const result = await opts.execute();
|
|
710
|
+
await this.completeRuntimeStepReceipt(key, runId, result);
|
|
711
|
+
return result;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
await this.failRuntimeStepReceipt(
|
|
714
|
+
key,
|
|
715
|
+
runId,
|
|
716
|
+
this.formatRuntimeError(error),
|
|
717
|
+
);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private get currentRunId(): string {
|
|
723
|
+
return this.options.runId ?? this.governance.currentRunId ?? 'unknown';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private emitScopedFieldMetaUpdate(input: {
|
|
727
|
+
rowId: number;
|
|
728
|
+
key: string | null;
|
|
729
|
+
tableNamespace: string | null;
|
|
730
|
+
fieldName?: string | null;
|
|
731
|
+
status:
|
|
732
|
+
| 'queued'
|
|
733
|
+
| 'running'
|
|
734
|
+
| 'completed'
|
|
735
|
+
| 'failed'
|
|
736
|
+
| 'cached'
|
|
737
|
+
| 'missed'
|
|
738
|
+
| 'skipped';
|
|
739
|
+
rowStatus?: PlayRowUpdate['status'];
|
|
740
|
+
stage?: string | null;
|
|
741
|
+
provider?: string | null;
|
|
742
|
+
error?: string | null;
|
|
743
|
+
reused?: boolean;
|
|
744
|
+
dataPatch?: Record<string, unknown>;
|
|
745
|
+
}): void {
|
|
746
|
+
this.progress.fieldMetaUpdate(input);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private emitCellUpdate(input: {
|
|
750
|
+
rowId: number;
|
|
751
|
+
key: string | null;
|
|
752
|
+
tableNamespace: string | null;
|
|
753
|
+
columnName: string;
|
|
754
|
+
status:
|
|
755
|
+
| 'queued'
|
|
756
|
+
| 'running'
|
|
757
|
+
| 'completed'
|
|
758
|
+
| 'failed'
|
|
759
|
+
| 'cached'
|
|
760
|
+
| 'missed'
|
|
761
|
+
| 'skipped';
|
|
762
|
+
rowStatus?: PlayRowUpdate['status'];
|
|
763
|
+
stage?: string | null;
|
|
764
|
+
provider?: string | null;
|
|
765
|
+
error?: string | null;
|
|
766
|
+
value?: unknown;
|
|
767
|
+
}): void {
|
|
768
|
+
this.progress.cellUpdate(input);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private emitQueuedInlineWaterfallSteps(
|
|
772
|
+
rowId: number,
|
|
773
|
+
key: string | null,
|
|
774
|
+
tableNamespace: string | null,
|
|
775
|
+
spec: InlineWaterfallSpec,
|
|
776
|
+
): void {
|
|
777
|
+
this.progress.queuedInlineWaterfallSteps({
|
|
778
|
+
rowId,
|
|
779
|
+
key,
|
|
780
|
+
tableNamespace,
|
|
781
|
+
spec,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private isCompletedFieldValue(value: unknown): boolean {
|
|
786
|
+
return (
|
|
787
|
+
value !== null &&
|
|
788
|
+
value !== undefined &&
|
|
789
|
+
!(typeof value === 'string' && value.length === 0)
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private shouldReuseExistingFieldValue(
|
|
794
|
+
row: Record<string, unknown>,
|
|
795
|
+
fieldName: string,
|
|
796
|
+
): boolean {
|
|
797
|
+
return (
|
|
798
|
+
Object.prototype.hasOwnProperty.call(row, fieldName) &&
|
|
799
|
+
this.isCompletedFieldValue(row[fieldName])
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private toVisibleDataPatch(
|
|
804
|
+
fields: Record<string, unknown>,
|
|
805
|
+
): Record<string, unknown> {
|
|
806
|
+
return Object.fromEntries(
|
|
807
|
+
Object.entries(fields).filter(
|
|
808
|
+
([fieldName]) => !fieldName.startsWith('_'),
|
|
809
|
+
),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private formatRuntimeError(error: unknown): string {
|
|
814
|
+
if (error instanceof Error) {
|
|
815
|
+
return error.message;
|
|
816
|
+
}
|
|
817
|
+
return String(error);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private logWaterfallMatch(input: {
|
|
821
|
+
queueKey: string;
|
|
822
|
+
rowId: number;
|
|
823
|
+
provider: string;
|
|
824
|
+
}): void {
|
|
825
|
+
const count = (this.waterfallMatchLogCounts.get(input.queueKey) ?? 0) + 1;
|
|
826
|
+
this.waterfallMatchLogCounts.set(input.queueKey, count);
|
|
827
|
+
|
|
828
|
+
if (count <= WATERFALL_ROW_MATCH_LOG_SAMPLE_LIMIT) {
|
|
829
|
+
this.log(
|
|
830
|
+
` Row ${input.rowId}: found with ${input.provider} (${count}/${WATERFALL_ROW_MATCH_LOG_SAMPLE_LIMIT} sample)`,
|
|
831
|
+
);
|
|
832
|
+
if (count === WATERFALL_ROW_MATCH_LOG_SAMPLE_LIMIT) {
|
|
833
|
+
this.log(
|
|
834
|
+
` Further per-row matches for ${input.queueKey} will be summarized every ${WATERFALL_ROW_MATCH_LOG_INTERVAL.toLocaleString()} hits.`,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (count % WATERFALL_ROW_MATCH_LOG_INTERVAL === 0) {
|
|
841
|
+
this.log(
|
|
842
|
+
` ${count.toLocaleString()} rows matched for ${input.queueKey}; latest row ${input.rowId} with ${input.provider}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private summarizeBatchSizes(sizes: readonly number[]): string {
|
|
848
|
+
if (sizes.length <= BATCH_SIZE_LOG_SAMPLE_LIMIT) {
|
|
849
|
+
return sizes.join(', ');
|
|
850
|
+
}
|
|
851
|
+
const sample = sizes.slice(0, BATCH_SIZE_LOG_SAMPLE_LIMIT).join(', ');
|
|
852
|
+
const remaining = sizes.length - BATCH_SIZE_LOG_SAMPLE_LIMIT;
|
|
853
|
+
return `${sample}, ... +${remaining} more`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private bumpCounter(
|
|
857
|
+
field:
|
|
858
|
+
| 'playCallCount'
|
|
859
|
+
| 'toolCallCount'
|
|
860
|
+
| 'waterfallStepExecutions'
|
|
861
|
+
| 'retryCount'
|
|
862
|
+
| 'descendantCount',
|
|
863
|
+
amount = 1,
|
|
864
|
+
): void {
|
|
865
|
+
this.governance[field] += amount;
|
|
866
|
+
const limits = this.governance.limits;
|
|
867
|
+
if (
|
|
868
|
+
field === 'playCallCount' &&
|
|
869
|
+
this.governance.playCallCount > limits.maxPlayCallCount
|
|
870
|
+
) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`Root play-call budget exceeded (${this.governance.playCallCount}/${limits.maxPlayCallCount}).`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (
|
|
876
|
+
field === 'toolCallCount' &&
|
|
877
|
+
this.governance.toolCallCount > limits.maxToolCallCount
|
|
878
|
+
) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`Root tool-call budget exceeded (${this.governance.toolCallCount}/${limits.maxToolCallCount}).`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (
|
|
884
|
+
field === 'waterfallStepExecutions' &&
|
|
885
|
+
typeof limits.maxWaterfallStepExecutions === 'number' &&
|
|
886
|
+
this.governance.waterfallStepExecutions >
|
|
887
|
+
limits.maxWaterfallStepExecutions
|
|
888
|
+
) {
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Root waterfall-step budget exceeded (${this.governance.waterfallStepExecutions}/${limits.maxWaterfallStepExecutions}).`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (
|
|
894
|
+
field === 'retryCount' &&
|
|
895
|
+
this.governance.retryCount > limits.maxRetryCount
|
|
896
|
+
) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Root retry budget exceeded (${this.governance.retryCount}/${limits.maxRetryCount}).`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (
|
|
902
|
+
field === 'descendantCount' &&
|
|
903
|
+
this.governance.descendantCount > limits.maxDescendants
|
|
904
|
+
) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`Root descendant budget exceeded (${this.governance.descendantCount}/${limits.maxDescendants}).`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private acquirePlayConcurrency(playId: string): void {
|
|
912
|
+
this.governance.inFlightPlayCalls += 1;
|
|
913
|
+
const inFlightByPlayId = (this.governance.inFlightPlayCallsByPlayId ??= {});
|
|
914
|
+
inFlightByPlayId[playId] = (inFlightByPlayId[playId] ?? 0) + 1;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private releasePlayConcurrency(playId: string): void {
|
|
918
|
+
this.governance.inFlightPlayCalls = Math.max(
|
|
919
|
+
0,
|
|
920
|
+
this.governance.inFlightPlayCalls - 1,
|
|
921
|
+
);
|
|
922
|
+
const inFlightByPlayId = (this.governance.inFlightPlayCallsByPlayId ??= {});
|
|
923
|
+
const nextPlayCount = Math.max(0, (inFlightByPlayId[playId] ?? 0) - 1);
|
|
924
|
+
if (nextPlayCount === 0) {
|
|
925
|
+
delete inFlightByPlayId[playId];
|
|
926
|
+
} else {
|
|
927
|
+
inFlightByPlayId[playId] = nextPlayCount;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private assertNoDuplicateConcurrentMapBackedPlay(): void {
|
|
932
|
+
if (this.governance.callDepth === 0) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const inFlightCount =
|
|
936
|
+
this.governance.inFlightPlayCallsByPlayId?.[
|
|
937
|
+
this.governance.currentPlayId
|
|
938
|
+
] ?? 0;
|
|
939
|
+
if (inFlightCount <= 1) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Concurrent map-backed play call blocked for ${this.governance.currentPlayId}. ` +
|
|
944
|
+
'A child play that uses ctx.map() cannot run more than once at the same time because its map tables share durable row identity. ' +
|
|
945
|
+
'Run these child play calls sequentially, or give each concurrent branch a different child play/table contract.',
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private acquireToolConcurrency(): void {
|
|
950
|
+
this.governance.inFlightToolCalls += 1;
|
|
951
|
+
if (
|
|
952
|
+
this.governance.inFlightToolCalls >
|
|
953
|
+
this.governance.limits.maxConcurrentToolCalls
|
|
954
|
+
) {
|
|
955
|
+
this.governance.inFlightToolCalls -= 1;
|
|
956
|
+
throw new Error(
|
|
957
|
+
`Concurrent tool-call limit exceeded (${this.governance.limits.maxConcurrentToolCalls}).`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private releaseToolConcurrency(): void {
|
|
963
|
+
this.governance.inFlightToolCalls = Math.max(
|
|
964
|
+
0,
|
|
965
|
+
this.governance.inFlightToolCalls - 1,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
private getCachedToolResult(
|
|
970
|
+
toolId: string,
|
|
971
|
+
rowCacheKey: string,
|
|
972
|
+
): ToolBatchResult | undefined {
|
|
973
|
+
return this.checkpoint.completedToolBatches[toolId]?.[rowCacheKey];
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private cacheToolResult(
|
|
977
|
+
toolId: string,
|
|
978
|
+
rowCacheKey: string,
|
|
979
|
+
result: unknown | null,
|
|
980
|
+
): void {
|
|
981
|
+
if (!this.checkpoint.completedToolBatches[toolId]) {
|
|
982
|
+
this.checkpoint.completedToolBatches[toolId] = {};
|
|
983
|
+
}
|
|
984
|
+
this.checkpoint.completedToolBatches[toolId]![rowCacheKey] = {
|
|
985
|
+
done: true,
|
|
986
|
+
result,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private buildToolResultCacheKey(input: {
|
|
991
|
+
rowId: number;
|
|
992
|
+
tableNamespace?: string;
|
|
993
|
+
rowKey?: string;
|
|
994
|
+
callId?: string;
|
|
995
|
+
}): string {
|
|
996
|
+
const scope = this.governance.currentRunId || this.options.runId || 'run';
|
|
997
|
+
if (input.callId?.trim()) {
|
|
998
|
+
return `${scope}:${input.callId.trim()}`;
|
|
999
|
+
}
|
|
1000
|
+
if (input.rowKey?.trim()) {
|
|
1001
|
+
return `${scope}:${input.rowKey.trim()}`;
|
|
1002
|
+
}
|
|
1003
|
+
if (input.tableNamespace?.trim()) {
|
|
1004
|
+
return `${scope}:${input.tableNamespace.trim()}:${input.rowId}`;
|
|
1005
|
+
}
|
|
1006
|
+
return `${scope}:direct:${input.rowId}`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private async resolveToolResultMetadata(
|
|
1010
|
+
toolId: string,
|
|
1011
|
+
): Promise<ToolResultMetadataInput> {
|
|
1012
|
+
const metadata = await this.options.getToolResultMetadata?.(toolId);
|
|
1013
|
+
return {
|
|
1014
|
+
toolId,
|
|
1015
|
+
resultIdentityGetters: metadata?.resultIdentityGetters ?? {},
|
|
1016
|
+
listExtractorPaths: metadata?.listExtractorPaths ?? [],
|
|
1017
|
+
listIdentityGetters: metadata?.listIdentityGetters ?? {},
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private async wrapToolExecutionResult(input: {
|
|
1022
|
+
toolId: string;
|
|
1023
|
+
status: string;
|
|
1024
|
+
result: unknown;
|
|
1025
|
+
metadata?: ToolResultMetadataInput | null;
|
|
1026
|
+
cached: boolean;
|
|
1027
|
+
source: 'live' | 'checkpoint' | 'cache';
|
|
1028
|
+
cacheKey?: string;
|
|
1029
|
+
}): Promise<ToolExecuteResult> {
|
|
1030
|
+
if (isToolExecuteResult(input.result)) {
|
|
1031
|
+
return cloneToolExecuteResultWithExecution(input.result, {
|
|
1032
|
+
idempotent: true,
|
|
1033
|
+
cached: input.cached,
|
|
1034
|
+
source: input.source,
|
|
1035
|
+
...(input.cacheKey ? { cacheKey: input.cacheKey } : {}),
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return createToolExecuteResult({
|
|
1039
|
+
status: input.status,
|
|
1040
|
+
result: input.result,
|
|
1041
|
+
metadata:
|
|
1042
|
+
input.metadata ?? (await this.resolveToolResultMetadata(input.toolId)),
|
|
1043
|
+
execution: {
|
|
1044
|
+
idempotent: true,
|
|
1045
|
+
cached: input.cached,
|
|
1046
|
+
source: input.source,
|
|
1047
|
+
...(input.cacheKey ? { cacheKey: input.cacheKey } : {}),
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private async resolveToolCall(
|
|
1053
|
+
toolId: string,
|
|
1054
|
+
request: ToolCallRequest,
|
|
1055
|
+
result: unknown | null,
|
|
1056
|
+
metadata?: ToolResultMetadataInput | null,
|
|
1057
|
+
): Promise<void> {
|
|
1058
|
+
const cacheKey = this.buildToolResultCacheKey({
|
|
1059
|
+
rowId: request.rowId,
|
|
1060
|
+
tableNamespace: request.tableNamespace,
|
|
1061
|
+
rowKey: request.rowKey ?? undefined,
|
|
1062
|
+
callId: request.callId,
|
|
1063
|
+
});
|
|
1064
|
+
const wrapped = await this.wrapToolExecutionResult({
|
|
1065
|
+
toolId,
|
|
1066
|
+
status: result == null ? 'no_result' : 'completed',
|
|
1067
|
+
result,
|
|
1068
|
+
metadata,
|
|
1069
|
+
cached: false,
|
|
1070
|
+
source: 'live',
|
|
1071
|
+
cacheKey,
|
|
1072
|
+
});
|
|
1073
|
+
this.cacheToolResult(toolId, cacheKey, wrapped);
|
|
1074
|
+
|
|
1075
|
+
const resolvers = this.toolCallResolvers.get(request.callId);
|
|
1076
|
+
if (resolvers) {
|
|
1077
|
+
for (const resolver of resolvers) {
|
|
1078
|
+
resolver(wrapped);
|
|
1079
|
+
}
|
|
1080
|
+
this.toolCallResolvers.delete(request.callId);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
this.emitScopedFieldMetaUpdate({
|
|
1084
|
+
rowId: request.rowId,
|
|
1085
|
+
key: request.rowKey ?? null,
|
|
1086
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
1087
|
+
fieldName: request.fieldName,
|
|
1088
|
+
status: 'running',
|
|
1089
|
+
rowStatus: 'running',
|
|
1090
|
+
stage: toolId,
|
|
1091
|
+
provider: null,
|
|
1092
|
+
error: null,
|
|
1093
|
+
dataPatch: {},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private pulseProgressHeartbeat(force = false): void {
|
|
1098
|
+
if (!this.options.onBatchComplete) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const now = Date.now();
|
|
1102
|
+
if (
|
|
1103
|
+
!force &&
|
|
1104
|
+
now - this.lastProgressHeartbeatAt < PROGRESS_HEARTBEAT_INTERVAL_MS
|
|
1105
|
+
) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.lastProgressHeartbeatAt = now;
|
|
1109
|
+
this.options.onBatchComplete(this.checkpoint);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ——— Public ctx API ———
|
|
1113
|
+
|
|
1114
|
+
async csv(
|
|
1115
|
+
path: string,
|
|
1116
|
+
_options?: CsvOptions,
|
|
1117
|
+
): Promise<PlayDataset<Record<string, unknown>>> {
|
|
1118
|
+
void _options;
|
|
1119
|
+
// In cloud mode, CSV data is passed in — path is just a label
|
|
1120
|
+
// The activity loads the actual data before creating the ctx
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`ctx.csv("${path}") is handled by the workflow. ` +
|
|
1123
|
+
`CSV data is loaded as a Temporal activity and passed to your play via input.`,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
map<T extends Record<string, unknown>>(
|
|
1128
|
+
key: string,
|
|
1129
|
+
items: PlayDatasetInput<T>,
|
|
1130
|
+
options: MapDefinitionOptions<T>,
|
|
1131
|
+
): RuntimeMapBuilder<T>;
|
|
1132
|
+
map<T extends Record<string, unknown>>(
|
|
1133
|
+
key: string,
|
|
1134
|
+
items: PlayDatasetInput<T>,
|
|
1135
|
+
): RuntimeMapBuilder<T>;
|
|
1136
|
+
map<T extends Record<string, unknown>>(
|
|
1137
|
+
key: string,
|
|
1138
|
+
items: PlayDatasetInput<T>,
|
|
1139
|
+
input: RuntimeStepProgram,
|
|
1140
|
+
options?: MapOptions<T>,
|
|
1141
|
+
): Promise<PlayDataset<Record<string, unknown>>>;
|
|
1142
|
+
map<
|
|
1143
|
+
T extends Record<string, unknown>,
|
|
1144
|
+
TColumns extends Record<string, unknown> = Record<string, unknown>,
|
|
1145
|
+
>(
|
|
1146
|
+
key: string,
|
|
1147
|
+
items: PlayDatasetInput<T>,
|
|
1148
|
+
input?:
|
|
1149
|
+
| MapFieldDefinition<T, TColumns>
|
|
1150
|
+
| RuntimeStepProgram
|
|
1151
|
+
| MapDefinitionOptions<T>,
|
|
1152
|
+
options?: MapOptions<T>,
|
|
1153
|
+
): RuntimeMapBuilder<T> | Promise<PlayDataset<Record<string, unknown>>> {
|
|
1154
|
+
if (rowContext.getStore()) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
'Nested ctx.map() is not supported. Flatten your fields into one map, or keep custom per-row logic inside a single map field.',
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
this.assertNoDuplicateConcurrentMapBackedPlay();
|
|
1160
|
+
|
|
1161
|
+
if (input === undefined || isMapDefinitionOptions(input)) {
|
|
1162
|
+
return new RuntimeMapBuilder(
|
|
1163
|
+
this,
|
|
1164
|
+
key,
|
|
1165
|
+
items,
|
|
1166
|
+
input as MapDefinitionOptions<T> | undefined,
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (isRuntimeStepProgram(input)) {
|
|
1171
|
+
return this.runStepProgramMap(key, items, input, options);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
throw new Error('ctx.map() accepts key, rows, and map options only.');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async runStepProgramMap<T extends Record<string, unknown>>(
|
|
1178
|
+
key: string,
|
|
1179
|
+
items: PlayDatasetInput<T>,
|
|
1180
|
+
program: RuntimeStepProgram,
|
|
1181
|
+
options?: MapOptions<T>,
|
|
1182
|
+
): Promise<PlayDataset<Record<string, unknown>>> {
|
|
1183
|
+
return this.runMapDefinition(
|
|
1184
|
+
key,
|
|
1185
|
+
items,
|
|
1186
|
+
this.stepProgramToMapDefinition(program),
|
|
1187
|
+
options,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private async runMapDefinition<
|
|
1192
|
+
T extends Record<string, unknown>,
|
|
1193
|
+
TColumns extends Record<string, unknown> = Record<string, unknown>,
|
|
1194
|
+
>(
|
|
1195
|
+
key: string,
|
|
1196
|
+
items: PlayDatasetInput<T>,
|
|
1197
|
+
input: MapFieldDefinition<T, TColumns>,
|
|
1198
|
+
options?: MapOptions<T>,
|
|
1199
|
+
): Promise<PlayDataset<Record<string, unknown>>> {
|
|
1200
|
+
if (rowContext.getStore()) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
'Nested ctx.map() is not supported. Flatten your fields into one map, or keep custom per-row logic inside a single map field.',
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
this.assertNoDuplicateConcurrentMapBackedPlay();
|
|
1206
|
+
|
|
1207
|
+
const normalizedMapKey = this.normalizeContextKey(key, 'map');
|
|
1208
|
+
|
|
1209
|
+
const normalizedMapNamespace = normalizeTableNamespace(normalizedMapKey);
|
|
1210
|
+
const staleAfterSeconds = options?.staleAfterSeconds ?? null;
|
|
1211
|
+
try {
|
|
1212
|
+
resolveStaleMapTableNamespace(normalizedMapNamespace, staleAfterSeconds);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
error instanceof Error
|
|
1216
|
+
? `${error.message} Example: ctx.map(\"${normalizedMapNamespace}\", rows, { key: \"stable_id\", staleAfterSeconds: 86400 }).step("company", row => row.domain).run({ description: "..." }).`
|
|
1217
|
+
: `ctx.map() key must normalize to <= ${MAP_KEY_NAMESPACE_MAX_LENGTH} characters.`,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (this.explicitMapInvocationKeys.has(normalizedMapNamespace)) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`Duplicate ctx.map() key "${normalizedMapNamespace}" in the same play. ` +
|
|
1223
|
+
'Each ctx.map() call must use a distinct idempotency key.',
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
this.explicitMapInvocationKeys.add(normalizedMapNamespace);
|
|
1227
|
+
|
|
1228
|
+
let resolvedTableNamespace = resolveStaleMapTableNamespace(
|
|
1229
|
+
normalizedMapNamespace,
|
|
1230
|
+
staleAfterSeconds,
|
|
1231
|
+
);
|
|
1232
|
+
let totalInputCount = 0;
|
|
1233
|
+
let rawItems: Record<string, unknown>[] = [];
|
|
1234
|
+
let itemsToProcess: Array<Record<string, unknown>> = [];
|
|
1235
|
+
let completedItemsByKey: Map<string, Record<string, unknown>> | null = null;
|
|
1236
|
+
const mapFieldNames = Object.keys(input);
|
|
1237
|
+
const userKeyFn = options?.key;
|
|
1238
|
+
const explicitKeyResolver = createExplicitMapKeyResolver({
|
|
1239
|
+
mapNamespace: normalizedMapNamespace,
|
|
1240
|
+
fieldNames: mapFieldNames,
|
|
1241
|
+
key: userKeyFn as Parameters<
|
|
1242
|
+
typeof createExplicitMapKeyResolver
|
|
1243
|
+
>[0]['key'],
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
const rowIdentity = (row: Record<string, unknown>, index = 0) =>
|
|
1247
|
+
deriveMapRowIdentity({
|
|
1248
|
+
row,
|
|
1249
|
+
index,
|
|
1250
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
1251
|
+
fieldNames: mapFieldNames,
|
|
1252
|
+
explicitKey: explicitKeyResolver,
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
const materializedItems = await materializePlayDatasetInput(items);
|
|
1256
|
+
totalInputCount = materializedItems.length;
|
|
1257
|
+
rawItems = materializedItems.map((item) => this.toOutputRow(item));
|
|
1258
|
+
itemsToProcess = materializedItems.map((item) =>
|
|
1259
|
+
this.toOutputRow(item as Record<string, unknown>),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
assertUniqueExplicitMapKeys({
|
|
1263
|
+
mapNamespace: normalizedMapNamespace,
|
|
1264
|
+
rows: rawItems,
|
|
1265
|
+
resolver: explicitKeyResolver,
|
|
1266
|
+
});
|
|
1267
|
+
if (this.options.onMapStart) {
|
|
1268
|
+
const mapStartResult = await this.options.onMapStart(
|
|
1269
|
+
rawItems,
|
|
1270
|
+
resolvedTableNamespace,
|
|
1271
|
+
{
|
|
1272
|
+
playName: this.options.playName,
|
|
1273
|
+
playId: this.options.playId,
|
|
1274
|
+
runId: this.options.runId,
|
|
1275
|
+
staticPipeline: this.options.staticPipeline,
|
|
1276
|
+
},
|
|
1277
|
+
);
|
|
1278
|
+
resolvedTableNamespace = normalizeTableNamespace(
|
|
1279
|
+
mapStartResult.tableNamespace,
|
|
1280
|
+
);
|
|
1281
|
+
const pendingKeys = new Set(
|
|
1282
|
+
mapStartResult.pendingRows.map((row) => rowIdentity(row)),
|
|
1283
|
+
);
|
|
1284
|
+
itemsToProcess = materializedItems
|
|
1285
|
+
.map((item) => this.toOutputRow(item as Record<string, unknown>))
|
|
1286
|
+
.filter((row) => pendingKeys.has(rowIdentity(row)));
|
|
1287
|
+
if (mapStartResult.completedRows.length > 0) {
|
|
1288
|
+
completedItemsByKey = new Map();
|
|
1289
|
+
for (const row of mapStartResult.completedRows) {
|
|
1290
|
+
const rowKey = rowIdentity(row);
|
|
1291
|
+
if (rowKey) completedItemsByKey.set(rowKey, row);
|
|
1292
|
+
}
|
|
1293
|
+
this.log(
|
|
1294
|
+
`Resuming: ${mapStartResult.completedRows.length} already completed, ${itemsToProcess.length} pending`,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const mapScope = this.mapRowIdentity.createScope({
|
|
1300
|
+
logicalNamespace: normalizedMapNamespace,
|
|
1301
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
1302
|
+
explicitKey: explicitKeyResolver,
|
|
1303
|
+
fieldNames: mapFieldNames,
|
|
1304
|
+
});
|
|
1305
|
+
const completedRowKeys =
|
|
1306
|
+
completedItemsByKey != null ? [...completedItemsByKey.keys()] : [];
|
|
1307
|
+
const pendingRowKeys = itemsToProcess.map((item) =>
|
|
1308
|
+
mapScope.rowIdentity(this.toOutputRow(item)),
|
|
1309
|
+
);
|
|
1310
|
+
this.mapFrames.start({
|
|
1311
|
+
scope: mapScope,
|
|
1312
|
+
totalRows: totalInputCount,
|
|
1313
|
+
completedRowKeys,
|
|
1314
|
+
pendingRowKeys,
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
const mapResults = await this.runFieldMap(
|
|
1318
|
+
itemsToProcess,
|
|
1319
|
+
mapScope,
|
|
1320
|
+
input as MapFieldDefinition<Record<string, unknown>>,
|
|
1321
|
+
options?.description,
|
|
1322
|
+
{
|
|
1323
|
+
totalRows: totalInputCount,
|
|
1324
|
+
completedRows: completedItemsByKey?.size ?? 0,
|
|
1325
|
+
},
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
const results =
|
|
1329
|
+
completedItemsByKey && completedItemsByKey.size > 0
|
|
1330
|
+
? (() => {
|
|
1331
|
+
const resultsByKey = new Map<string, Record<string, unknown>>();
|
|
1332
|
+
for (let index = 0; index < mapResults.length; index += 1) {
|
|
1333
|
+
const row = mapResults[index]!;
|
|
1334
|
+
const rowIdentityRow = this.toOutputRow(
|
|
1335
|
+
itemsToProcess[index] as Record<string, unknown>,
|
|
1336
|
+
);
|
|
1337
|
+
const rowKey = rowIdentity(rowIdentityRow);
|
|
1338
|
+
if (rowKey) resultsByKey.set(rowKey, row);
|
|
1339
|
+
}
|
|
1340
|
+
return rawItems.map((rawItem) => {
|
|
1341
|
+
const rowKey = rowIdentity(rawItem);
|
|
1342
|
+
return (
|
|
1343
|
+
resultsByKey.get(rowKey) ??
|
|
1344
|
+
completedItemsByKey!.get(rowKey) ??
|
|
1345
|
+
rawItem
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
})()
|
|
1349
|
+
: mapResults;
|
|
1350
|
+
|
|
1351
|
+
return createDeferredPlayDataset({
|
|
1352
|
+
datasetKind: 'map',
|
|
1353
|
+
datasetId: createRuntimeDatasetId(
|
|
1354
|
+
this.options.playName ?? this.options.playId ?? 'play',
|
|
1355
|
+
resolvedTableNamespace,
|
|
1356
|
+
),
|
|
1357
|
+
count: results.length,
|
|
1358
|
+
previewRows: results.slice(0, 10),
|
|
1359
|
+
tableNamespace: resolvedTableNamespace,
|
|
1360
|
+
resolvers: {
|
|
1361
|
+
count: async () => results.length,
|
|
1362
|
+
peek: async (limit) => results.slice(0, Math.max(0, limit)),
|
|
1363
|
+
materialize: async (limit) =>
|
|
1364
|
+
limit === undefined
|
|
1365
|
+
? [...results]
|
|
1366
|
+
: results.slice(0, Math.max(0, limit)),
|
|
1367
|
+
iterate: () =>
|
|
1368
|
+
({
|
|
1369
|
+
async *[Symbol.asyncIterator]() {
|
|
1370
|
+
for (const row of results) {
|
|
1371
|
+
yield row;
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
}) as AsyncIterable<Record<string, unknown>>,
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private async runFieldMap<T extends Record<string, unknown>>(
|
|
1380
|
+
items: T[],
|
|
1381
|
+
mapScope: MapExecutionScope,
|
|
1382
|
+
definition: MapFieldDefinition<T>,
|
|
1383
|
+
description?: string,
|
|
1384
|
+
executionSummary?: {
|
|
1385
|
+
totalRows: number;
|
|
1386
|
+
completedRows: number;
|
|
1387
|
+
},
|
|
1388
|
+
runtimeOptions?: {
|
|
1389
|
+
emitTerminalEvent?: boolean;
|
|
1390
|
+
},
|
|
1391
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
1392
|
+
const fieldEntries = Object.entries(definition);
|
|
1393
|
+
const mapFieldNames = fieldEntries.map(([fieldName]) => fieldName);
|
|
1394
|
+
const visibleFields = fieldEntries
|
|
1395
|
+
.map(([fieldName]) => fieldName)
|
|
1396
|
+
.filter((fieldName) => !fieldName.startsWith('_'));
|
|
1397
|
+
const normalizedTableNamespace = mapScope.artifactTableNamespace;
|
|
1398
|
+
const rowIdentity = (row: Record<string, unknown>, index = 0) =>
|
|
1399
|
+
mapScope.rowIdentity(
|
|
1400
|
+
Object.fromEntries(
|
|
1401
|
+
Object.entries(row).filter(
|
|
1402
|
+
([fieldName]) => !mapFieldNames.includes(fieldName),
|
|
1403
|
+
),
|
|
1404
|
+
),
|
|
1405
|
+
index,
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
const totalRows = Math.max(
|
|
1409
|
+
executionSummary?.totalRows ?? items.length,
|
|
1410
|
+
items.length,
|
|
1411
|
+
);
|
|
1412
|
+
const completedRows = Math.min(
|
|
1413
|
+
executionSummary?.completedRows ?? 0,
|
|
1414
|
+
totalRows,
|
|
1415
|
+
);
|
|
1416
|
+
const pendingRows = Math.max(0, items.length);
|
|
1417
|
+
const emitTerminalEvent = runtimeOptions?.emitTerminalEvent !== false;
|
|
1418
|
+
|
|
1419
|
+
if (completedRows > 0 || pendingRows !== totalRows) {
|
|
1420
|
+
this.log(
|
|
1421
|
+
`Starting map over ${totalRows} items with ${visibleFields.length} fields (key: ${normalizedTableNamespace}; ${completedRows} already satisfied; ${pendingRows} pending)`,
|
|
1422
|
+
);
|
|
1423
|
+
} else {
|
|
1424
|
+
this.log(
|
|
1425
|
+
`Starting map over ${items.length} items with ${visibleFields.length} fields (key: ${normalizedTableNamespace})`,
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
this.processedRowCount = items.length;
|
|
1429
|
+
const mapStep: Extract<PlayStep, { type: 'map' }> = {
|
|
1430
|
+
type: 'map',
|
|
1431
|
+
items: items.length,
|
|
1432
|
+
fields: visibleFields,
|
|
1433
|
+
substeps: [],
|
|
1434
|
+
description: normalizeStepDescription(description),
|
|
1435
|
+
};
|
|
1436
|
+
this.steps.push(mapStep);
|
|
1437
|
+
this.activeMapStep = mapStep;
|
|
1438
|
+
|
|
1439
|
+
const updateMapFrameProgress = (input: {
|
|
1440
|
+
status?: MapExecutionFrame['status'];
|
|
1441
|
+
completedRowKey?: string | null;
|
|
1442
|
+
pendingRowKey?: string | null;
|
|
1443
|
+
activeBoundaryId?: string | null;
|
|
1444
|
+
emitEventType?: PlayExecutionEvent['type'];
|
|
1445
|
+
}) => {
|
|
1446
|
+
this.mapFrames.updateProgress({
|
|
1447
|
+
...input,
|
|
1448
|
+
scope: mapScope,
|
|
1449
|
+
totalRows,
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
if (this.canUsePureJsMapFastPath(definition)) {
|
|
1454
|
+
const results = await this.runPureFieldMap(
|
|
1455
|
+
items,
|
|
1456
|
+
fieldEntries,
|
|
1457
|
+
visibleFields,
|
|
1458
|
+
normalizedTableNamespace,
|
|
1459
|
+
);
|
|
1460
|
+
for (const row of results) {
|
|
1461
|
+
updateMapFrameProgress({
|
|
1462
|
+
completedRowKey: rowIdentity(row),
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
if (emitTerminalEvent) {
|
|
1466
|
+
updateMapFrameProgress({
|
|
1467
|
+
status: 'completed',
|
|
1468
|
+
activeBoundaryId: null,
|
|
1469
|
+
emitEventType: 'map.completed',
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
this.lastMapStep = mapStep;
|
|
1473
|
+
this.activeMapStep = null;
|
|
1474
|
+
this.log(
|
|
1475
|
+
`Map completed: ${results.length + completedRows} results (${results.length} executed, ${completedRows} already satisfied)`,
|
|
1476
|
+
);
|
|
1477
|
+
return results;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
this.initializeRowStates(items);
|
|
1481
|
+
|
|
1482
|
+
const promises = items.map(async (item, idx) => {
|
|
1483
|
+
const baseRow = this.toOutputRow(item);
|
|
1484
|
+
const rowKey = rowIdentity(baseRow, idx);
|
|
1485
|
+
const computedFields: Record<string, unknown> = {};
|
|
1486
|
+
let activeFieldName: string | null = null;
|
|
1487
|
+
|
|
1488
|
+
try {
|
|
1489
|
+
for (const [fieldName, resolver] of fieldEntries) {
|
|
1490
|
+
activeFieldName = fieldName;
|
|
1491
|
+
if (this.shouldReuseExistingFieldValue(baseRow, fieldName)) {
|
|
1492
|
+
const reusedValue = baseRow[fieldName];
|
|
1493
|
+
computedFields[fieldName] = reusedValue;
|
|
1494
|
+
this.rowStates.get(idx)?.results.set(fieldName, reusedValue);
|
|
1495
|
+
this.emitScopedFieldMetaUpdate({
|
|
1496
|
+
rowId: idx,
|
|
1497
|
+
key: rowKey,
|
|
1498
|
+
tableNamespace: normalizedTableNamespace,
|
|
1499
|
+
fieldName,
|
|
1500
|
+
status: 'cached',
|
|
1501
|
+
reused: true,
|
|
1502
|
+
dataPatch: {},
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
this.emitScopedFieldMetaUpdate({
|
|
1508
|
+
rowId: idx,
|
|
1509
|
+
key: rowKey,
|
|
1510
|
+
tableNamespace: normalizedTableNamespace,
|
|
1511
|
+
fieldName,
|
|
1512
|
+
status: 'running',
|
|
1513
|
+
rowStatus: 'running',
|
|
1514
|
+
stage: fieldName,
|
|
1515
|
+
dataPatch: {},
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
const value = await rowContext.run(
|
|
1519
|
+
{
|
|
1520
|
+
rowId: idx,
|
|
1521
|
+
fieldName,
|
|
1522
|
+
tableNamespace: normalizedTableNamespace,
|
|
1523
|
+
rowKey,
|
|
1524
|
+
mapScope,
|
|
1525
|
+
},
|
|
1526
|
+
async () =>
|
|
1527
|
+
await this.resolveMapFieldValue(
|
|
1528
|
+
resolver,
|
|
1529
|
+
item,
|
|
1530
|
+
{ ...baseRow, ...computedFields },
|
|
1531
|
+
idx,
|
|
1532
|
+
),
|
|
1533
|
+
);
|
|
1534
|
+
computedFields[fieldName] = value;
|
|
1535
|
+
this.rowStates.get(idx)?.results.set(fieldName, value);
|
|
1536
|
+
this.emitScopedFieldMetaUpdate({
|
|
1537
|
+
rowId: idx,
|
|
1538
|
+
key: rowKey,
|
|
1539
|
+
tableNamespace: normalizedTableNamespace,
|
|
1540
|
+
fieldName,
|
|
1541
|
+
status: 'completed',
|
|
1542
|
+
stage: 'completed',
|
|
1543
|
+
dataPatch: fieldName.startsWith('_') ? {} : { [fieldName]: value },
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const merged = { ...baseRow, ...computedFields };
|
|
1548
|
+
activeFieldName = null;
|
|
1549
|
+
updateMapFrameProgress({
|
|
1550
|
+
completedRowKey: rowKey,
|
|
1551
|
+
});
|
|
1552
|
+
this.emitScopedRowUpdate(rowKey, normalizedTableNamespace, {
|
|
1553
|
+
rowId: idx,
|
|
1554
|
+
status: 'completed',
|
|
1555
|
+
stage: 'completed',
|
|
1556
|
+
provider: null,
|
|
1557
|
+
error: null,
|
|
1558
|
+
dataPatch: {},
|
|
1559
|
+
});
|
|
1560
|
+
return Object.fromEntries(
|
|
1561
|
+
Object.entries(merged).filter(
|
|
1562
|
+
([fieldName]) => !fieldName.startsWith('_'),
|
|
1563
|
+
),
|
|
1564
|
+
);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
if (isPlayRowExecutionSuspendedError(error)) {
|
|
1567
|
+
this.pendingRowEventBoundaries.push(error.boundary);
|
|
1568
|
+
this.rowStates.get(idx)!.status = 'waiting';
|
|
1569
|
+
updateMapFrameProgress({
|
|
1570
|
+
pendingRowKey: rowKey,
|
|
1571
|
+
activeBoundaryId: error.boundary.boundaryId,
|
|
1572
|
+
emitEventType: 'map.suspended',
|
|
1573
|
+
});
|
|
1574
|
+
const fieldName = rowContext.getStore()?.fieldName ?? activeFieldName;
|
|
1575
|
+
this.emitScopedFieldMetaUpdate({
|
|
1576
|
+
rowId: idx,
|
|
1577
|
+
key: rowKey,
|
|
1578
|
+
tableNamespace: normalizedTableNamespace,
|
|
1579
|
+
fieldName,
|
|
1580
|
+
status: 'running',
|
|
1581
|
+
rowStatus: 'running',
|
|
1582
|
+
stage: 'waiting_for_event',
|
|
1583
|
+
provider: null,
|
|
1584
|
+
error: null,
|
|
1585
|
+
dataPatch: {},
|
|
1586
|
+
});
|
|
1587
|
+
return WAITING_ROW;
|
|
1588
|
+
}
|
|
1589
|
+
const fieldName = rowContext.getStore()?.fieldName;
|
|
1590
|
+
updateMapFrameProgress({
|
|
1591
|
+
status: 'failed',
|
|
1592
|
+
emitEventType: 'map.failed',
|
|
1593
|
+
});
|
|
1594
|
+
this.emitScopedFieldMetaUpdate({
|
|
1595
|
+
rowId: idx,
|
|
1596
|
+
key: rowKey,
|
|
1597
|
+
tableNamespace: normalizedTableNamespace,
|
|
1598
|
+
fieldName: fieldName ?? activeFieldName,
|
|
1599
|
+
status: 'failed',
|
|
1600
|
+
rowStatus: 'failed',
|
|
1601
|
+
stage: 'failed',
|
|
1602
|
+
provider: null,
|
|
1603
|
+
error: this.formatRuntimeError(error),
|
|
1604
|
+
dataPatch: {},
|
|
1605
|
+
});
|
|
1606
|
+
throw error;
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
await this.drainQueuedWork(promises);
|
|
1611
|
+
this.lastMapStep = mapStep;
|
|
1612
|
+
this.activeMapStep = null;
|
|
1613
|
+
|
|
1614
|
+
const settledResults = await Promise.all(promises);
|
|
1615
|
+
if (this.pendingRowEventBoundaries.length > 0) {
|
|
1616
|
+
const completedRowKeys = new Set<string>();
|
|
1617
|
+
for (let index = 0; index < settledResults.length; index += 1) {
|
|
1618
|
+
const result = settledResults[index];
|
|
1619
|
+
const rawItem = this.toOutputRow(
|
|
1620
|
+
items[index] as Record<string, unknown>,
|
|
1621
|
+
);
|
|
1622
|
+
if (result === WAITING_ROW) {
|
|
1623
|
+
const key = rowIdentity(rawItem);
|
|
1624
|
+
if (key) {
|
|
1625
|
+
completedRowKeys.add(key);
|
|
1626
|
+
}
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
const row = result as Record<string, unknown>;
|
|
1630
|
+
const key = rowIdentity(row);
|
|
1631
|
+
if (key) completedRowKeys.add(key);
|
|
1632
|
+
}
|
|
1633
|
+
this.mapFrames.set({
|
|
1634
|
+
...(this.checkpoint.mapFrames?.[mapScope.mapInvocationId] ?? {
|
|
1635
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
1636
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
1637
|
+
artifactTableNamespace: mapScope.artifactTableNamespace,
|
|
1638
|
+
status: 'suspended' as const,
|
|
1639
|
+
totalRows,
|
|
1640
|
+
completedRowKeys: [...completedRowKeys],
|
|
1641
|
+
pendingRowKeys: items.map((item) =>
|
|
1642
|
+
rowIdentity(this.toOutputRow(item as Record<string, unknown>)),
|
|
1643
|
+
),
|
|
1644
|
+
startedAt: Date.now(),
|
|
1645
|
+
updatedAt: Date.now(),
|
|
1646
|
+
}),
|
|
1647
|
+
status: 'suspended',
|
|
1648
|
+
completedRowKeys: [...completedRowKeys],
|
|
1649
|
+
updatedAt: Date.now(),
|
|
1650
|
+
});
|
|
1651
|
+
const uniqueBoundaries = [
|
|
1652
|
+
...new Map(
|
|
1653
|
+
this.pendingRowEventBoundaries.map((boundary) => [
|
|
1654
|
+
boundary.boundaryId,
|
|
1655
|
+
boundary,
|
|
1656
|
+
]),
|
|
1657
|
+
).values(),
|
|
1658
|
+
];
|
|
1659
|
+
this.pendingRowEventBoundaries = [];
|
|
1660
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
1661
|
+
throw new PlayExecutionSuspendedError({
|
|
1662
|
+
kind: 'integration_event_batch',
|
|
1663
|
+
boundaries: uniqueBoundaries,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
const results = settledResults.filter(
|
|
1667
|
+
(result): result is Record<string, unknown> => result !== WAITING_ROW,
|
|
1668
|
+
);
|
|
1669
|
+
for (const row of results) {
|
|
1670
|
+
updateMapFrameProgress({
|
|
1671
|
+
completedRowKey: rowIdentity(row),
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (emitTerminalEvent) {
|
|
1675
|
+
updateMapFrameProgress({
|
|
1676
|
+
status: 'completed',
|
|
1677
|
+
activeBoundaryId: null,
|
|
1678
|
+
emitEventType: 'map.completed',
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
this.log(
|
|
1682
|
+
`Map completed: ${results.length + completedRows} results (${results.length} executed, ${completedRows} already satisfied)`,
|
|
1683
|
+
);
|
|
1684
|
+
return results;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
private stepProgramToMapDefinition(
|
|
1688
|
+
program: RuntimeStepProgram,
|
|
1689
|
+
): MapFieldDefinition<Record<string, unknown>> {
|
|
1690
|
+
const definition: MapFieldDefinition<Record<string, unknown>> = {};
|
|
1691
|
+
for (const step of program.steps) {
|
|
1692
|
+
const resolver: MapFieldResolver<
|
|
1693
|
+
Record<string, unknown>,
|
|
1694
|
+
Record<string, unknown>
|
|
1695
|
+
> = async (
|
|
1696
|
+
_row: Record<string, unknown>,
|
|
1697
|
+
_ctx: unknown,
|
|
1698
|
+
currentRow: Record<string, unknown>,
|
|
1699
|
+
index: number,
|
|
1700
|
+
) =>
|
|
1701
|
+
await this.executeStepProgramStep(step, currentRow, index, [step.name]);
|
|
1702
|
+
definition[step.name] = resolver;
|
|
1703
|
+
}
|
|
1704
|
+
Object.defineProperty(definition, STEP_PROGRAM_MAP_DEFINITION, {
|
|
1705
|
+
value: true,
|
|
1706
|
+
enumerable: false,
|
|
1707
|
+
});
|
|
1708
|
+
return definition;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
private async executeStepProgram(
|
|
1712
|
+
program: RuntimeStepProgram,
|
|
1713
|
+
row: Record<string, unknown>,
|
|
1714
|
+
index: number,
|
|
1715
|
+
path: string[],
|
|
1716
|
+
): Promise<unknown> {
|
|
1717
|
+
let currentRow = { ...row };
|
|
1718
|
+
const produced: Record<string, unknown> = {};
|
|
1719
|
+
for (const step of program.steps) {
|
|
1720
|
+
const value = await this.executeStepProgramStep(step, currentRow, index, [
|
|
1721
|
+
...path,
|
|
1722
|
+
step.name,
|
|
1723
|
+
]);
|
|
1724
|
+
produced[step.name] = value;
|
|
1725
|
+
currentRow = { ...currentRow, [step.name]: value };
|
|
1726
|
+
}
|
|
1727
|
+
if (typeof program.returnResolver === 'function') {
|
|
1728
|
+
return await program.returnResolver(currentRow, this, index);
|
|
1729
|
+
}
|
|
1730
|
+
return produced;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
private async executeStepProgramStep(
|
|
1734
|
+
step: RuntimeStepProgramStep,
|
|
1735
|
+
currentRow: Record<string, unknown>,
|
|
1736
|
+
index: number,
|
|
1737
|
+
path: string[],
|
|
1738
|
+
): Promise<unknown> {
|
|
1739
|
+
const resolver = step.resolver;
|
|
1740
|
+
const store = rowContext.getStore();
|
|
1741
|
+
const nestedFieldName = path.join('.');
|
|
1742
|
+
const runWithStepScope = async (run: () => Promise<unknown>) => {
|
|
1743
|
+
if (!store) return await run();
|
|
1744
|
+
return await rowContext.run(
|
|
1745
|
+
{
|
|
1746
|
+
...store,
|
|
1747
|
+
fieldName: nestedFieldName,
|
|
1748
|
+
},
|
|
1749
|
+
run,
|
|
1750
|
+
);
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
if (isRuntimeStepProgram(resolver)) {
|
|
1754
|
+
return await runWithStepScope(
|
|
1755
|
+
async () =>
|
|
1756
|
+
await this.executeStepProgram(resolver, currentRow, index, path),
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (isRuntimeConditionalStepResolver(resolver)) {
|
|
1761
|
+
const shouldRun = await resolver.when(currentRow, index);
|
|
1762
|
+
if (!shouldRun) {
|
|
1763
|
+
const elseValue = Object.prototype.hasOwnProperty.call(
|
|
1764
|
+
resolver,
|
|
1765
|
+
'elseValue',
|
|
1766
|
+
)
|
|
1767
|
+
? resolver.elseValue
|
|
1768
|
+
: null;
|
|
1769
|
+
if (store) {
|
|
1770
|
+
this.emitScopedFieldMetaUpdate({
|
|
1771
|
+
rowId: store.rowId,
|
|
1772
|
+
key: store.rowKey ?? null,
|
|
1773
|
+
tableNamespace: store.tableNamespace ?? null,
|
|
1774
|
+
fieldName: nestedFieldName,
|
|
1775
|
+
status: 'skipped',
|
|
1776
|
+
rowStatus: 'running',
|
|
1777
|
+
stage: 'skipped',
|
|
1778
|
+
provider: null,
|
|
1779
|
+
error: null,
|
|
1780
|
+
dataPatch: {},
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return elseValue;
|
|
1784
|
+
}
|
|
1785
|
+
return await runWithStepScope(
|
|
1786
|
+
async () => await resolver.run(currentRow, this, index),
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (typeof resolver !== 'function') {
|
|
1791
|
+
return resolver;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
return await runWithStepScope(
|
|
1795
|
+
async () => await resolver(currentRow, this, index),
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private canUsePureJsMapFastPath<T>(
|
|
1800
|
+
definition: MapFieldDefinition<T>,
|
|
1801
|
+
): boolean {
|
|
1802
|
+
if (
|
|
1803
|
+
(definition as Record<PropertyKey, unknown>)[STEP_PROGRAM_MAP_DEFINITION]
|
|
1804
|
+
) {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
const toolExecuteMarker = ['.tools', 'execute('].join('.');
|
|
1808
|
+
return Object.values(definition).every((resolver) => {
|
|
1809
|
+
if (typeof resolver !== 'function') {
|
|
1810
|
+
return true;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const source = Function.prototype.toString.call(resolver);
|
|
1814
|
+
return (
|
|
1815
|
+
!source.includes(toolExecuteMarker) && !source.includes('.waterfall(')
|
|
1816
|
+
);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
private async runPureFieldMap<T>(
|
|
1821
|
+
items: T[],
|
|
1822
|
+
fieldEntries: [string, MapFieldDefinition<T>[string]][],
|
|
1823
|
+
visibleFields: string[],
|
|
1824
|
+
tableNamespace: string,
|
|
1825
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
1826
|
+
const results: Array<Record<string, unknown>> = [];
|
|
1827
|
+
this.pureMapExecutionActive = true;
|
|
1828
|
+
|
|
1829
|
+
const rowIdentity = (row: Record<string, unknown>) =>
|
|
1830
|
+
derivePlayRowIdentity(row, tableNamespace);
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1834
|
+
const item = items[index]!;
|
|
1835
|
+
const baseRow = this.toOutputRow(item);
|
|
1836
|
+
const computedFields: Record<string, unknown> = {};
|
|
1837
|
+
let activeFieldName: string | null = null;
|
|
1838
|
+
|
|
1839
|
+
try {
|
|
1840
|
+
for (const [fieldName, resolver] of fieldEntries) {
|
|
1841
|
+
activeFieldName = fieldName;
|
|
1842
|
+
if (this.shouldReuseExistingFieldValue(baseRow, fieldName)) {
|
|
1843
|
+
computedFields[fieldName] = baseRow[fieldName];
|
|
1844
|
+
this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
|
|
1845
|
+
rowId: index,
|
|
1846
|
+
status: undefined,
|
|
1847
|
+
stage: null,
|
|
1848
|
+
provider: null,
|
|
1849
|
+
error: null,
|
|
1850
|
+
dataPatch: {},
|
|
1851
|
+
cellMetaPatch: {
|
|
1852
|
+
[fieldName]: {
|
|
1853
|
+
status: 'cached',
|
|
1854
|
+
reused: true,
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
|
|
1862
|
+
rowId: index,
|
|
1863
|
+
status: 'running',
|
|
1864
|
+
stage: fieldName,
|
|
1865
|
+
provider: null,
|
|
1866
|
+
error: null,
|
|
1867
|
+
dataPatch: {},
|
|
1868
|
+
cellMetaPatch: {
|
|
1869
|
+
[fieldName]: {
|
|
1870
|
+
status: 'running',
|
|
1871
|
+
stage: fieldName,
|
|
1872
|
+
},
|
|
1873
|
+
},
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
computedFields[fieldName] = await this.resolveMapFieldValue(
|
|
1877
|
+
resolver,
|
|
1878
|
+
item,
|
|
1879
|
+
{ ...baseRow, ...computedFields },
|
|
1880
|
+
index,
|
|
1881
|
+
);
|
|
1882
|
+
this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
|
|
1883
|
+
rowId: index,
|
|
1884
|
+
status: undefined,
|
|
1885
|
+
stage: 'completed',
|
|
1886
|
+
provider: null,
|
|
1887
|
+
error: null,
|
|
1888
|
+
dataPatch: fieldName.startsWith('_')
|
|
1889
|
+
? {}
|
|
1890
|
+
: { [fieldName]: computedFields[fieldName] },
|
|
1891
|
+
cellMetaPatch: {
|
|
1892
|
+
[fieldName]: {
|
|
1893
|
+
status: 'completed',
|
|
1894
|
+
stage: 'completed',
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const merged = { ...baseRow, ...computedFields };
|
|
1901
|
+
results.push(
|
|
1902
|
+
Object.fromEntries(
|
|
1903
|
+
Object.entries(merged).filter(
|
|
1904
|
+
([fieldName]) => !fieldName.startsWith('_'),
|
|
1905
|
+
),
|
|
1906
|
+
),
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1909
|
+
const rowKey = rowIdentity(baseRow);
|
|
1910
|
+
activeFieldName = null;
|
|
1911
|
+
this.emitScopedRowUpdate(rowKey, tableNamespace, {
|
|
1912
|
+
rowId: index,
|
|
1913
|
+
status: 'completed',
|
|
1914
|
+
stage: 'completed',
|
|
1915
|
+
provider: null,
|
|
1916
|
+
error: null,
|
|
1917
|
+
dataPatch: {},
|
|
1918
|
+
});
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
const rowKey = rowIdentity(baseRow);
|
|
1921
|
+
this.emitScopedRowUpdate(rowKey, tableNamespace, {
|
|
1922
|
+
rowId: index,
|
|
1923
|
+
status: 'failed',
|
|
1924
|
+
stage: 'failed',
|
|
1925
|
+
provider: null,
|
|
1926
|
+
error: this.formatRuntimeError(error),
|
|
1927
|
+
dataPatch: {},
|
|
1928
|
+
cellMetaPatch: {
|
|
1929
|
+
[String(activeFieldName ?? '__unknown')]: {
|
|
1930
|
+
status: 'failed',
|
|
1931
|
+
stage: 'failed',
|
|
1932
|
+
error: this.formatRuntimeError(error),
|
|
1933
|
+
},
|
|
1934
|
+
},
|
|
1935
|
+
});
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if ((index + 1) % PURE_JS_HEARTBEAT_ROW_INTERVAL === 0) {
|
|
1940
|
+
this.pulseProgressHeartbeat();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
} finally {
|
|
1944
|
+
this.pulseProgressHeartbeat(true);
|
|
1945
|
+
this.pureMapExecutionActive = false;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
return results;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
private initializeRowStates<T>(items: T[]): void {
|
|
1952
|
+
items.forEach((item, idx) => {
|
|
1953
|
+
this.rowStates.set(idx, {
|
|
1954
|
+
id: idx,
|
|
1955
|
+
input: item,
|
|
1956
|
+
status: 'pending',
|
|
1957
|
+
waterfalls: new Map(),
|
|
1958
|
+
toolCalls: new Map(),
|
|
1959
|
+
results: new Map(),
|
|
1960
|
+
});
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
private async drainQueuedWork<T>(promises: Promise<T>[]): Promise<void> {
|
|
1965
|
+
// Drain loop: each pass resolves queued waterfalls/tool calls.
|
|
1966
|
+
// When a batch resolves, rows resume and may queue MORE calls
|
|
1967
|
+
// (e.g. row executes one tool request, then queues another sequentially).
|
|
1968
|
+
// We keep looping until nothing new is queued and all rows finish.
|
|
1969
|
+
//
|
|
1970
|
+
// Important: an unresolved Promise by itself does not keep Node alive. The
|
|
1971
|
+
// play runner is a short-lived child process, so parking on
|
|
1972
|
+
// Promise.allSettled(promises) while there is no timer/socket/file handle can
|
|
1973
|
+
// let Node exit 0 before main() emits the result envelope. That presents as
|
|
1974
|
+
// "Local play runner produced no result" even though the real bug was a
|
|
1975
|
+
// map row waiting for queued tool/waterfall work. Always race row settlement
|
|
1976
|
+
// against a small timer and re-check the queues instead of blocking forever
|
|
1977
|
+
// on promises that may be waiting for this drain loop to do the next batch.
|
|
1978
|
+
const raceSettled = () =>
|
|
1979
|
+
Promise.race([
|
|
1980
|
+
Promise.allSettled(promises).then(() => 'done' as const),
|
|
1981
|
+
new Promise<'pending'>((r) => setTimeout(() => r('pending'), 50)),
|
|
1982
|
+
]);
|
|
1983
|
+
|
|
1984
|
+
let pass = 0;
|
|
1985
|
+
while (true) {
|
|
1986
|
+
const hasPendingWork =
|
|
1987
|
+
this.waterfallQueue.size > 0 || this.toolCallQueue.length > 0;
|
|
1988
|
+
|
|
1989
|
+
if (!hasPendingWork) {
|
|
1990
|
+
const status = await raceSettled();
|
|
1991
|
+
if (
|
|
1992
|
+
status === 'done' &&
|
|
1993
|
+
this.waterfallQueue.size === 0 &&
|
|
1994
|
+
this.toolCallQueue.length === 0
|
|
1995
|
+
) {
|
|
1996
|
+
break;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// Promises are still running, or they settled while scheduling one last
|
|
2000
|
+
// batch. Give resolver microtasks a turn, then loop back and drain any
|
|
2001
|
+
// newly queued work. This timer is deliberately tiny; it is here for
|
|
2002
|
+
// correctness/liveness, not throughput throttling.
|
|
2003
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
pass++;
|
|
2008
|
+
this.log(` Batch pass ${pass}`);
|
|
2009
|
+
this.log(
|
|
2010
|
+
` Queue depth before drain: waterfalls=${this.waterfallQueue.size} tool_calls=${this.toolCallQueue.length}`,
|
|
2011
|
+
);
|
|
2012
|
+
|
|
2013
|
+
if (this.waterfallQueue.size > 0) {
|
|
2014
|
+
await this.executeBatchedWaterfalls();
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (this.toolCallQueue.length > 0) {
|
|
2018
|
+
await this.executeBatchedToolCalls();
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private async resolveMapFieldValue<T>(
|
|
2024
|
+
resolver: MapFieldResolver<T>,
|
|
2025
|
+
row: T,
|
|
2026
|
+
fields: Record<string, unknown>,
|
|
2027
|
+
index: number,
|
|
2028
|
+
): Promise<unknown> {
|
|
2029
|
+
if (typeof resolver !== 'function') {
|
|
2030
|
+
return resolver;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
return await resolver(row, this, fields, index);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
private toOutputRow<T>(item: T): Record<string, unknown> {
|
|
2037
|
+
if (item != null && typeof item === 'object' && !Array.isArray(item)) {
|
|
2038
|
+
return item as Record<string, unknown>;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
return { value: item };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
async waterfall(
|
|
2045
|
+
toolNameOrSpec: string | InlineWaterfallSpec,
|
|
2046
|
+
input: Record<string, unknown>,
|
|
2047
|
+
opts?: WaterfallOptions,
|
|
2048
|
+
): Promise<unknown | null> {
|
|
2049
|
+
const store = rowContext.getStore();
|
|
2050
|
+
const toolName =
|
|
2051
|
+
typeof toolNameOrSpec === 'string' ? toolNameOrSpec : toolNameOrSpec.id;
|
|
2052
|
+
const baseQueueKey =
|
|
2053
|
+
typeof toolNameOrSpec === 'string'
|
|
2054
|
+
? toolNameOrSpec
|
|
2055
|
+
: `inline:${toolNameOrSpec.id}`;
|
|
2056
|
+
const inlineSpec = isInlineWaterfallSpec(toolNameOrSpec)
|
|
2057
|
+
? toolNameOrSpec
|
|
2058
|
+
: undefined;
|
|
2059
|
+
|
|
2060
|
+
if (this.pureMapExecutionActive && !store) {
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
'ctx.waterfall() cannot run inside the pure-JS fast path. Call it directly in the map definition so the batching runtime can stay enabled.',
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
if (!store) {
|
|
2067
|
+
return this.executeWaterfallDirect(toolNameOrSpec, input, opts);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
const rowId = store.rowId;
|
|
2071
|
+
const fieldName = store.fieldName;
|
|
2072
|
+
const queueKey = store.tableNamespace?.trim()
|
|
2073
|
+
? `${baseQueueKey}:${store.tableNamespace.trim()}`
|
|
2074
|
+
: baseQueueKey;
|
|
2075
|
+
const rowState = this.rowStates.get(rowId);
|
|
2076
|
+
if (rowState && !rowState.waterfalls.has(toolName)) {
|
|
2077
|
+
rowState.waterfalls.set(toolName, {
|
|
2078
|
+
status: 'pending',
|
|
2079
|
+
providerIndex: 0,
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Check if this was already resolved in a previous attempt (checkpoint)
|
|
2084
|
+
const resolved = this.waterfallReplay.getResolved(queueKey, {
|
|
2085
|
+
rowId,
|
|
2086
|
+
rowKey: store.rowKey ?? null,
|
|
2087
|
+
});
|
|
2088
|
+
if (resolved.found) {
|
|
2089
|
+
this.log(` Row ${rowId} ${toolName}: recovered from checkpoint`);
|
|
2090
|
+
return resolved.value;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return new Promise((resolve) => {
|
|
2094
|
+
const resolverId = `${rowId}-${queueKey}`;
|
|
2095
|
+
this.resolvers.set(resolverId, resolve);
|
|
2096
|
+
|
|
2097
|
+
this.emitScopedFieldMetaUpdate({
|
|
2098
|
+
rowId,
|
|
2099
|
+
key: store?.rowKey ?? null,
|
|
2100
|
+
tableNamespace: store?.tableNamespace ?? null,
|
|
2101
|
+
fieldName,
|
|
2102
|
+
status: 'running',
|
|
2103
|
+
rowStatus: 'running',
|
|
2104
|
+
stage: toolName,
|
|
2105
|
+
provider: null,
|
|
2106
|
+
error: null,
|
|
2107
|
+
dataPatch: {},
|
|
2108
|
+
});
|
|
2109
|
+
if (inlineSpec) {
|
|
2110
|
+
// Inline waterfalls have fully compiled child stages, so we can publish a
|
|
2111
|
+
// real queued state for each step immediately instead of forcing the grid
|
|
2112
|
+
// to guess from the row's broader status.
|
|
2113
|
+
this.emitQueuedInlineWaterfallSteps(
|
|
2114
|
+
rowId,
|
|
2115
|
+
store?.rowKey ?? null,
|
|
2116
|
+
store?.tableNamespace ?? null,
|
|
2117
|
+
inlineSpec,
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
if (!this.waterfallQueue.has(queueKey)) {
|
|
2122
|
+
this.waterfallQueue.set(queueKey, []);
|
|
2123
|
+
}
|
|
2124
|
+
this.waterfallQueue.get(queueKey)!.push({
|
|
2125
|
+
rowId,
|
|
2126
|
+
fieldName,
|
|
2127
|
+
tableNamespace: store?.tableNamespace,
|
|
2128
|
+
rowKey: store?.rowKey ?? null,
|
|
2129
|
+
key: queueKey,
|
|
2130
|
+
toolName,
|
|
2131
|
+
input,
|
|
2132
|
+
providerIndex: 0,
|
|
2133
|
+
opts,
|
|
2134
|
+
description: normalizeStepDescription(opts?.description),
|
|
2135
|
+
...(inlineSpec ? { spec: inlineSpec } : {}),
|
|
2136
|
+
});
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
private async executeTool(request: ToolExecutionRequest): Promise<unknown> {
|
|
2141
|
+
const { tool: toolId, input } = request;
|
|
2142
|
+
const normalizedKey = this.normalizeContextKey(request.id, 'tool');
|
|
2143
|
+
|
|
2144
|
+
const eventWaitHandler =
|
|
2145
|
+
(await this.options.getIntegrationEventWaitHandler?.(toolId)) ?? null;
|
|
2146
|
+
const store = rowContext.getStore();
|
|
2147
|
+
|
|
2148
|
+
const executeTool = async (): Promise<unknown> => {
|
|
2149
|
+
if (eventWaitHandler) {
|
|
2150
|
+
return this.executeIntegrationEventWaitTool(
|
|
2151
|
+
toolId,
|
|
2152
|
+
input,
|
|
2153
|
+
eventWaitHandler,
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
if (toolId === 'run_javascript') {
|
|
2157
|
+
throw new Error(DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE);
|
|
2158
|
+
}
|
|
2159
|
+
this.bumpCounter('toolCallCount');
|
|
2160
|
+
|
|
2161
|
+
if (this.pureMapExecutionActive && !store) {
|
|
2162
|
+
throw new Error(
|
|
2163
|
+
'Tool execution cannot run inside the pure-JS fast path. Call ctx.tools.execute({ id, tool, input }) directly in the map definition so the batching runtime can stay enabled.',
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
if (!store) {
|
|
2168
|
+
const directRowId = -(this.directToolCallIndex + 1);
|
|
2169
|
+
this.directToolCallIndex += 1;
|
|
2170
|
+
const directCacheKey = this.buildToolResultCacheKey({
|
|
2171
|
+
rowId: directRowId,
|
|
2172
|
+
});
|
|
2173
|
+
const cached = this.getCachedToolResult(toolId, directCacheKey);
|
|
2174
|
+
if (cached?.done) {
|
|
2175
|
+
this.log(`Calling tool: ${toolId} recovered from checkpoint`);
|
|
2176
|
+
return await this.wrapToolExecutionResult({
|
|
2177
|
+
toolId,
|
|
2178
|
+
status: cached.result == null ? 'no_result' : 'completed',
|
|
2179
|
+
result: cached.result,
|
|
2180
|
+
cached: true,
|
|
2181
|
+
source: 'checkpoint',
|
|
2182
|
+
cacheKey: directCacheKey,
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
this.log(`Calling tool: ${toolId}`);
|
|
2186
|
+
const execution = await this.callToolExecutionAPI(toolId, input);
|
|
2187
|
+
const wrapped = await this.wrapToolExecutionResult({
|
|
2188
|
+
toolId,
|
|
2189
|
+
status: execution.status,
|
|
2190
|
+
result: execution.result,
|
|
2191
|
+
metadata: execution.metadata,
|
|
2192
|
+
cached: false,
|
|
2193
|
+
source: 'live',
|
|
2194
|
+
cacheKey: directCacheKey,
|
|
2195
|
+
});
|
|
2196
|
+
this.checkpoint.completedToolBatches[toolId] = {
|
|
2197
|
+
...(this.checkpoint.completedToolBatches[toolId] ?? {}),
|
|
2198
|
+
[directCacheKey]: {
|
|
2199
|
+
done: true,
|
|
2200
|
+
result: wrapped,
|
|
2201
|
+
},
|
|
2202
|
+
};
|
|
2203
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2204
|
+
return wrapped;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const rowId = store.rowId;
|
|
2208
|
+
const fieldName = store.fieldName;
|
|
2209
|
+
const callId = [
|
|
2210
|
+
store.tableNamespace?.trim() || 'map',
|
|
2211
|
+
store.rowKey?.trim() || String(rowId),
|
|
2212
|
+
fieldName?.trim() || 'field',
|
|
2213
|
+
normalizedKey,
|
|
2214
|
+
].join(':');
|
|
2215
|
+
|
|
2216
|
+
const cached = this.getCachedToolResult(
|
|
2217
|
+
toolId,
|
|
2218
|
+
this.buildToolResultCacheKey({
|
|
2219
|
+
rowId,
|
|
2220
|
+
tableNamespace: store.tableNamespace,
|
|
2221
|
+
rowKey: store.rowKey,
|
|
2222
|
+
callId,
|
|
2223
|
+
}),
|
|
2224
|
+
);
|
|
2225
|
+
if (cached?.done) {
|
|
2226
|
+
this.log(` Row ${rowId} ${toolId}: recovered from checkpoint`);
|
|
2227
|
+
return await this.wrapToolExecutionResult({
|
|
2228
|
+
toolId,
|
|
2229
|
+
status: cached.result == null ? 'no_result' : 'completed',
|
|
2230
|
+
result: cached.result,
|
|
2231
|
+
cached: true,
|
|
2232
|
+
source: 'checkpoint',
|
|
2233
|
+
cacheKey: this.buildToolResultCacheKey({
|
|
2234
|
+
rowId,
|
|
2235
|
+
tableNamespace: store.tableNamespace,
|
|
2236
|
+
rowKey: store.rowKey,
|
|
2237
|
+
callId,
|
|
2238
|
+
}),
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
return new Promise((resolve) => {
|
|
2243
|
+
const existingResolvers = this.toolCallResolvers.get(callId);
|
|
2244
|
+
if (existingResolvers) {
|
|
2245
|
+
existingResolvers.push(resolve);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
this.toolCallResolvers.set(callId, [resolve]);
|
|
2249
|
+
this.emitScopedFieldMetaUpdate({
|
|
2250
|
+
rowId,
|
|
2251
|
+
key: store.rowKey ?? null,
|
|
2252
|
+
tableNamespace: store.tableNamespace ?? null,
|
|
2253
|
+
fieldName,
|
|
2254
|
+
status: 'running',
|
|
2255
|
+
rowStatus: 'running',
|
|
2256
|
+
stage: toolId,
|
|
2257
|
+
provider: null,
|
|
2258
|
+
error: null,
|
|
2259
|
+
dataPatch: {},
|
|
2260
|
+
});
|
|
2261
|
+
this.toolCallQueue.push({
|
|
2262
|
+
callId,
|
|
2263
|
+
rowId,
|
|
2264
|
+
fieldName,
|
|
2265
|
+
toolId,
|
|
2266
|
+
input,
|
|
2267
|
+
tableNamespace: store.tableNamespace,
|
|
2268
|
+
rowKey: store.rowKey ?? null,
|
|
2269
|
+
description: normalizeStepDescription(request.description),
|
|
2270
|
+
});
|
|
2271
|
+
});
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
if (store) {
|
|
2275
|
+
return await executeTool();
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
return this.executeWithRuntimeReceipt(
|
|
2279
|
+
'tool',
|
|
2280
|
+
normalizedKey,
|
|
2281
|
+
this.currentRunId,
|
|
2282
|
+
{
|
|
2283
|
+
markSkipped: () => {
|
|
2284
|
+
this.log(
|
|
2285
|
+
`ctx.tools.execute({ id: ${normalizedKey}, tool: ${toolId} }): no-op due completed receipt`,
|
|
2286
|
+
);
|
|
2287
|
+
},
|
|
2288
|
+
execute: executeTool,
|
|
2289
|
+
},
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
private async executeIntegrationEventWaitTool(
|
|
2294
|
+
toolId: string,
|
|
2295
|
+
input: Record<string, unknown>,
|
|
2296
|
+
handler: IntegrationEventWaitHandler,
|
|
2297
|
+
): Promise<unknown> {
|
|
2298
|
+
if (!this.options.durableBoundaries) {
|
|
2299
|
+
throw new Error(`${toolId} requires durable play boundaries.`);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
const rowScope = rowContext.getStore();
|
|
2303
|
+
const eventContext = {
|
|
2304
|
+
playId: this.options.playId,
|
|
2305
|
+
runId: this.options.runId,
|
|
2306
|
+
workflowId: this.options.workflowId,
|
|
2307
|
+
orgId: this.options.orgId,
|
|
2308
|
+
executorToken: this.options.executorToken,
|
|
2309
|
+
};
|
|
2310
|
+
const basePreparedBoundary = await handler.prepare({
|
|
2311
|
+
payload: input,
|
|
2312
|
+
context: eventContext,
|
|
2313
|
+
});
|
|
2314
|
+
const preparedBoundary = rowScope
|
|
2315
|
+
? {
|
|
2316
|
+
...basePreparedBoundary,
|
|
2317
|
+
boundaryId: `map-row-${stableFetchHash(
|
|
2318
|
+
JSON.stringify({
|
|
2319
|
+
tableNamespace: rowScope.tableNamespace,
|
|
2320
|
+
rowKey: rowScope.rowKey,
|
|
2321
|
+
fieldName: rowScope.fieldName,
|
|
2322
|
+
boundaryId: basePreparedBoundary.boundaryId,
|
|
2323
|
+
eventKey: basePreparedBoundary.eventKey,
|
|
2324
|
+
}),
|
|
2325
|
+
)}`,
|
|
2326
|
+
}
|
|
2327
|
+
: basePreparedBoundary;
|
|
2328
|
+
|
|
2329
|
+
const existing =
|
|
2330
|
+
this.checkpoint.resolvedBoundaries?.[preparedBoundary.boundaryId];
|
|
2331
|
+
if (existing?.kind === 'integration_event' && 'output' in existing) {
|
|
2332
|
+
this.log(
|
|
2333
|
+
`Integration event ${preparedBoundary.boundaryId}: recovered response from checkpoint`,
|
|
2334
|
+
);
|
|
2335
|
+
return existing.output;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const boundary = await handler.arm({
|
|
2339
|
+
payload: input,
|
|
2340
|
+
context: eventContext,
|
|
2341
|
+
boundary: preparedBoundary,
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
this.log(
|
|
2345
|
+
`Armed ${handler.provider} integration event wait: ${boundary.eventKey}`,
|
|
2346
|
+
);
|
|
2347
|
+
this.checkpoint.resolvedBoundaries = {
|
|
2348
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
2349
|
+
[boundary.boundaryId]: {
|
|
2350
|
+
kind: 'integration_event',
|
|
2351
|
+
eventKey: boundary.eventKey,
|
|
2352
|
+
timeoutMs: boundary.timeoutMs,
|
|
2353
|
+
provider: boundary.provider,
|
|
2354
|
+
toolId: boundary.toolId,
|
|
2355
|
+
...(rowScope
|
|
2356
|
+
? {
|
|
2357
|
+
scope: {
|
|
2358
|
+
type: 'map_row' as const,
|
|
2359
|
+
tableNamespace: rowScope.tableNamespace,
|
|
2360
|
+
rowKey: rowScope.rowKey,
|
|
2361
|
+
rowIndex: rowScope.rowId,
|
|
2362
|
+
fieldName: rowScope.fieldName,
|
|
2363
|
+
},
|
|
2364
|
+
}
|
|
2365
|
+
: { scope: { type: 'workflow' as const } }),
|
|
2366
|
+
...(boundary.messageRef ? { messageRef: boundary.messageRef } : {}),
|
|
2367
|
+
},
|
|
2368
|
+
};
|
|
2369
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2370
|
+
|
|
2371
|
+
if (rowScope) {
|
|
2372
|
+
throw new PlayRowExecutionSuspendedError({
|
|
2373
|
+
boundaryId: boundary.boundaryId,
|
|
2374
|
+
eventKey: boundary.eventKey,
|
|
2375
|
+
timeoutMs: boundary.timeoutMs,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
throw new PlayExecutionSuspendedError({
|
|
2380
|
+
kind: 'integration_event',
|
|
2381
|
+
boundaryId: boundary.boundaryId,
|
|
2382
|
+
eventKey: boundary.eventKey,
|
|
2383
|
+
timeoutMs: boundary.timeoutMs,
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
async runPlay(
|
|
2388
|
+
key: string,
|
|
2389
|
+
playRef: string | { playName?: string; name?: string },
|
|
2390
|
+
input: Record<string, unknown>,
|
|
2391
|
+
options?: PlayCallOptions,
|
|
2392
|
+
): Promise<unknown> {
|
|
2393
|
+
const normalizedKey = this.normalizeContextKey(key, 'runPlay');
|
|
2394
|
+
const resolvedName =
|
|
2395
|
+
typeof playRef === 'string'
|
|
2396
|
+
? playRef
|
|
2397
|
+
: typeof playRef.playName === 'string'
|
|
2398
|
+
? playRef.playName
|
|
2399
|
+
: (playRef.name ?? '');
|
|
2400
|
+
|
|
2401
|
+
if (!resolvedName.trim()) {
|
|
2402
|
+
throw new Error('ctx.runPlay(...) requires a resolvable play name.');
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (!this.options.resolvePlay) {
|
|
2406
|
+
throw new Error(
|
|
2407
|
+
'ctx.runPlay(...) is unavailable because no play resolver was configured.',
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (this.governance.ancestryPlayIds.includes(resolvedName)) {
|
|
2412
|
+
const chain = [...this.governance.ancestryPlayIds, resolvedName].join(
|
|
2413
|
+
' -> ',
|
|
2414
|
+
);
|
|
2415
|
+
throw new Error(`Recursive play graph detected: ${chain}`);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const nextDepth = this.governance.callDepth + 1;
|
|
2419
|
+
if (nextDepth > this.governance.limits.maxPlayCallDepth) {
|
|
2420
|
+
throw new Error(
|
|
2421
|
+
`Play-call depth exceeded (${nextDepth}/${this.governance.limits.maxPlayCallDepth}) while calling ${resolvedName}.`,
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const parentKey = this.governance.currentPlayId;
|
|
2426
|
+
const nextParentCalls =
|
|
2427
|
+
(this.governance.parentChildCalls[parentKey] ?? 0) + 1;
|
|
2428
|
+
if (nextParentCalls > this.governance.limits.maxChildPlayCallsPerParent) {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`Child play-call cap exceeded for ${parentKey} (${nextParentCalls}/${this.governance.limits.maxChildPlayCallsPerParent}).`,
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const executePlayCall = async (): Promise<unknown> => {
|
|
2435
|
+
this.bumpCounter('playCallCount');
|
|
2436
|
+
this.bumpCounter('descendantCount');
|
|
2437
|
+
this.governance.parentChildCalls[parentKey] = nextParentCalls;
|
|
2438
|
+
this.acquirePlayConcurrency(resolvedName);
|
|
2439
|
+
|
|
2440
|
+
try {
|
|
2441
|
+
const resolvePlay = this.options.resolvePlay;
|
|
2442
|
+
if (!resolvePlay) {
|
|
2443
|
+
throw new Error(
|
|
2444
|
+
'ctx.runPlay(...) is unavailable because no play resolver was configured.',
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
const resolvedPlay = await resolvePlay(resolvedName);
|
|
2448
|
+
if (!resolvedPlay) {
|
|
2449
|
+
throw new Error(
|
|
2450
|
+
`Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
const childGovernance: PlayExecutionGovernanceState = {
|
|
2455
|
+
...this.governance,
|
|
2456
|
+
currentPlayId: resolvedName,
|
|
2457
|
+
currentRunId: `${this.governance.rootRunId}:${resolvedName}:${this.governance.playCallCount}`,
|
|
2458
|
+
ancestryPlayIds: [...this.governance.ancestryPlayIds, resolvedName],
|
|
2459
|
+
ancestryRunIds: [
|
|
2460
|
+
...this.governance.ancestryRunIds,
|
|
2461
|
+
`${this.governance.rootRunId}:${resolvedName}:${this.governance.playCallCount}`,
|
|
2462
|
+
],
|
|
2463
|
+
callDepth: nextDepth,
|
|
2464
|
+
};
|
|
2465
|
+
const childContext = createPlayContext({
|
|
2466
|
+
...this.options,
|
|
2467
|
+
playId: resolvedName,
|
|
2468
|
+
playName: resolvedName,
|
|
2469
|
+
runId: childGovernance.currentRunId,
|
|
2470
|
+
staticPipeline: resolvedPlay.staticPipeline ?? null,
|
|
2471
|
+
checkpoint: this.checkpoint,
|
|
2472
|
+
governance: childGovernance,
|
|
2473
|
+
getRuntimeStepReceipt: undefined,
|
|
2474
|
+
claimRuntimeStepReceipt: undefined,
|
|
2475
|
+
completeRuntimeStepReceipt: undefined,
|
|
2476
|
+
failRuntimeStepReceipt: undefined,
|
|
2477
|
+
skipRuntimeStepReceipt: undefined,
|
|
2478
|
+
});
|
|
2479
|
+
const childExecution = this.executeResolvedPlay(
|
|
2480
|
+
resolvedPlay,
|
|
2481
|
+
childContext,
|
|
2482
|
+
input,
|
|
2483
|
+
);
|
|
2484
|
+
await childContext.drainQueuedWork([childExecution]);
|
|
2485
|
+
const result = await childExecution;
|
|
2486
|
+
const step = {
|
|
2487
|
+
type: 'play_call' as const,
|
|
2488
|
+
playId: resolvedName,
|
|
2489
|
+
nestedSteps: childContext.getSteps(),
|
|
2490
|
+
description: normalizeStepDescription(options?.description),
|
|
2491
|
+
};
|
|
2492
|
+
if (this.activeMapStep) {
|
|
2493
|
+
this.activeMapStep.substeps.push(step);
|
|
2494
|
+
} else {
|
|
2495
|
+
this.steps.push(step);
|
|
2496
|
+
}
|
|
2497
|
+
return result;
|
|
2498
|
+
} finally {
|
|
2499
|
+
this.releasePlayConcurrency(resolvedName);
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
if (rowContext.getStore()) {
|
|
2504
|
+
return await executePlayCall();
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
return this.executeWithRuntimeReceipt<unknown>(
|
|
2508
|
+
'runPlay',
|
|
2509
|
+
normalizedKey,
|
|
2510
|
+
this.currentRunId,
|
|
2511
|
+
{
|
|
2512
|
+
markSkipped: () => {
|
|
2513
|
+
this.log(
|
|
2514
|
+
`ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
|
|
2515
|
+
);
|
|
2516
|
+
},
|
|
2517
|
+
execute: executePlayCall,
|
|
2518
|
+
},
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Extract a list from a tool result.
|
|
2524
|
+
* e.g. ctx.extractList(result, 'people', ['first_name', 'last_name', 'email'])
|
|
2525
|
+
*/
|
|
2526
|
+
extractList(
|
|
2527
|
+
result: unknown,
|
|
2528
|
+
listPath: string,
|
|
2529
|
+
fields?: string[],
|
|
2530
|
+
): Record<string, unknown>[] {
|
|
2531
|
+
if (result == null || typeof result !== 'object') return [];
|
|
2532
|
+
|
|
2533
|
+
let list: unknown = result;
|
|
2534
|
+
for (const key of listPath.split('.')) {
|
|
2535
|
+
if (list == null || typeof list !== 'object') return [];
|
|
2536
|
+
list = (list as Record<string, unknown>)[key];
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
if (!Array.isArray(list)) return [];
|
|
2540
|
+
|
|
2541
|
+
if (!fields || fields.length === 0) {
|
|
2542
|
+
return list.filter(
|
|
2543
|
+
(item): item is Record<string, unknown> =>
|
|
2544
|
+
item != null && typeof item === 'object',
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
return list
|
|
2549
|
+
.filter(
|
|
2550
|
+
(item): item is Record<string, unknown> =>
|
|
2551
|
+
item != null && typeof item === 'object',
|
|
2552
|
+
)
|
|
2553
|
+
.map((item) => {
|
|
2554
|
+
const picked: Record<string, unknown> = {};
|
|
2555
|
+
for (const field of fields) {
|
|
2556
|
+
if (field in item) picked[field] = item[field];
|
|
2557
|
+
}
|
|
2558
|
+
return picked;
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
log(msg: string): void {
|
|
2563
|
+
const line = `[${new Date().toISOString()}] ${msg}`;
|
|
2564
|
+
this.logBuffer.push(line);
|
|
2565
|
+
this.options.onLog?.(line);
|
|
2566
|
+
if (this.options.verbose) console.log(line);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
async sleep(ms: number): Promise<void> {
|
|
2570
|
+
if (this.options.durableBoundaries) {
|
|
2571
|
+
const delayMs = Math.max(0, Math.round(ms));
|
|
2572
|
+
const boundaryId = this.durableBoundaryId(
|
|
2573
|
+
`sleep-${this.sleepBoundaryIndex}-${delayMs}`,
|
|
2574
|
+
);
|
|
2575
|
+
this.sleepBoundaryIndex += 1;
|
|
2576
|
+
const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
|
|
2577
|
+
if (existing?.kind === 'sleep' && existing.completedAt !== undefined) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
this.checkpoint.resolvedBoundaries = {
|
|
2581
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
2582
|
+
[boundaryId]: {
|
|
2583
|
+
kind: 'sleep',
|
|
2584
|
+
delayMs,
|
|
2585
|
+
},
|
|
2586
|
+
};
|
|
2587
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2588
|
+
throw new PlayExecutionSuspendedError({
|
|
2589
|
+
kind: 'sleep',
|
|
2590
|
+
boundaryId,
|
|
2591
|
+
delayMs,
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
async fetch(
|
|
2598
|
+
key: string,
|
|
2599
|
+
input: string | URL,
|
|
2600
|
+
init: RequestInit = {},
|
|
2601
|
+
): Promise<PlayFetchResponse> {
|
|
2602
|
+
const normalizedKey = this.normalizeContextKey(key, 'fetch');
|
|
2603
|
+
if (rowContext.getStore()) {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
'ctx.fetch() must run outside ctx.map(); use ctx.tools.execute({ id, tool, input }) for row-level external requests so Deepline can batch and checkpoint them.',
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
return this.executeWithRuntimeReceipt<PlayFetchResponse>(
|
|
2610
|
+
'fetch',
|
|
2611
|
+
normalizedKey,
|
|
2612
|
+
this.currentRunId,
|
|
2613
|
+
{
|
|
2614
|
+
execute: async () => {
|
|
2615
|
+
const url = input.toString();
|
|
2616
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
2617
|
+
const headers = normalizeFetchHeaders(init.headers);
|
|
2618
|
+
const boundaryId = this.durableBoundaryId(
|
|
2619
|
+
`fetch-${this.fetchCallIndex}-${stableFetchHash(
|
|
2620
|
+
JSON.stringify({
|
|
2621
|
+
url,
|
|
2622
|
+
method,
|
|
2623
|
+
headers,
|
|
2624
|
+
body: typeof init.body === 'string' ? init.body : null,
|
|
2625
|
+
}),
|
|
2626
|
+
)}`,
|
|
2627
|
+
);
|
|
2628
|
+
this.fetchCallIndex += 1;
|
|
2629
|
+
|
|
2630
|
+
const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
|
|
2631
|
+
if (existing?.kind === 'fetch' && 'output' in existing) {
|
|
2632
|
+
this.log(`ctx.fetch(${url}): recovered response from checkpoint`);
|
|
2633
|
+
return existing.output as PlayFetchResponse;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
2637
|
+
const hasIdempotencyKey =
|
|
2638
|
+
headers['idempotency-key'] !== undefined ||
|
|
2639
|
+
headers['x-idempotency-key'] !== undefined;
|
|
2640
|
+
if (!hasIdempotencyKey) {
|
|
2641
|
+
throw new Error(
|
|
2642
|
+
`ctx.fetch(${method} ${url}) needs an Idempotency-Key header. Durable plays can replay after waits/retries; add an idempotency key or wrap the side effect in a Deepline integration tool.`,
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
const response = await fetch(url, init);
|
|
2648
|
+
const bodyText = await response.text();
|
|
2649
|
+
const output: PlayFetchResponse = {
|
|
2650
|
+
ok: response.ok,
|
|
2651
|
+
status: response.status,
|
|
2652
|
+
statusText: response.statusText,
|
|
2653
|
+
url: response.url,
|
|
2654
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
2655
|
+
bodyText,
|
|
2656
|
+
json: parseJsonOrNull(bodyText),
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
this.checkpoint.resolvedBoundaries = {
|
|
2660
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
2661
|
+
[boundaryId]: {
|
|
2662
|
+
kind: 'fetch',
|
|
2663
|
+
url,
|
|
2664
|
+
method,
|
|
2665
|
+
output,
|
|
2666
|
+
completedAt: Date.now(),
|
|
2667
|
+
},
|
|
2668
|
+
};
|
|
2669
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2670
|
+
return output;
|
|
2671
|
+
},
|
|
2672
|
+
markSkipped: (output) => {
|
|
2673
|
+
this.log(
|
|
2674
|
+
`ctx.fetch(${output?.url ?? ''}): no-op due completed receipt`,
|
|
2675
|
+
);
|
|
2676
|
+
},
|
|
2677
|
+
},
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
async step<T>(key: string, run: () => T | Promise<T>): Promise<T> {
|
|
2682
|
+
const normalizedKey = this.normalizeContextKey(key, 'step');
|
|
2683
|
+
if (!normalizedKey.trim()) {
|
|
2684
|
+
throw new Error('ctx.step(key, fn) requires a non-empty stable step id.');
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const rowStore = rowContext.getStore();
|
|
2688
|
+
const scope = rowStore
|
|
2689
|
+
? `row-${rowStore.tableNamespace?.trim() || 'map'}:${rowStore.rowKey?.trim() || String(rowStore.rowId)}`
|
|
2690
|
+
: 'workflow';
|
|
2691
|
+
const callIndexKey = `${scope}:${normalizedKey}`;
|
|
2692
|
+
const callIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
|
|
2693
|
+
this.stepCallIndexByKey.set(callIndexKey, callIndex + 1);
|
|
2694
|
+
const boundarySuffix = callIndex === 0 ? '' : `:${callIndex}`;
|
|
2695
|
+
const boundaryId = this.durableBoundaryId(
|
|
2696
|
+
`step-${scope}:${normalizedKey}${boundarySuffix}`,
|
|
2697
|
+
);
|
|
2698
|
+
const executeStep = async (): Promise<T> => {
|
|
2699
|
+
const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
|
|
2700
|
+
if (existing?.kind === 'step' && 'output' in existing) {
|
|
2701
|
+
this.log(
|
|
2702
|
+
`ctx.step(${normalizedKey}): recovered result from checkpoint`,
|
|
2703
|
+
);
|
|
2704
|
+
return existing.output as T;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const output = await run();
|
|
2708
|
+
assertJsonSerializableStepOutput(normalizedKey, output);
|
|
2709
|
+
this.checkpoint.resolvedBoundaries = {
|
|
2710
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
2711
|
+
[boundaryId]: {
|
|
2712
|
+
kind: 'step',
|
|
2713
|
+
stepId: normalizedKey,
|
|
2714
|
+
output,
|
|
2715
|
+
completedAt: Date.now(),
|
|
2716
|
+
},
|
|
2717
|
+
};
|
|
2718
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2719
|
+
return output;
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
if (rowStore) {
|
|
2723
|
+
return await executeStep();
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
return this.executeWithRuntimeReceipt<T>(
|
|
2727
|
+
'step',
|
|
2728
|
+
normalizedKey,
|
|
2729
|
+
this.currentRunId,
|
|
2730
|
+
{
|
|
2731
|
+
markSkipped: (output) => {
|
|
2732
|
+
this.log(`ctx.step(${normalizedKey}): no-op due completed receipt`);
|
|
2733
|
+
assertJsonSerializableStepOutput(normalizedKey, output);
|
|
2734
|
+
},
|
|
2735
|
+
execute: executeStep,
|
|
2736
|
+
},
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
getLogs(): string[] {
|
|
2741
|
+
return this.logBuffer;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
getCheckpoint(): PlayCheckpoint {
|
|
2745
|
+
return this.checkpoint;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
getSteps(): PlayStep[] {
|
|
2749
|
+
return this.steps;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
recordStep(step: PlayStep): void {
|
|
2753
|
+
const isSubstepType =
|
|
2754
|
+
step.type === 'waterfall' ||
|
|
2755
|
+
step.type === 'tool' ||
|
|
2756
|
+
step.type === 'run_javascript';
|
|
2757
|
+
const targetMap = this.activeMapStep ?? this.lastMapStep;
|
|
2758
|
+
if (targetMap && isSubstepType) {
|
|
2759
|
+
targetMap.substeps.push(step);
|
|
2760
|
+
} else {
|
|
2761
|
+
this.steps.push(step);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
recordReturn(outputRows: number): void {
|
|
2766
|
+
this.lastMapStep = null; // No more substeps expected
|
|
2767
|
+
this.steps.push({ type: 'return', outputRows });
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
getStats(): Record<string, unknown> {
|
|
2771
|
+
const stats: Record<
|
|
2772
|
+
string,
|
|
2773
|
+
{ total: number; complete: number; failed: number }
|
|
2774
|
+
> = {};
|
|
2775
|
+
for (const [, rowState] of this.rowStates) {
|
|
2776
|
+
for (const [toolName, wState] of rowState.waterfalls) {
|
|
2777
|
+
if (!stats[toolName])
|
|
2778
|
+
stats[toolName] = { total: 0, complete: 0, failed: 0 };
|
|
2779
|
+
stats[toolName].total++;
|
|
2780
|
+
if (wState.status === 'complete') stats[toolName].complete++;
|
|
2781
|
+
if (wState.status === 'failed') stats[toolName].failed++;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
rowsProcessed: Math.max(this.rowStates.size, this.processedRowCount),
|
|
2786
|
+
waterfalls: stats,
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// ——— Batched waterfall execution (the core engine) ———
|
|
2791
|
+
|
|
2792
|
+
private async executeBatchedWaterfalls(): Promise<void> {
|
|
2793
|
+
const queuedWaterfalls = this.waterfallQueue;
|
|
2794
|
+
this.waterfallQueue = new Map();
|
|
2795
|
+
this.log(`Executing batched waterfalls for ${queuedWaterfalls.size} tools`);
|
|
2796
|
+
|
|
2797
|
+
for (const [queueKey, requests] of queuedWaterfalls) {
|
|
2798
|
+
const inlineSpec = requests[0]?.spec;
|
|
2799
|
+
if (inlineSpec) {
|
|
2800
|
+
await this.executeInlineWaterfall(queueKey, inlineSpec, requests);
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
const toolName = requests[0]?.toolName ?? queueKey;
|
|
2805
|
+
const providers = requests[0]?.opts?.providers ?? [
|
|
2806
|
+
'hunter',
|
|
2807
|
+
'leadmagic',
|
|
2808
|
+
'pdl',
|
|
2809
|
+
'dropcontact',
|
|
2810
|
+
'prospeo',
|
|
2811
|
+
];
|
|
2812
|
+
|
|
2813
|
+
this.log(
|
|
2814
|
+
`Processing waterfall ${toolName}: ${requests.length} rows, providers: ${providers.join(', ')}`,
|
|
2815
|
+
);
|
|
2816
|
+
|
|
2817
|
+
if (!this.checkpoint.resolvedWaterfalls[queueKey]) {
|
|
2818
|
+
this.checkpoint.resolvedWaterfalls[queueKey] = {};
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
await executeWaterfallProviders<WaterfallRequest, unknown>({
|
|
2822
|
+
providers,
|
|
2823
|
+
getPendingRequests: () =>
|
|
2824
|
+
requests.filter((req) => {
|
|
2825
|
+
const wState = this.rowStates
|
|
2826
|
+
.get(req.rowId)
|
|
2827
|
+
?.waterfalls.get(toolName);
|
|
2828
|
+
return wState?.status === 'pending';
|
|
2829
|
+
}),
|
|
2830
|
+
getCachedResults: (provider) => {
|
|
2831
|
+
const batchKey = `${queueKey}:${provider}`;
|
|
2832
|
+
const recovered = this.waterfallReplay.readProviderBatch({
|
|
2833
|
+
batchKey,
|
|
2834
|
+
requests,
|
|
2835
|
+
});
|
|
2836
|
+
if (recovered) {
|
|
2837
|
+
this.log(` ${provider}: skipping (recovered from checkpoint)`);
|
|
2838
|
+
}
|
|
2839
|
+
return recovered;
|
|
2840
|
+
},
|
|
2841
|
+
storeCachedResults: (provider, results) => {
|
|
2842
|
+
const batchKey = `${queueKey}:${provider}`;
|
|
2843
|
+
this.waterfallReplay.writeProviderBatch(batchKey, results);
|
|
2844
|
+
},
|
|
2845
|
+
executeProviderRequests: async (provider, pending) => {
|
|
2846
|
+
const providerToolId = resolveWaterfallToolId(provider, toolName);
|
|
2847
|
+
const strategy =
|
|
2848
|
+
this.options.getBatchOperationStrategy?.(providerToolId) ?? null;
|
|
2849
|
+
this.log(` ${provider}: ${pending.length} pending rows`);
|
|
2850
|
+
this.bumpCounter('retryCount', pending.length);
|
|
2851
|
+
|
|
2852
|
+
if (strategy) {
|
|
2853
|
+
const compiledBatches = compileRequestsWithStrategy({
|
|
2854
|
+
requests: pending,
|
|
2855
|
+
strategy,
|
|
2856
|
+
getPayload: (request: WaterfallRequest) => request.input,
|
|
2857
|
+
});
|
|
2858
|
+
const flattenedResults: Array<{
|
|
2859
|
+
request: WaterfallRequest;
|
|
2860
|
+
result: unknown | null;
|
|
2861
|
+
}> = [];
|
|
2862
|
+
|
|
2863
|
+
await executeChunkedRequests({
|
|
2864
|
+
requests: compiledBatches,
|
|
2865
|
+
batchSize:
|
|
2866
|
+
compiledBatches.length > 0
|
|
2867
|
+
? await this.rateLimitScheduler.getSuggestedParallelism(
|
|
2868
|
+
compiledBatches[0]!.batchOperation,
|
|
2869
|
+
4,
|
|
2870
|
+
)
|
|
2871
|
+
: 4,
|
|
2872
|
+
execute: async (batch) =>
|
|
2873
|
+
await this.callToolAPI(
|
|
2874
|
+
batch.batchOperation,
|
|
2875
|
+
batch.batchPayload,
|
|
2876
|
+
),
|
|
2877
|
+
onChunkComplete: async (chunkResults) => {
|
|
2878
|
+
for (const entry of chunkResults) {
|
|
2879
|
+
const splitResults =
|
|
2880
|
+
entry.result != null
|
|
2881
|
+
? entry.request.splitResults(entry.result)
|
|
2882
|
+
: entry.request.memberRequests.map(() => null);
|
|
2883
|
+
|
|
2884
|
+
for (
|
|
2885
|
+
let index = 0;
|
|
2886
|
+
index < entry.request.memberRequests.length;
|
|
2887
|
+
index += 1
|
|
2888
|
+
) {
|
|
2889
|
+
flattenedResults.push({
|
|
2890
|
+
request: entry.request.memberRequests[index]!,
|
|
2891
|
+
result: splitResults[index] ?? null,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2896
|
+
},
|
|
2897
|
+
});
|
|
2898
|
+
|
|
2899
|
+
return flattenedResults;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
const chunkResults: Array<{
|
|
2903
|
+
request: WaterfallRequest;
|
|
2904
|
+
result: unknown | null;
|
|
2905
|
+
}> = [];
|
|
2906
|
+
await executeChunkedRequests<WaterfallRequest, unknown>({
|
|
2907
|
+
requests: pending,
|
|
2908
|
+
batchSize: await this.rateLimitScheduler.getSuggestedParallelism(
|
|
2909
|
+
providerToolId,
|
|
2910
|
+
50,
|
|
2911
|
+
),
|
|
2912
|
+
execute: async (request) =>
|
|
2913
|
+
await this.callToolAPI(providerToolId, request.input),
|
|
2914
|
+
onChunkComplete: async (results) => {
|
|
2915
|
+
chunkResults.push(...results);
|
|
2916
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2917
|
+
},
|
|
2918
|
+
});
|
|
2919
|
+
return chunkResults;
|
|
2920
|
+
},
|
|
2921
|
+
onHit: (provider, request, result) => {
|
|
2922
|
+
this.resolveWaterfall(
|
|
2923
|
+
queueKey,
|
|
2924
|
+
toolName,
|
|
2925
|
+
request.rowId,
|
|
2926
|
+
result,
|
|
2927
|
+
provider,
|
|
2928
|
+
request.rowKey ?? null,
|
|
2929
|
+
request.tableNamespace ?? null,
|
|
2930
|
+
request.fieldName,
|
|
2931
|
+
);
|
|
2932
|
+
},
|
|
2933
|
+
onMiss: (_provider, request) => {
|
|
2934
|
+
const wState = this.rowStates
|
|
2935
|
+
.get(request.rowId)
|
|
2936
|
+
?.waterfalls.get(toolName);
|
|
2937
|
+
if (wState) wState.providerIndex++;
|
|
2938
|
+
this.log(` Row ${request.rowId}: miss`);
|
|
2939
|
+
},
|
|
2940
|
+
onProviderComplete: () => {
|
|
2941
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
2942
|
+
},
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2945
|
+
const stepResults: PlayStepRowResult[] = requests.map((req) => {
|
|
2946
|
+
const wState = this.rowStates.get(req.rowId)?.waterfalls.get(toolName);
|
|
2947
|
+
const success = wState?.status === 'complete';
|
|
2948
|
+
return {
|
|
2949
|
+
rowId: req.rowId,
|
|
2950
|
+
status: success ? 'completed' : 'missed',
|
|
2951
|
+
success,
|
|
2952
|
+
value: wState?.result,
|
|
2953
|
+
error: success ? null : (wState?.error ?? null),
|
|
2954
|
+
};
|
|
2955
|
+
});
|
|
2956
|
+
const waterfallStep = {
|
|
2957
|
+
type: 'waterfall' as const,
|
|
2958
|
+
tool: toolName,
|
|
2959
|
+
providers,
|
|
2960
|
+
results: stepResults,
|
|
2961
|
+
description: normalizeStepDescription(requests[0]?.description),
|
|
2962
|
+
};
|
|
2963
|
+
if (this.activeMapStep) {
|
|
2964
|
+
this.activeMapStep.substeps.push(waterfallStep);
|
|
2965
|
+
} else {
|
|
2966
|
+
this.steps.push(waterfallStep);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
for (const req of requests) {
|
|
2970
|
+
const wState = this.rowStates.get(req.rowId)?.waterfalls.get(toolName);
|
|
2971
|
+
if (wState?.status === 'pending') {
|
|
2972
|
+
wState.status = 'failed';
|
|
2973
|
+
wState.error = 'All providers exhausted';
|
|
2974
|
+
this.waterfallReplay.setResolved(queueKey, req, null);
|
|
2975
|
+
|
|
2976
|
+
const resolver = this.resolvers.get(`${req.rowId}-${queueKey}`);
|
|
2977
|
+
if (resolver) {
|
|
2978
|
+
resolver(null);
|
|
2979
|
+
this.resolvers.delete(`${req.rowId}-${queueKey}`);
|
|
2980
|
+
}
|
|
2981
|
+
this.emitScopedFieldMetaUpdate({
|
|
2982
|
+
rowId: req.rowId,
|
|
2983
|
+
key: req.rowKey ?? null,
|
|
2984
|
+
tableNamespace: req.tableNamespace ?? null,
|
|
2985
|
+
fieldName: req.fieldName,
|
|
2986
|
+
status: 'failed',
|
|
2987
|
+
rowStatus: 'running',
|
|
2988
|
+
stage: toolName,
|
|
2989
|
+
provider: null,
|
|
2990
|
+
error: 'All providers exhausted',
|
|
2991
|
+
dataPatch: {},
|
|
2992
|
+
});
|
|
2993
|
+
this.log(` Row ${req.rowId}: all providers exhausted`);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
private async executeInlineWaterfall(
|
|
3000
|
+
queueKey: string,
|
|
3001
|
+
spec: InlineWaterfallSpec,
|
|
3002
|
+
requests: WaterfallRequest[],
|
|
3003
|
+
): Promise<void> {
|
|
3004
|
+
if (!this.checkpoint.resolvedWaterfalls[queueKey]) {
|
|
3005
|
+
this.checkpoint.resolvedWaterfalls[queueKey] = {};
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
const pendingRows = new Set<number>(
|
|
3009
|
+
requests
|
|
3010
|
+
.filter(
|
|
3011
|
+
(req) =>
|
|
3012
|
+
this.rowStates.get(req.rowId)?.waterfalls.get(spec.id)?.status ===
|
|
3013
|
+
'pending',
|
|
3014
|
+
)
|
|
3015
|
+
.map((req) => req.rowId),
|
|
3016
|
+
);
|
|
3017
|
+
const resultsByRow = new Map<number, unknown[]>();
|
|
3018
|
+
const stepResults: Array<{
|
|
3019
|
+
id: string;
|
|
3020
|
+
kind?: 'tool' | 'code';
|
|
3021
|
+
toolId?: string;
|
|
3022
|
+
results: PlayStepRowResult[];
|
|
3023
|
+
}> = [];
|
|
3024
|
+
const stepColumnNames = spec.steps.map((s) =>
|
|
3025
|
+
sqlSafePlayColumnName(`${spec.id}.${s.id}`),
|
|
3026
|
+
);
|
|
3027
|
+
const resolvedInChunkRowIds = new Set<number>();
|
|
3028
|
+
let stepIdx = 0;
|
|
3029
|
+
|
|
3030
|
+
for (const step of spec.steps) {
|
|
3031
|
+
const stepColumnName = sqlSafePlayColumnName(`${spec.id}.${step.id}`);
|
|
3032
|
+
const stepProvider = isInlineWaterfallToolStep(step)
|
|
3033
|
+
? step.toolId
|
|
3034
|
+
: 'code';
|
|
3035
|
+
if (pendingRows.size === 0) {
|
|
3036
|
+
const skippedResults: PlayStepRowResult[] = requests.map((request) => {
|
|
3037
|
+
this.emitCellUpdate({
|
|
3038
|
+
rowId: request.rowId,
|
|
3039
|
+
key: request.rowKey ?? null,
|
|
3040
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3041
|
+
columnName: stepColumnName,
|
|
3042
|
+
status: 'skipped',
|
|
3043
|
+
stage: step.id,
|
|
3044
|
+
provider: stepProvider,
|
|
3045
|
+
value: null,
|
|
3046
|
+
});
|
|
3047
|
+
return {
|
|
3048
|
+
rowId: request.rowId,
|
|
3049
|
+
status: 'skipped',
|
|
3050
|
+
success: false,
|
|
3051
|
+
value: null,
|
|
3052
|
+
error: null,
|
|
3053
|
+
};
|
|
3054
|
+
});
|
|
3055
|
+
stepResults.push({
|
|
3056
|
+
id: step.id,
|
|
3057
|
+
kind: isInlineWaterfallCodeStep(step) ? 'code' : 'tool',
|
|
3058
|
+
toolId: stepProvider,
|
|
3059
|
+
results: skippedResults,
|
|
3060
|
+
});
|
|
3061
|
+
stepIdx++;
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
this.bumpCounter('waterfallStepExecutions', pendingRows.size);
|
|
3065
|
+
const pendingRowIds = new Set<number>(pendingRows);
|
|
3066
|
+
const stepRequests = requests.filter((req) =>
|
|
3067
|
+
pendingRowIds.has(req.rowId),
|
|
3068
|
+
);
|
|
3069
|
+
const perRowResultsByRowId = new Map<number, PlayStepRowResult>();
|
|
3070
|
+
if (isInlineWaterfallCodeStep(step)) {
|
|
3071
|
+
this.log(
|
|
3072
|
+
` Inline waterfall ${spec.id} -> ${step.id}: ${stepRequests.length} pending rows (code)`,
|
|
3073
|
+
);
|
|
3074
|
+
await executeChunkedRequests<WaterfallRequest, unknown>({
|
|
3075
|
+
requests: stepRequests,
|
|
3076
|
+
batchSize: await this.rateLimitScheduler.getSuggestedParallelism(
|
|
3077
|
+
`code:${spec.id}:${step.id}`,
|
|
3078
|
+
20,
|
|
3079
|
+
),
|
|
3080
|
+
execute: async (request) => {
|
|
3081
|
+
const codeStepCtx = {
|
|
3082
|
+
tools: {
|
|
3083
|
+
execute: async (request: ToolExecutionRequest) =>
|
|
3084
|
+
await this.callToolAPI(request.tool, request.input),
|
|
3085
|
+
},
|
|
3086
|
+
};
|
|
3087
|
+
return await step.run(request.input, codeStepCtx);
|
|
3088
|
+
},
|
|
3089
|
+
onChunkComplete: async (chunkResults) => {
|
|
3090
|
+
for (const entry of chunkResults) {
|
|
3091
|
+
const matchedValue = extractInlineWaterfallCodeStepValue(
|
|
3092
|
+
spec.output,
|
|
3093
|
+
entry.result,
|
|
3094
|
+
);
|
|
3095
|
+
const bucket = resultsByRow.get(entry.request.rowId) ?? [];
|
|
3096
|
+
const nextBucket = Array.isArray(matchedValue)
|
|
3097
|
+
? [...bucket, ...matchedValue]
|
|
3098
|
+
: matchedValue != null
|
|
3099
|
+
? [...bucket, matchedValue]
|
|
3100
|
+
: bucket;
|
|
3101
|
+
resultsByRow.set(entry.request.rowId, nextBucket);
|
|
3102
|
+
const satisfied = nextBucket.length >= spec.minResults;
|
|
3103
|
+
perRowResultsByRowId.set(entry.request.rowId, {
|
|
3104
|
+
rowId: entry.request.rowId,
|
|
3105
|
+
status: matchedValue != null ? 'completed' : 'missed',
|
|
3106
|
+
success: matchedValue != null,
|
|
3107
|
+
value: matchedValue,
|
|
3108
|
+
provider: matchedValue != null ? 'code' : undefined,
|
|
3109
|
+
error: null,
|
|
3110
|
+
});
|
|
3111
|
+
if (satisfied) {
|
|
3112
|
+
const finalValue =
|
|
3113
|
+
spec.minResults === 1 ? (nextBucket[0] ?? null) : nextBucket;
|
|
3114
|
+
this.resolveWaterfall(
|
|
3115
|
+
queueKey,
|
|
3116
|
+
spec.id,
|
|
3117
|
+
entry.request.rowId,
|
|
3118
|
+
finalValue,
|
|
3119
|
+
'code',
|
|
3120
|
+
entry.request.rowKey ?? null,
|
|
3121
|
+
entry.request.tableNamespace ?? null,
|
|
3122
|
+
entry.request.fieldName,
|
|
3123
|
+
);
|
|
3124
|
+
pendingRows.delete(entry.request.rowId);
|
|
3125
|
+
this.emitCellUpdate({
|
|
3126
|
+
rowId: entry.request.rowId,
|
|
3127
|
+
key: entry.request.rowKey ?? null,
|
|
3128
|
+
tableNamespace: entry.request.tableNamespace ?? null,
|
|
3129
|
+
columnName: stepColumnName,
|
|
3130
|
+
status: 'completed',
|
|
3131
|
+
stage: step.id,
|
|
3132
|
+
provider: 'code',
|
|
3133
|
+
value: matchedValue,
|
|
3134
|
+
});
|
|
3135
|
+
for (let ri = stepIdx + 1; ri < spec.steps.length; ri++) {
|
|
3136
|
+
const skippedStep = spec.steps[ri]!;
|
|
3137
|
+
this.emitCellUpdate({
|
|
3138
|
+
rowId: entry.request.rowId,
|
|
3139
|
+
key: entry.request.rowKey ?? null,
|
|
3140
|
+
tableNamespace: entry.request.tableNamespace ?? null,
|
|
3141
|
+
columnName: stepColumnNames[ri]!,
|
|
3142
|
+
status: 'skipped',
|
|
3143
|
+
stage: skippedStep.id,
|
|
3144
|
+
provider: isInlineWaterfallToolStep(skippedStep)
|
|
3145
|
+
? skippedStep.toolId
|
|
3146
|
+
: 'code',
|
|
3147
|
+
value: null,
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
resolvedInChunkRowIds.add(entry.request.rowId);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3154
|
+
},
|
|
3155
|
+
});
|
|
3156
|
+
} else {
|
|
3157
|
+
const strategy =
|
|
3158
|
+
this.options.getBatchOperationStrategy?.(step.toolId) ?? null;
|
|
3159
|
+
this.log(
|
|
3160
|
+
` Inline waterfall ${spec.id} -> ${step.id}: ${stepRequests.length} pending rows`,
|
|
3161
|
+
);
|
|
3162
|
+
for (const request of stepRequests) {
|
|
3163
|
+
this.emitCellUpdate({
|
|
3164
|
+
rowId: request.rowId,
|
|
3165
|
+
key: request.rowKey ?? null,
|
|
3166
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3167
|
+
columnName: stepColumnName,
|
|
3168
|
+
status: 'running',
|
|
3169
|
+
stage: step.id,
|
|
3170
|
+
provider: step.toolId,
|
|
3171
|
+
value: null,
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
if (strategy) {
|
|
3176
|
+
const compiledBatches = compileRequestsWithStrategy({
|
|
3177
|
+
requests: stepRequests,
|
|
3178
|
+
strategy,
|
|
3179
|
+
getPayload: (request: WaterfallRequest) =>
|
|
3180
|
+
step.mapInput(request.input),
|
|
3181
|
+
});
|
|
3182
|
+
this.log(
|
|
3183
|
+
` ${step.toolId}: compiled ${compiledBatches.length} batch request(s)` +
|
|
3184
|
+
` (${this.summarizeBatchSizes(
|
|
3185
|
+
compiledBatches.map((batch) => batch.memberRequests.length),
|
|
3186
|
+
)})`,
|
|
3187
|
+
);
|
|
3188
|
+
await executeChunkedRequests({
|
|
3189
|
+
requests: compiledBatches,
|
|
3190
|
+
batchSize:
|
|
3191
|
+
compiledBatches.length > 0
|
|
3192
|
+
? await this.rateLimitScheduler.getSuggestedParallelism(
|
|
3193
|
+
compiledBatches[0]!.batchOperation,
|
|
3194
|
+
4,
|
|
3195
|
+
)
|
|
3196
|
+
: 4,
|
|
3197
|
+
execute: async (batch) =>
|
|
3198
|
+
await this.callToolAPI(batch.batchOperation, batch.batchPayload),
|
|
3199
|
+
onChunkComplete: async (chunkResults) => {
|
|
3200
|
+
for (const entry of chunkResults) {
|
|
3201
|
+
const splitResults =
|
|
3202
|
+
entry.result != null
|
|
3203
|
+
? entry.request.splitResults(entry.result)
|
|
3204
|
+
: entry.request.memberRequests.map(() => null);
|
|
3205
|
+
for (
|
|
3206
|
+
let index = 0;
|
|
3207
|
+
index < entry.request.memberRequests.length;
|
|
3208
|
+
index += 1
|
|
3209
|
+
) {
|
|
3210
|
+
const request = entry.request.memberRequests[index]!;
|
|
3211
|
+
const rawResult = splitResults[index] ?? null;
|
|
3212
|
+
const matchedValue = await extractWaterfallOutputValue(
|
|
3213
|
+
step.toolId,
|
|
3214
|
+
spec.output,
|
|
3215
|
+
rawResult,
|
|
3216
|
+
this.options.getToolResultIdentityGetters,
|
|
3217
|
+
);
|
|
3218
|
+
const bucket = resultsByRow.get(request.rowId) ?? [];
|
|
3219
|
+
const nextBucket = Array.isArray(matchedValue)
|
|
3220
|
+
? [...bucket, ...matchedValue]
|
|
3221
|
+
: matchedValue != null
|
|
3222
|
+
? [...bucket, matchedValue]
|
|
3223
|
+
: bucket;
|
|
3224
|
+
resultsByRow.set(request.rowId, nextBucket);
|
|
3225
|
+
const satisfied = nextBucket.length >= spec.minResults;
|
|
3226
|
+
perRowResultsByRowId.set(request.rowId, {
|
|
3227
|
+
rowId: request.rowId,
|
|
3228
|
+
status: matchedValue != null ? 'completed' : 'missed',
|
|
3229
|
+
success: matchedValue != null,
|
|
3230
|
+
value: matchedValue,
|
|
3231
|
+
provider: matchedValue != null ? step.toolId : undefined,
|
|
3232
|
+
error: null,
|
|
3233
|
+
});
|
|
3234
|
+
if (satisfied) {
|
|
3235
|
+
const finalValue =
|
|
3236
|
+
spec.minResults === 1
|
|
3237
|
+
? (nextBucket[0] ?? null)
|
|
3238
|
+
: nextBucket;
|
|
3239
|
+
this.resolveWaterfall(
|
|
3240
|
+
queueKey,
|
|
3241
|
+
spec.id,
|
|
3242
|
+
request.rowId,
|
|
3243
|
+
finalValue,
|
|
3244
|
+
step.toolId,
|
|
3245
|
+
request.rowKey ?? null,
|
|
3246
|
+
request.tableNamespace ?? null,
|
|
3247
|
+
request.fieldName,
|
|
3248
|
+
);
|
|
3249
|
+
pendingRows.delete(request.rowId);
|
|
3250
|
+
this.emitCellUpdate({
|
|
3251
|
+
rowId: request.rowId,
|
|
3252
|
+
key: request.rowKey ?? null,
|
|
3253
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3254
|
+
columnName: stepColumnNames[stepIdx]!,
|
|
3255
|
+
status: 'completed',
|
|
3256
|
+
stage: step.id,
|
|
3257
|
+
provider: step.toolId,
|
|
3258
|
+
value: matchedValue,
|
|
3259
|
+
});
|
|
3260
|
+
for (let ri = stepIdx + 1; ri < spec.steps.length; ri++) {
|
|
3261
|
+
const skippedStep = spec.steps[ri]!;
|
|
3262
|
+
this.emitCellUpdate({
|
|
3263
|
+
rowId: request.rowId,
|
|
3264
|
+
key: request.rowKey ?? null,
|
|
3265
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3266
|
+
columnName: stepColumnNames[ri]!,
|
|
3267
|
+
status: 'skipped',
|
|
3268
|
+
stage: skippedStep.id,
|
|
3269
|
+
provider: isInlineWaterfallToolStep(skippedStep)
|
|
3270
|
+
? skippedStep.toolId
|
|
3271
|
+
: 'code',
|
|
3272
|
+
value: null,
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
resolvedInChunkRowIds.add(request.rowId);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3280
|
+
},
|
|
3281
|
+
});
|
|
3282
|
+
} else {
|
|
3283
|
+
this.log(
|
|
3284
|
+
` ${step.toolId}: executing ${stepRequests.length} unbatched request(s)`,
|
|
3285
|
+
);
|
|
3286
|
+
await executeChunkedRequests<WaterfallRequest, unknown>({
|
|
3287
|
+
requests: stepRequests,
|
|
3288
|
+
batchSize: await this.rateLimitScheduler.getSuggestedParallelism(
|
|
3289
|
+
step.toolId,
|
|
3290
|
+
50,
|
|
3291
|
+
),
|
|
3292
|
+
execute: async (request) =>
|
|
3293
|
+
await this.callToolAPI(step.toolId, step.mapInput(request.input)),
|
|
3294
|
+
onChunkComplete: async (chunkResults) => {
|
|
3295
|
+
for (const entry of chunkResults) {
|
|
3296
|
+
const matchedValue = await extractWaterfallOutputValue(
|
|
3297
|
+
step.toolId,
|
|
3298
|
+
spec.output,
|
|
3299
|
+
entry.result,
|
|
3300
|
+
this.options.getToolResultIdentityGetters,
|
|
3301
|
+
);
|
|
3302
|
+
const bucket = resultsByRow.get(entry.request.rowId) ?? [];
|
|
3303
|
+
const nextBucket = Array.isArray(matchedValue)
|
|
3304
|
+
? [...bucket, ...matchedValue]
|
|
3305
|
+
: matchedValue != null
|
|
3306
|
+
? [...bucket, matchedValue]
|
|
3307
|
+
: bucket;
|
|
3308
|
+
resultsByRow.set(entry.request.rowId, nextBucket);
|
|
3309
|
+
const satisfied = nextBucket.length >= spec.minResults;
|
|
3310
|
+
perRowResultsByRowId.set(entry.request.rowId, {
|
|
3311
|
+
rowId: entry.request.rowId,
|
|
3312
|
+
status: matchedValue != null ? 'completed' : 'missed',
|
|
3313
|
+
success: matchedValue != null,
|
|
3314
|
+
value: matchedValue,
|
|
3315
|
+
provider: matchedValue != null ? step.toolId : undefined,
|
|
3316
|
+
error: null,
|
|
3317
|
+
});
|
|
3318
|
+
if (satisfied) {
|
|
3319
|
+
const finalValue =
|
|
3320
|
+
spec.minResults === 1
|
|
3321
|
+
? (nextBucket[0] ?? null)
|
|
3322
|
+
: nextBucket;
|
|
3323
|
+
this.resolveWaterfall(
|
|
3324
|
+
queueKey,
|
|
3325
|
+
spec.id,
|
|
3326
|
+
entry.request.rowId,
|
|
3327
|
+
finalValue,
|
|
3328
|
+
step.toolId,
|
|
3329
|
+
entry.request.rowKey ?? null,
|
|
3330
|
+
entry.request.tableNamespace ?? null,
|
|
3331
|
+
entry.request.fieldName,
|
|
3332
|
+
);
|
|
3333
|
+
pendingRows.delete(entry.request.rowId);
|
|
3334
|
+
this.emitCellUpdate({
|
|
3335
|
+
rowId: entry.request.rowId,
|
|
3336
|
+
key: entry.request.rowKey ?? null,
|
|
3337
|
+
tableNamespace: entry.request.tableNamespace ?? null,
|
|
3338
|
+
columnName: stepColumnNames[stepIdx]!,
|
|
3339
|
+
status: 'completed',
|
|
3340
|
+
stage: step.id,
|
|
3341
|
+
provider: step.toolId,
|
|
3342
|
+
value: matchedValue,
|
|
3343
|
+
});
|
|
3344
|
+
for (let ri = stepIdx + 1; ri < spec.steps.length; ri++) {
|
|
3345
|
+
const skippedStep = spec.steps[ri]!;
|
|
3346
|
+
this.emitCellUpdate({
|
|
3347
|
+
rowId: entry.request.rowId,
|
|
3348
|
+
key: entry.request.rowKey ?? null,
|
|
3349
|
+
tableNamespace: entry.request.tableNamespace ?? null,
|
|
3350
|
+
columnName: stepColumnNames[ri]!,
|
|
3351
|
+
status: 'skipped',
|
|
3352
|
+
stage: skippedStep.id,
|
|
3353
|
+
provider: isInlineWaterfallToolStep(skippedStep)
|
|
3354
|
+
? skippedStep.toolId
|
|
3355
|
+
: 'code',
|
|
3356
|
+
value: null,
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
resolvedInChunkRowIds.add(entry.request.rowId);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3363
|
+
},
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
const perRowResults: PlayStepRowResult[] = requests.map((request) => {
|
|
3369
|
+
if (resolvedInChunkRowIds.has(request.rowId)) {
|
|
3370
|
+
const existing = perRowResultsByRowId.get(request.rowId);
|
|
3371
|
+
return (
|
|
3372
|
+
existing ?? {
|
|
3373
|
+
rowId: request.rowId,
|
|
3374
|
+
status: 'completed' as const,
|
|
3375
|
+
success: true,
|
|
3376
|
+
value: null,
|
|
3377
|
+
error: null,
|
|
3378
|
+
}
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
const existing = perRowResultsByRowId.get(request.rowId);
|
|
3382
|
+
if (existing) {
|
|
3383
|
+
this.emitCellUpdate({
|
|
3384
|
+
rowId: request.rowId,
|
|
3385
|
+
key: request.rowKey ?? null,
|
|
3386
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3387
|
+
columnName: stepColumnName,
|
|
3388
|
+
status: existing.status,
|
|
3389
|
+
stage: step.id,
|
|
3390
|
+
provider: existing.provider ?? stepProvider,
|
|
3391
|
+
error: existing.error ?? null,
|
|
3392
|
+
value: existing.value ?? null,
|
|
3393
|
+
});
|
|
3394
|
+
return existing;
|
|
3395
|
+
}
|
|
3396
|
+
if (!pendingRowIds.has(request.rowId)) {
|
|
3397
|
+
this.emitCellUpdate({
|
|
3398
|
+
rowId: request.rowId,
|
|
3399
|
+
key: request.rowKey ?? null,
|
|
3400
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3401
|
+
columnName: stepColumnName,
|
|
3402
|
+
status: 'skipped',
|
|
3403
|
+
stage: step.id,
|
|
3404
|
+
provider: stepProvider,
|
|
3405
|
+
value: null,
|
|
3406
|
+
});
|
|
3407
|
+
return {
|
|
3408
|
+
rowId: request.rowId,
|
|
3409
|
+
status: 'skipped',
|
|
3410
|
+
success: false,
|
|
3411
|
+
value: null,
|
|
3412
|
+
error: null,
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
this.emitCellUpdate({
|
|
3416
|
+
rowId: request.rowId,
|
|
3417
|
+
key: request.rowKey ?? null,
|
|
3418
|
+
tableNamespace: request.tableNamespace ?? null,
|
|
3419
|
+
columnName: stepColumnName,
|
|
3420
|
+
status: 'missed',
|
|
3421
|
+
stage: step.id,
|
|
3422
|
+
provider: stepProvider,
|
|
3423
|
+
value: null,
|
|
3424
|
+
});
|
|
3425
|
+
return {
|
|
3426
|
+
rowId: request.rowId,
|
|
3427
|
+
status: 'missed',
|
|
3428
|
+
success: false,
|
|
3429
|
+
value: null,
|
|
3430
|
+
error: null,
|
|
3431
|
+
};
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
stepResults.push({
|
|
3435
|
+
id: step.id,
|
|
3436
|
+
kind: isInlineWaterfallCodeStep(step) ? 'code' : 'tool',
|
|
3437
|
+
toolId: stepProvider,
|
|
3438
|
+
// Persist only a bounded preview in the in-memory execution trace.
|
|
3439
|
+
// The authoritative per-row state already lives in Neon/live progress.
|
|
3440
|
+
results: compactRowResultsPreview(perRowResults),
|
|
3441
|
+
});
|
|
3442
|
+
stepIdx++;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
for (const req of requests) {
|
|
3446
|
+
const wState = this.rowStates.get(req.rowId)?.waterfalls.get(spec.id);
|
|
3447
|
+
if (wState?.status === 'pending') {
|
|
3448
|
+
wState.status = 'failed';
|
|
3449
|
+
wState.error = 'All waterfall steps exhausted';
|
|
3450
|
+
this.waterfallReplay.setResolved(queueKey, req, null);
|
|
3451
|
+
const resolver = this.resolvers.get(`${req.rowId}-${queueKey}`);
|
|
3452
|
+
if (resolver) {
|
|
3453
|
+
resolver(null);
|
|
3454
|
+
this.resolvers.delete(`${req.rowId}-${queueKey}`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
const groupResults: PlayStepRowResult[] = requests.map((req) => {
|
|
3460
|
+
const wState = this.rowStates.get(req.rowId)?.waterfalls.get(spec.id);
|
|
3461
|
+
const success = wState?.status === 'complete';
|
|
3462
|
+
return {
|
|
3463
|
+
rowId: req.rowId,
|
|
3464
|
+
status: success ? 'completed' : 'missed',
|
|
3465
|
+
success,
|
|
3466
|
+
value: wState?.result,
|
|
3467
|
+
error: success ? null : (wState?.error ?? null),
|
|
3468
|
+
};
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
const waterfallStep = {
|
|
3472
|
+
type: 'waterfall' as const,
|
|
3473
|
+
id: spec.id,
|
|
3474
|
+
output: spec.output,
|
|
3475
|
+
minResults: spec.minResults,
|
|
3476
|
+
steps: stepResults,
|
|
3477
|
+
results: compactRowResultsPreview(groupResults),
|
|
3478
|
+
description: normalizeStepDescription(requests[0]?.description),
|
|
3479
|
+
};
|
|
3480
|
+
if (this.activeMapStep) {
|
|
3481
|
+
this.activeMapStep.substeps.push(waterfallStep);
|
|
3482
|
+
} else {
|
|
3483
|
+
this.steps.push(waterfallStep);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
private resolveWaterfall(
|
|
3488
|
+
queueKey: string,
|
|
3489
|
+
toolName: string,
|
|
3490
|
+
rowId: number,
|
|
3491
|
+
result: unknown,
|
|
3492
|
+
provider: string,
|
|
3493
|
+
rowKey: string | null,
|
|
3494
|
+
tableNamespace: string | null,
|
|
3495
|
+
fieldName?: string,
|
|
3496
|
+
): void {
|
|
3497
|
+
const wState = this.rowStates.get(rowId)?.waterfalls.get(toolName);
|
|
3498
|
+
if (!wState || wState.status !== 'pending') return;
|
|
3499
|
+
|
|
3500
|
+
wState.status = 'complete';
|
|
3501
|
+
wState.result = result;
|
|
3502
|
+
this.waterfallReplay.setResolved(
|
|
3503
|
+
queueKey,
|
|
3504
|
+
{
|
|
3505
|
+
rowId,
|
|
3506
|
+
rowKey,
|
|
3507
|
+
},
|
|
3508
|
+
result,
|
|
3509
|
+
);
|
|
3510
|
+
|
|
3511
|
+
const resolver = this.resolvers.get(`${rowId}-${queueKey}`);
|
|
3512
|
+
if (resolver) {
|
|
3513
|
+
resolver(result);
|
|
3514
|
+
this.resolvers.delete(`${rowId}-${queueKey}`);
|
|
3515
|
+
}
|
|
3516
|
+
this.emitScopedFieldMetaUpdate({
|
|
3517
|
+
rowId,
|
|
3518
|
+
key: rowKey,
|
|
3519
|
+
tableNamespace,
|
|
3520
|
+
fieldName,
|
|
3521
|
+
status: 'running',
|
|
3522
|
+
rowStatus: 'running',
|
|
3523
|
+
stage: toolName,
|
|
3524
|
+
provider,
|
|
3525
|
+
error: null,
|
|
3526
|
+
dataPatch: {},
|
|
3527
|
+
});
|
|
3528
|
+
this.logWaterfallMatch({ queueKey, rowId, provider });
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
// ——— Batched tool call execution ———
|
|
3532
|
+
|
|
3533
|
+
private async executeBatchedToolCalls(): Promise<void> {
|
|
3534
|
+
const queuedToolCalls = this.toolCallQueue;
|
|
3535
|
+
this.toolCallQueue = [];
|
|
3536
|
+
|
|
3537
|
+
// Group by toolId
|
|
3538
|
+
const byTool = new Map<string, ToolCallRequest[]>();
|
|
3539
|
+
for (const req of queuedToolCalls) {
|
|
3540
|
+
if (!byTool.has(req.toolId)) byTool.set(req.toolId, []);
|
|
3541
|
+
byTool.get(req.toolId)!.push(req);
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
await Promise.all(
|
|
3545
|
+
[...byTool.entries()].map(async ([toolId, requests]) => {
|
|
3546
|
+
this.log(`Executing tool batch ${toolId}: ${requests.length} calls`);
|
|
3547
|
+
|
|
3548
|
+
const pendingRequests: ToolCallRequest[] = [];
|
|
3549
|
+
for (const req of requests) {
|
|
3550
|
+
const cached = this.getCachedToolResult(
|
|
3551
|
+
toolId,
|
|
3552
|
+
this.buildToolResultCacheKey({
|
|
3553
|
+
rowId: req.rowId,
|
|
3554
|
+
tableNamespace: req.tableNamespace,
|
|
3555
|
+
rowKey: req.rowKey ?? undefined,
|
|
3556
|
+
callId: req.callId,
|
|
3557
|
+
}),
|
|
3558
|
+
);
|
|
3559
|
+
if (cached?.done) {
|
|
3560
|
+
this.log(` Row ${req.rowId} ${toolId}: recovered from checkpoint`);
|
|
3561
|
+
const resolvers = this.toolCallResolvers.get(req.callId);
|
|
3562
|
+
if (resolvers) {
|
|
3563
|
+
for (const resolver of resolvers) {
|
|
3564
|
+
resolver(cached.result);
|
|
3565
|
+
}
|
|
3566
|
+
this.toolCallResolvers.delete(req.callId);
|
|
3567
|
+
}
|
|
3568
|
+
} else {
|
|
3569
|
+
pendingRequests.push(req);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
if (pendingRequests.length > 0) {
|
|
3574
|
+
const strategy =
|
|
3575
|
+
this.options.getBatchOperationStrategy?.(toolId) ?? null;
|
|
3576
|
+
|
|
3577
|
+
if (strategy) {
|
|
3578
|
+
const compiledBatches = compileRequestsWithStrategy({
|
|
3579
|
+
requests: pendingRequests,
|
|
3580
|
+
strategy,
|
|
3581
|
+
getPayload: (request: ToolCallRequest) => request.input,
|
|
3582
|
+
});
|
|
3583
|
+
|
|
3584
|
+
await executeChunkedRequests({
|
|
3585
|
+
requests: compiledBatches,
|
|
3586
|
+
batchSize:
|
|
3587
|
+
compiledBatches.length > 0
|
|
3588
|
+
? await this.rateLimitScheduler.getSuggestedParallelism(
|
|
3589
|
+
compiledBatches[0]!.batchOperation,
|
|
3590
|
+
4,
|
|
3591
|
+
)
|
|
3592
|
+
: 4,
|
|
3593
|
+
execute: async (batch) =>
|
|
3594
|
+
await this.callToolAPI(
|
|
3595
|
+
batch.batchOperation,
|
|
3596
|
+
batch.batchPayload,
|
|
3597
|
+
),
|
|
3598
|
+
onChunkComplete: async (chunkResults) => {
|
|
3599
|
+
for (const entry of chunkResults) {
|
|
3600
|
+
const splitResults =
|
|
3601
|
+
entry.result != null
|
|
3602
|
+
? entry.request.splitResults(entry.result)
|
|
3603
|
+
: entry.request.memberRequests.map(() => null);
|
|
3604
|
+
|
|
3605
|
+
for (
|
|
3606
|
+
let index = 0;
|
|
3607
|
+
index < entry.request.memberRequests.length;
|
|
3608
|
+
index += 1
|
|
3609
|
+
) {
|
|
3610
|
+
const request = entry.request.memberRequests[index]!;
|
|
3611
|
+
await this.resolveToolCall(
|
|
3612
|
+
toolId,
|
|
3613
|
+
request,
|
|
3614
|
+
splitResults[index] ?? null,
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3620
|
+
},
|
|
3621
|
+
});
|
|
3622
|
+
} else {
|
|
3623
|
+
await executeChunkedRequests<
|
|
3624
|
+
ToolCallRequest,
|
|
3625
|
+
ToolExecutionResponse
|
|
3626
|
+
>({
|
|
3627
|
+
requests: pendingRequests,
|
|
3628
|
+
batchSize: await this.rateLimitScheduler.getSuggestedParallelism(
|
|
3629
|
+
toolId,
|
|
3630
|
+
50,
|
|
3631
|
+
),
|
|
3632
|
+
execute: async (request) =>
|
|
3633
|
+
await this.callToolExecutionAPI(toolId, request.input),
|
|
3634
|
+
onChunkComplete: async (chunkResults) => {
|
|
3635
|
+
for (const entry of chunkResults) {
|
|
3636
|
+
await this.resolveToolCall(
|
|
3637
|
+
toolId,
|
|
3638
|
+
entry.request,
|
|
3639
|
+
entry.result?.result ?? null,
|
|
3640
|
+
entry.result?.metadata ?? null,
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3645
|
+
},
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// Record step — nest under map if inside one
|
|
3651
|
+
const stepResults: PlayStepRowResult[] = requests.map((req) => {
|
|
3652
|
+
const cachedResult = this.getCachedToolResult(
|
|
3653
|
+
toolId,
|
|
3654
|
+
this.buildToolResultCacheKey({
|
|
3655
|
+
rowId: req.rowId,
|
|
3656
|
+
tableNamespace: req.tableNamespace,
|
|
3657
|
+
rowKey: req.rowKey ?? undefined,
|
|
3658
|
+
callId: req.callId,
|
|
3659
|
+
}),
|
|
3660
|
+
);
|
|
3661
|
+
return {
|
|
3662
|
+
rowId: req.rowId,
|
|
3663
|
+
status: cachedResult?.result != null ? 'completed' : 'failed',
|
|
3664
|
+
success: cachedResult?.result != null,
|
|
3665
|
+
value: cachedResult?.result,
|
|
3666
|
+
error: cachedResult?.result != null ? null : 'Tool call failed',
|
|
3667
|
+
};
|
|
3668
|
+
});
|
|
3669
|
+
const toolStep = {
|
|
3670
|
+
type: 'tool' as const,
|
|
3671
|
+
toolId,
|
|
3672
|
+
// Keep the step trace preview-sized for large map pages.
|
|
3673
|
+
results: compactRowResultsPreview(stepResults),
|
|
3674
|
+
description: normalizeStepDescription(requests[0]?.description),
|
|
3675
|
+
};
|
|
3676
|
+
if (this.activeMapStep) {
|
|
3677
|
+
this.activeMapStep.substeps.push(toolStep);
|
|
3678
|
+
} else {
|
|
3679
|
+
this.steps.push(toolStep);
|
|
3680
|
+
}
|
|
3681
|
+
}),
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
private async executeResolvedPlay(
|
|
3686
|
+
resolvedPlay: ResolvedPlayExecution,
|
|
3687
|
+
ctx: PlayContextImpl,
|
|
3688
|
+
input: Record<string, unknown>,
|
|
3689
|
+
): Promise<unknown> {
|
|
3690
|
+
if (resolvedPlay.definition) {
|
|
3691
|
+
if (!this.options.executeStructuredPlayDefinition) {
|
|
3692
|
+
throw new Error(
|
|
3693
|
+
`Play "${resolvedPlay.playId}" is a structured play, but this runtime did not provide a structured play executor.`,
|
|
3694
|
+
);
|
|
3695
|
+
}
|
|
3696
|
+
return await this.options.executeStructuredPlayDefinition({
|
|
3697
|
+
definition: resolvedPlay.definition,
|
|
3698
|
+
ctx,
|
|
3699
|
+
rows: [],
|
|
3700
|
+
playInput: input,
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
if (resolvedPlay.codeFormat === 'cjs_module') {
|
|
3705
|
+
const artifact = resolvedPlay.artifact;
|
|
3706
|
+
if (!artifact) {
|
|
3707
|
+
throw new Error(
|
|
3708
|
+
`Play "${resolvedPlay.playId}" is missing a bundled artifact.`,
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
const runtimeModule =
|
|
3712
|
+
(await import('node:module')) as unknown as typeof import('node:module') & {
|
|
3713
|
+
Module: typeof import('node:module').Module & {
|
|
3714
|
+
_nodeModulePaths: (from: string) => string[];
|
|
3715
|
+
};
|
|
3716
|
+
};
|
|
3717
|
+
const compiled = new runtimeModule.Module(artifact.virtualFilename);
|
|
3718
|
+
compiled.filename = artifact.virtualFilename;
|
|
3719
|
+
compiled.paths = runtimeModule.Module._nodeModulePaths(process.cwd());
|
|
3720
|
+
(
|
|
3721
|
+
compiled as import('node:module').Module & {
|
|
3722
|
+
_compile: (code: string, filename: string) => void;
|
|
3723
|
+
}
|
|
3724
|
+
)._compile(artifact.bundledCode, artifact.virtualFilename);
|
|
3725
|
+
const candidate =
|
|
3726
|
+
typeof compiled.exports === 'function'
|
|
3727
|
+
? compiled.exports
|
|
3728
|
+
: (compiled.exports as { default?: unknown }).default;
|
|
3729
|
+
if (typeof candidate !== 'function') {
|
|
3730
|
+
throw new Error(
|
|
3731
|
+
`Play "${resolvedPlay.playId}" does not export a callable default.`,
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
return await (
|
|
3735
|
+
candidate as (
|
|
3736
|
+
ctx: unknown,
|
|
3737
|
+
runtimeInput: Record<string, unknown>,
|
|
3738
|
+
) => Promise<unknown>
|
|
3739
|
+
)(ctx, input);
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
const code = resolvedPlay.code ?? resolvedPlay.sourceCode;
|
|
3743
|
+
if (!code?.trim()) {
|
|
3744
|
+
throw new Error(
|
|
3745
|
+
`Play "${resolvedPlay.playId}" is missing executable source.`,
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
return await new Function(
|
|
3749
|
+
'ctx',
|
|
3750
|
+
'input',
|
|
3751
|
+
`
|
|
3752
|
+
const __playFn = ${code};
|
|
3753
|
+
return __playFn(ctx, input);
|
|
3754
|
+
`,
|
|
3755
|
+
)(ctx, input);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
// ——— Direct execution (outside map context) ———
|
|
3759
|
+
|
|
3760
|
+
private async executeWaterfallDirect(
|
|
3761
|
+
toolNameOrSpec: string | InlineWaterfallSpec,
|
|
3762
|
+
input: Record<string, unknown>,
|
|
3763
|
+
opts?: WaterfallOptions,
|
|
3764
|
+
): Promise<unknown | null> {
|
|
3765
|
+
if (isInlineWaterfallSpec(toolNameOrSpec)) {
|
|
3766
|
+
this.log(`Direct inline waterfall: ${toolNameOrSpec.id}`);
|
|
3767
|
+
const collected: unknown[] = [];
|
|
3768
|
+
for (const step of toolNameOrSpec.steps) {
|
|
3769
|
+
this.bumpCounter('waterfallStepExecutions');
|
|
3770
|
+
const matched = isInlineWaterfallCodeStep(step)
|
|
3771
|
+
? extractInlineWaterfallCodeStepValue(
|
|
3772
|
+
toolNameOrSpec.output,
|
|
3773
|
+
await step.run(input, {
|
|
3774
|
+
tools: {
|
|
3775
|
+
execute: async (request: ToolExecutionRequest) =>
|
|
3776
|
+
await this.callToolAPI(request.tool, request.input),
|
|
3777
|
+
},
|
|
3778
|
+
}),
|
|
3779
|
+
)
|
|
3780
|
+
: await extractWaterfallOutputValue(
|
|
3781
|
+
step.toolId,
|
|
3782
|
+
toolNameOrSpec.output,
|
|
3783
|
+
await this.callToolAPI(step.toolId, step.mapInput(input)),
|
|
3784
|
+
this.options.getToolResultIdentityGetters,
|
|
3785
|
+
);
|
|
3786
|
+
if (Array.isArray(matched)) {
|
|
3787
|
+
collected.push(...matched);
|
|
3788
|
+
} else if (matched != null) {
|
|
3789
|
+
collected.push(matched);
|
|
3790
|
+
}
|
|
3791
|
+
if (collected.length >= toolNameOrSpec.minResults) {
|
|
3792
|
+
return toolNameOrSpec.minResults === 1
|
|
3793
|
+
? (collected[0] ?? null)
|
|
3794
|
+
: collected;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
const toolName = toolNameOrSpec;
|
|
3801
|
+
this.log(`Direct waterfall: ${toolName}`);
|
|
3802
|
+
const providers = opts?.providers ?? ['hunter', 'leadmagic', 'pdl'];
|
|
3803
|
+
|
|
3804
|
+
for (const provider of providers) {
|
|
3805
|
+
this.log(` Trying ${provider}`);
|
|
3806
|
+
try {
|
|
3807
|
+
this.bumpCounter('retryCount');
|
|
3808
|
+
const result = await this.callToolAPI(
|
|
3809
|
+
resolveWaterfallToolId(provider, toolName),
|
|
3810
|
+
input,
|
|
3811
|
+
);
|
|
3812
|
+
if (
|
|
3813
|
+
result != null &&
|
|
3814
|
+
typeof result === 'object' &&
|
|
3815
|
+
Object.keys(result as object).length > 0
|
|
3816
|
+
) {
|
|
3817
|
+
this.log(` Found with ${provider}`);
|
|
3818
|
+
return result;
|
|
3819
|
+
}
|
|
3820
|
+
} catch {
|
|
3821
|
+
this.log(` Failed with ${provider}`);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
this.log(` All providers exhausted`);
|
|
3826
|
+
return null;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
private async callToolAPI(
|
|
3830
|
+
toolId: string,
|
|
3831
|
+
input: Record<string, unknown>,
|
|
3832
|
+
): Promise<unknown> {
|
|
3833
|
+
return (await this.callToolExecutionAPI(toolId, input)).result;
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
private async callToolExecutionAPI(
|
|
3837
|
+
toolId: string,
|
|
3838
|
+
input: Record<string, unknown>,
|
|
3839
|
+
): Promise<ToolExecutionResponse> {
|
|
3840
|
+
if (!this.options.executorToken || !this.options.baseUrl) {
|
|
3841
|
+
throw new Error(
|
|
3842
|
+
'executorToken and baseUrl are required for tool API calls (cloud execution only)',
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
const url = `${this.options.baseUrl}/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
|
|
3846
|
+
|
|
3847
|
+
this.acquireToolConcurrency();
|
|
3848
|
+
try {
|
|
3849
|
+
return await withActiveSpan(
|
|
3850
|
+
'plays.tool.execute',
|
|
3851
|
+
{
|
|
3852
|
+
tracer: 'deepline.plays',
|
|
3853
|
+
attributes: {
|
|
3854
|
+
'plays.play_name': this.options.playId ?? 'anonymous-play',
|
|
3855
|
+
'plays.workflow_id': this.options.workflowId ?? '',
|
|
3856
|
+
'plays.run_id': this.options.runId ?? this.options.workflowId ?? '',
|
|
3857
|
+
'plays.tool_id': toolId,
|
|
3858
|
+
},
|
|
3859
|
+
},
|
|
3860
|
+
async (span) =>
|
|
3861
|
+
await this.rateLimitScheduler.run(toolId, async () => {
|
|
3862
|
+
let rateLimitAttempt = 0;
|
|
3863
|
+
|
|
3864
|
+
while (true) {
|
|
3865
|
+
const response = await fetch(url, {
|
|
3866
|
+
method: 'POST',
|
|
3867
|
+
headers: {
|
|
3868
|
+
'Content-Type': 'application/json',
|
|
3869
|
+
Authorization: `Bearer ${this.options.executorToken}`,
|
|
3870
|
+
[EXECUTE_TOOL_METADATA_HEADER]: 'true',
|
|
3871
|
+
},
|
|
3872
|
+
body: JSON.stringify({ payload: input }),
|
|
3873
|
+
});
|
|
3874
|
+
|
|
3875
|
+
span.setAttribute('plays.http_status_code', response.status);
|
|
3876
|
+
|
|
3877
|
+
if (response.status === 429) {
|
|
3878
|
+
rateLimitAttempt += 1;
|
|
3879
|
+
this.bumpCounter('retryCount');
|
|
3880
|
+
const retryAfterMs = parseRetryAfterMs(
|
|
3881
|
+
response.headers.get('retry-after'),
|
|
3882
|
+
);
|
|
3883
|
+
span.setAttribute(
|
|
3884
|
+
'plays.rate_limit_retry_after_ms',
|
|
3885
|
+
retryAfterMs,
|
|
3886
|
+
);
|
|
3887
|
+
span.setAttribute('plays.rate_limit_attempt', rateLimitAttempt);
|
|
3888
|
+
this.log(
|
|
3889
|
+
`Tool ${toolId} rate limited; retrying after ${retryAfterMs}ms`,
|
|
3890
|
+
);
|
|
3891
|
+
await this.sleepWithCheckpointHeartbeat(retryAfterMs);
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
if (!response.ok) {
|
|
3896
|
+
const text = await response.text();
|
|
3897
|
+
if (
|
|
3898
|
+
isRetryableTransientToolHttpStatus(toolId, response.status) &&
|
|
3899
|
+
rateLimitAttempt + 1 < TOOL_TRANSIENT_HTTP_MAX_ATTEMPTS
|
|
3900
|
+
) {
|
|
3901
|
+
rateLimitAttempt += 1;
|
|
3902
|
+
this.bumpCounter('retryCount');
|
|
3903
|
+
const retryAfterMs = parseRetryAfterMs(
|
|
3904
|
+
response.headers.get('retry-after'),
|
|
3905
|
+
);
|
|
3906
|
+
span.setAttribute(
|
|
3907
|
+
'plays.transient_http_retry_after_ms',
|
|
3908
|
+
retryAfterMs,
|
|
3909
|
+
);
|
|
3910
|
+
span.setAttribute(
|
|
3911
|
+
'plays.transient_http_attempt',
|
|
3912
|
+
rateLimitAttempt,
|
|
3913
|
+
);
|
|
3914
|
+
this.log(
|
|
3915
|
+
`Tool ${toolId} returned ${response.status}; retrying after ${retryAfterMs}ms`,
|
|
3916
|
+
);
|
|
3917
|
+
await this.sleepWithCheckpointHeartbeat(retryAfterMs);
|
|
3918
|
+
continue;
|
|
3919
|
+
}
|
|
3920
|
+
const failureDetail = (() => {
|
|
3921
|
+
try {
|
|
3922
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
3923
|
+
const detail =
|
|
3924
|
+
typeof parsed.message === 'string'
|
|
3925
|
+
? parsed.message
|
|
3926
|
+
: typeof parsed.error === 'string'
|
|
3927
|
+
? parsed.error
|
|
3928
|
+
: text;
|
|
3929
|
+
const code =
|
|
3930
|
+
typeof parsed.code === 'string'
|
|
3931
|
+
? ` code=${parsed.code}`
|
|
3932
|
+
: '';
|
|
3933
|
+
const requestId =
|
|
3934
|
+
typeof parsed.request_id === 'string'
|
|
3935
|
+
? ` requestId=${parsed.request_id}`
|
|
3936
|
+
: typeof parsed.requestId === 'string'
|
|
3937
|
+
? ` requestId=${parsed.requestId}`
|
|
3938
|
+
: '';
|
|
3939
|
+
return `${detail}${code}${requestId}`;
|
|
3940
|
+
} catch {
|
|
3941
|
+
return text;
|
|
3942
|
+
}
|
|
3943
|
+
})();
|
|
3944
|
+
const message = `Tool ${toolId} failed (${response.status}): ${failureDetail}`;
|
|
3945
|
+
this.log(message);
|
|
3946
|
+
throw new Error(message);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
3950
|
+
const normalized = normalizePlayToolResult(data.result ?? data);
|
|
3951
|
+
const status =
|
|
3952
|
+
typeof data.status === 'string'
|
|
3953
|
+
? data.status
|
|
3954
|
+
: normalized == null
|
|
3955
|
+
? 'no_result'
|
|
3956
|
+
: 'completed';
|
|
3957
|
+
setSpanAttributes(span, {
|
|
3958
|
+
'plays.tool_result_kind':
|
|
3959
|
+
normalized == null
|
|
3960
|
+
? 'null'
|
|
3961
|
+
: Array.isArray(normalized)
|
|
3962
|
+
? 'array'
|
|
3963
|
+
: typeof normalized,
|
|
3964
|
+
});
|
|
3965
|
+
return {
|
|
3966
|
+
status,
|
|
3967
|
+
result: normalized,
|
|
3968
|
+
metadata: parseExecuteToolMetadata(toolId, data),
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
}),
|
|
3972
|
+
);
|
|
3973
|
+
} finally {
|
|
3974
|
+
this.releaseToolConcurrency();
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
private async sleepWithCheckpointHeartbeat(ms: number): Promise<void> {
|
|
3979
|
+
const waitMs = Math.max(1, Math.ceil(ms));
|
|
3980
|
+
const startedAt = Date.now();
|
|
3981
|
+
|
|
3982
|
+
while (Date.now() - startedAt < waitMs) {
|
|
3983
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3984
|
+
const remainingMs = waitMs - (Date.now() - startedAt);
|
|
3985
|
+
await new Promise((resolve) =>
|
|
3986
|
+
setTimeout(
|
|
3987
|
+
resolve,
|
|
3988
|
+
Math.min(TOOL_RETRY_HEARTBEAT_INTERVAL_MS, Math.max(1, remainingMs)),
|
|
3989
|
+
),
|
|
3990
|
+
);
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
this.options.onBatchComplete?.(this.checkpoint);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
export function createPlayContext(options: ContextOptions): PlayContextImpl {
|
|
3998
|
+
return new PlayContextImpl(options);
|
|
3999
|
+
}
|