deepline 0.3.64 → 0.3.65
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/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +999 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +265 -35
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-async-batching.ts +141 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +57 -10
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +2 -0
- package/package.json +1 -1
|
@@ -95,6 +95,7 @@ import { resolveExecutionPolicy } from './governor/policy';
|
|
|
95
95
|
import {
|
|
96
96
|
createRuntimeResourceGovernor,
|
|
97
97
|
type RuntimeResourceGovernor,
|
|
98
|
+
type RuntimeResourceLease,
|
|
98
99
|
} from './resource-governor';
|
|
99
100
|
import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
|
|
100
101
|
import {
|
|
@@ -207,6 +208,14 @@ import {
|
|
|
207
208
|
waitForCompletedRuntimeReceipts,
|
|
208
209
|
type DurableReceiptExecutionStore,
|
|
209
210
|
} from './durable-receipt-execution';
|
|
211
|
+
import {
|
|
212
|
+
asyncOperationDurableStartInput,
|
|
213
|
+
asyncOperationShouldWait,
|
|
214
|
+
classifyAsyncOperationResult,
|
|
215
|
+
executeAsyncOperationLifecycle,
|
|
216
|
+
resolveAsyncOperationPolling,
|
|
217
|
+
type AsyncOperationContractWire,
|
|
218
|
+
} from './async-operation';
|
|
210
219
|
import {
|
|
211
220
|
QUERY_RESULT_DATASET_PAGE_SIZE,
|
|
212
221
|
isCustomerDbDatasetTool,
|
|
@@ -1228,6 +1237,27 @@ function publicToolResponseEnvelope(value: unknown): {
|
|
|
1228
1237
|
};
|
|
1229
1238
|
}
|
|
1230
1239
|
|
|
1240
|
+
function materializedAsyncToolResponse(
|
|
1241
|
+
response: ParsedToolExecuteResponse['toolResponse'] | undefined,
|
|
1242
|
+
result: unknown,
|
|
1243
|
+
): ParsedToolExecuteResponse['toolResponse'] {
|
|
1244
|
+
const hasRawV2 =
|
|
1245
|
+
response !== undefined &&
|
|
1246
|
+
Object.prototype.hasOwnProperty.call(response, 'rawV2');
|
|
1247
|
+
const materializedRawV2 =
|
|
1248
|
+
response?.view === 'data'
|
|
1249
|
+
? {
|
|
1250
|
+
...(recordOrNull(response.rawV2) ?? {}),
|
|
1251
|
+
data: result,
|
|
1252
|
+
}
|
|
1253
|
+
: result;
|
|
1254
|
+
return {
|
|
1255
|
+
...(response ?? {}),
|
|
1256
|
+
raw: result,
|
|
1257
|
+
...(hasRawV2 ? { rawV2: materializedRawV2 } : {}),
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1231
1261
|
/**
|
|
1232
1262
|
* A batched provider request returns one envelope for several logical source
|
|
1233
1263
|
* tool calls. Each source call persists only its own item-shaped canonical
|
|
@@ -1384,6 +1414,8 @@ type ToolExecutionApiOptions = {
|
|
|
1384
1414
|
pageSize: number;
|
|
1385
1415
|
totalRows: number;
|
|
1386
1416
|
};
|
|
1417
|
+
/** Internal recursion guard for lifecycle poll/finish calls. */
|
|
1418
|
+
skipAsyncOperationResolution?: boolean;
|
|
1387
1419
|
};
|
|
1388
1420
|
const IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT = 25;
|
|
1389
1421
|
/**
|
|
@@ -2048,6 +2080,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
2048
2080
|
string,
|
|
2049
2081
|
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
|
2050
2082
|
>();
|
|
2083
|
+
private readonly asyncOperationCapacity = new Map<
|
|
2084
|
+
string,
|
|
2085
|
+
{ active: number; waiters: Array<() => void> }
|
|
2086
|
+
>();
|
|
2051
2087
|
#options: ContextOptions;
|
|
2052
2088
|
private readonly executionScope: RunExecutionScope;
|
|
2053
2089
|
private logBuffer: string[] = [];
|
|
@@ -13123,6 +13159,154 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13123
13159
|
});
|
|
13124
13160
|
}
|
|
13125
13161
|
|
|
13162
|
+
private async acquireAsyncOperationCapacity(input: {
|
|
13163
|
+
toolId: string;
|
|
13164
|
+
provider: string;
|
|
13165
|
+
contract: AsyncOperationContractWire;
|
|
13166
|
+
authScopeDigest: string | null;
|
|
13167
|
+
}): Promise<() => void> {
|
|
13168
|
+
const scopeSuffix =
|
|
13169
|
+
input.contract.capacity.scope === 'organization_action'
|
|
13170
|
+
? input.toolId
|
|
13171
|
+
: input.contract.capacity.scope === 'organization_connection'
|
|
13172
|
+
? (input.authScopeDigest ?? 'default-connection')
|
|
13173
|
+
: input.provider;
|
|
13174
|
+
const key = `${this.#options.orgId ?? 'unknown-org'}:${input.provider}:${input.contract.capacity.scope}:${scopeSuffix}`;
|
|
13175
|
+
let limit = 1;
|
|
13176
|
+
if (input.contract.capacity.limit.kind === 'fixed') {
|
|
13177
|
+
limit = input.contract.capacity.limit.maxConcurrentJobs;
|
|
13178
|
+
} else {
|
|
13179
|
+
const hints =
|
|
13180
|
+
(await this.#options.getToolQueueHints?.(input.toolId)) ?? [];
|
|
13181
|
+
const declaredLimits = hints
|
|
13182
|
+
.map((hint) => hint.maxConcurrency)
|
|
13183
|
+
.filter(
|
|
13184
|
+
(value): value is number =>
|
|
13185
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0,
|
|
13186
|
+
);
|
|
13187
|
+
limit = declaredLimits.length > 0 ? Math.min(...declaredLimits) : 1;
|
|
13188
|
+
}
|
|
13189
|
+
const state = this.asyncOperationCapacity.get(key) ?? {
|
|
13190
|
+
active: 0,
|
|
13191
|
+
waiters: [],
|
|
13192
|
+
};
|
|
13193
|
+
this.asyncOperationCapacity.set(key, state);
|
|
13194
|
+
if (state.active >= limit) {
|
|
13195
|
+
if (input.contract.capacity.onConflict === 'fail') {
|
|
13196
|
+
throw new Error(
|
|
13197
|
+
`Async provider capacity is full for ${input.provider}; the contract allows ${limit} active job(s).`,
|
|
13198
|
+
);
|
|
13199
|
+
}
|
|
13200
|
+
await new Promise<void>((resolve) => state.waiters.push(resolve));
|
|
13201
|
+
}
|
|
13202
|
+
state.active += 1;
|
|
13203
|
+
let released = false;
|
|
13204
|
+
return () => {
|
|
13205
|
+
if (released) return;
|
|
13206
|
+
released = true;
|
|
13207
|
+
state.active = Math.max(0, state.active - 1);
|
|
13208
|
+
state.waiters.shift()?.();
|
|
13209
|
+
if (state.active === 0 && state.waiters.length === 0) {
|
|
13210
|
+
this.asyncOperationCapacity.delete(key);
|
|
13211
|
+
}
|
|
13212
|
+
};
|
|
13213
|
+
}
|
|
13214
|
+
|
|
13215
|
+
private asyncOperationActionReceiptKey(input: {
|
|
13216
|
+
action: string;
|
|
13217
|
+
parentReceiptKey: string | null;
|
|
13218
|
+
jobId: string;
|
|
13219
|
+
phase: string;
|
|
13220
|
+
}): string | null {
|
|
13221
|
+
if (!input.parentReceiptKey) return null;
|
|
13222
|
+
return `${buildDurableToolReceiptPrefix({
|
|
13223
|
+
orgId: this.#options.orgId,
|
|
13224
|
+
toolId: input.action,
|
|
13225
|
+
})}${stableDigest(
|
|
13226
|
+
stableStringify({
|
|
13227
|
+
parentReceiptKey: input.parentReceiptKey,
|
|
13228
|
+
jobId: input.jobId,
|
|
13229
|
+
phase: input.phase,
|
|
13230
|
+
}),
|
|
13231
|
+
)}`;
|
|
13232
|
+
}
|
|
13233
|
+
|
|
13234
|
+
private async resolveAsyncOperation(input: {
|
|
13235
|
+
toolId: string;
|
|
13236
|
+
startInput: Record<string, unknown>;
|
|
13237
|
+
startResponse: ParsedToolExecuteResponse;
|
|
13238
|
+
contract: AsyncOperationContractWire;
|
|
13239
|
+
receiptKey: string | null;
|
|
13240
|
+
}): Promise<ParsedToolExecuteResponse> {
|
|
13241
|
+
const responseValue = (response: ParsedToolExecuteResponse): unknown => {
|
|
13242
|
+
if (response.toolResponse && 'raw' in response.toolResponse) {
|
|
13243
|
+
return response.toolResponse.raw;
|
|
13244
|
+
}
|
|
13245
|
+
const result = recordOrNull(response.result);
|
|
13246
|
+
return result && Object.prototype.hasOwnProperty.call(result, 'data')
|
|
13247
|
+
? result.data
|
|
13248
|
+
: response.result;
|
|
13249
|
+
};
|
|
13250
|
+
const startResult = responseValue(input.startResponse);
|
|
13251
|
+
const lifecycle = await executeAsyncOperationLifecycle({
|
|
13252
|
+
contract: input.contract,
|
|
13253
|
+
startInput: input.startInput,
|
|
13254
|
+
startResult,
|
|
13255
|
+
jobId: input.startResponse.jobId,
|
|
13256
|
+
sleep: async (delayMs) =>
|
|
13257
|
+
await this.sleepWithCheckpointHeartbeat(delayMs),
|
|
13258
|
+
responseValue,
|
|
13259
|
+
executeAction: async (request) => {
|
|
13260
|
+
const receiptKey = this.asyncOperationActionReceiptKey({
|
|
13261
|
+
action: request.action,
|
|
13262
|
+
parentReceiptKey: input.receiptKey,
|
|
13263
|
+
jobId: request.jobId,
|
|
13264
|
+
phase: `${request.phase}:${request.pollAttempt}:${request.page ?? ''}`,
|
|
13265
|
+
});
|
|
13266
|
+
return await this.callToolExecutionAPI(request.action, request.input, {
|
|
13267
|
+
skipAsyncOperationResolution: true,
|
|
13268
|
+
durableCallReceiptKey: receiptKey,
|
|
13269
|
+
providerIdempotencyReceiptKey: receiptKey,
|
|
13270
|
+
providerIdempotencyKey: receiptKey,
|
|
13271
|
+
});
|
|
13272
|
+
},
|
|
13273
|
+
});
|
|
13274
|
+
if (lifecycle.kind === 'timed_out') {
|
|
13275
|
+
const polling = resolveAsyncOperationPolling({
|
|
13276
|
+
contract: input.contract,
|
|
13277
|
+
startInput: input.startInput,
|
|
13278
|
+
});
|
|
13279
|
+
throw new Error(
|
|
13280
|
+
`Async operation ${input.toolId} (${lifecycle.jobId}) did not reach a terminal state within ${polling.timeoutMs}ms.`,
|
|
13281
|
+
);
|
|
13282
|
+
}
|
|
13283
|
+
if (
|
|
13284
|
+
lifecycle.classification.outcome === 'failed' ||
|
|
13285
|
+
lifecycle.classification.outcome === 'cancelled'
|
|
13286
|
+
) {
|
|
13287
|
+
throw new Error(
|
|
13288
|
+
lifecycle.classification.message ??
|
|
13289
|
+
`Async operation ${input.toolId} (${lifecycle.jobId}) ended ${lifecycle.classification.outcome}.`,
|
|
13290
|
+
);
|
|
13291
|
+
}
|
|
13292
|
+
const materialized = lifecycle.materialized!;
|
|
13293
|
+
const selectedResponse =
|
|
13294
|
+
lifecycle.finishResponse ?? lifecycle.pollResponse;
|
|
13295
|
+
return {
|
|
13296
|
+
...selectedResponse,
|
|
13297
|
+
status:
|
|
13298
|
+
lifecycle.classification.outcome === 'no_result' ||
|
|
13299
|
+
materialized.outcome === 'no_result'
|
|
13300
|
+
? 'no_result'
|
|
13301
|
+
: 'completed',
|
|
13302
|
+
result: { data: materialized.result },
|
|
13303
|
+
toolResponse: materializedAsyncToolResponse(
|
|
13304
|
+
selectedResponse.toolResponse,
|
|
13305
|
+
materialized.result,
|
|
13306
|
+
),
|
|
13307
|
+
};
|
|
13308
|
+
}
|
|
13309
|
+
|
|
13126
13310
|
private async callToolExecutionAPI(
|
|
13127
13311
|
toolId: string,
|
|
13128
13312
|
input: Record<string, unknown>,
|
|
@@ -13152,6 +13336,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13152
13336
|
this.currentAuthoringContractEdition,
|
|
13153
13337
|
);
|
|
13154
13338
|
const provider = toolId.split(/[._]/)[0]?.trim() || 'provider';
|
|
13339
|
+
const asyncContract = options?.skipAsyncOperationResolution
|
|
13340
|
+
? null
|
|
13341
|
+
: ((await this.#options.getToolAsyncOperation?.(toolId)) ?? null);
|
|
13342
|
+
const shouldWaitForAsyncOperation = Boolean(
|
|
13343
|
+
asyncContract && asyncOperationShouldWait(asyncContract, input),
|
|
13344
|
+
);
|
|
13345
|
+
const effectiveInput =
|
|
13346
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
13347
|
+
? asyncOperationDurableStartInput(asyncContract, input)
|
|
13348
|
+
: input;
|
|
13155
13349
|
const activityId = `provider:${toolId}`;
|
|
13156
13350
|
let retryActivityEmitted = false;
|
|
13157
13351
|
let toolCallSucceeded = false;
|
|
@@ -13160,39 +13354,51 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13160
13354
|
// execution. Per-provider rate admission is intentionally deferred until
|
|
13161
13355
|
// each physical fetch is ready to leave this process.
|
|
13162
13356
|
const admissionStartedAt = Date.now();
|
|
13163
|
-
const
|
|
13164
|
-
|
|
13357
|
+
const asyncCapacityRelease =
|
|
13358
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
13359
|
+
? await this.acquireAsyncOperationCapacity({
|
|
13360
|
+
toolId,
|
|
13361
|
+
provider,
|
|
13362
|
+
contract: asyncContract,
|
|
13363
|
+
authScopeDigest:
|
|
13364
|
+
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null,
|
|
13365
|
+
})
|
|
13366
|
+
: null;
|
|
13367
|
+
let toolSlot: RuntimeResourceLease;
|
|
13368
|
+
try {
|
|
13369
|
+
toolSlot = await this.resourceGovernor.acquireTool({
|
|
13165
13370
|
orgId: this.#options.orgId ?? null,
|
|
13166
13371
|
providerResourceKey: `tool:${toolId}`,
|
|
13167
13372
|
toolId,
|
|
13168
|
-
})
|
|
13169
|
-
.catch((error: unknown) => {
|
|
13170
|
-
if (!(error instanceof ProviderExhaustedError)) throw error;
|
|
13171
|
-
const retryAtMs = Date.parse(error.retryAt);
|
|
13172
|
-
throw createToolHttpError(
|
|
13173
|
-
toolErrorSchemaVersion,
|
|
13174
|
-
error.message,
|
|
13175
|
-
null,
|
|
13176
|
-
429,
|
|
13177
|
-
'repairable',
|
|
13178
|
-
{
|
|
13179
|
-
toolId,
|
|
13180
|
-
provider: error.provider,
|
|
13181
|
-
operation: toolId,
|
|
13182
|
-
code: error.code,
|
|
13183
|
-
origin: 'provider',
|
|
13184
|
-
category: 'rate_limit',
|
|
13185
|
-
retryable: true,
|
|
13186
|
-
statusCode: 429,
|
|
13187
|
-
requestId: null,
|
|
13188
|
-
retryAfterMs: Number.isFinite(retryAtMs)
|
|
13189
|
-
? Math.max(0, retryAtMs - Date.now())
|
|
13190
|
-
: null,
|
|
13191
|
-
networkKind: null,
|
|
13192
|
-
networkScope: null,
|
|
13193
|
-
},
|
|
13194
|
-
);
|
|
13195
13373
|
});
|
|
13374
|
+
} catch (error) {
|
|
13375
|
+
asyncCapacityRelease?.();
|
|
13376
|
+
if (!(error instanceof ProviderExhaustedError)) throw error;
|
|
13377
|
+
const retryAtMs = Date.parse(error.retryAt);
|
|
13378
|
+
throw createToolHttpError(
|
|
13379
|
+
toolErrorSchemaVersion,
|
|
13380
|
+
error.message,
|
|
13381
|
+
null,
|
|
13382
|
+
429,
|
|
13383
|
+
'repairable',
|
|
13384
|
+
{
|
|
13385
|
+
toolId,
|
|
13386
|
+
provider: error.provider,
|
|
13387
|
+
operation: toolId,
|
|
13388
|
+
code: error.code,
|
|
13389
|
+
origin: 'provider',
|
|
13390
|
+
category: 'rate_limit',
|
|
13391
|
+
retryable: true,
|
|
13392
|
+
statusCode: 429,
|
|
13393
|
+
requestId: null,
|
|
13394
|
+
retryAfterMs: Number.isFinite(retryAtMs)
|
|
13395
|
+
? Math.max(0, retryAtMs - Date.now())
|
|
13396
|
+
: null,
|
|
13397
|
+
networkKind: null,
|
|
13398
|
+
networkScope: null,
|
|
13399
|
+
},
|
|
13400
|
+
);
|
|
13401
|
+
}
|
|
13196
13402
|
if (runtimeReceiptReadTraceEnabled) {
|
|
13197
13403
|
this.log(
|
|
13198
13404
|
`[perf] tool call id=${toolId} phase=governor_admission elapsed_ms=${Date.now() - admissionStartedAt}`,
|
|
@@ -13222,10 +13428,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13222
13428
|
// Snapshot the caller-controlled payload exactly once. The replay
|
|
13223
13429
|
// decision and every physical attempt must refer to the same bytes,
|
|
13224
13430
|
// even if the original object has getters or is later mutated.
|
|
13225
|
-
const toolInputSnapshot = JSON.parse(
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
>;
|
|
13431
|
+
const toolInputSnapshot = JSON.parse(
|
|
13432
|
+
JSON.stringify(effectiveInput),
|
|
13433
|
+
) as Record<string, unknown>;
|
|
13229
13434
|
const retryPolicy = await this.#options
|
|
13230
13435
|
.getToolRetryPolicy?.(toolId, toolInputSnapshot)
|
|
13231
13436
|
.catch(() => null);
|
|
@@ -13865,7 +14070,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13865
14070
|
);
|
|
13866
14071
|
}
|
|
13867
14072
|
if (failure.shouldRetry) {
|
|
13868
|
-
if (
|
|
14073
|
+
if (
|
|
14074
|
+
failure.reason !== 'gateway_invocation_in_progress' &&
|
|
14075
|
+
failure.reason !== 'concurrency_backpressure'
|
|
14076
|
+
) {
|
|
13869
14077
|
invocationAttempt += 1;
|
|
13870
14078
|
}
|
|
13871
14079
|
if (failure.chargeRetryBudget) {
|
|
@@ -13949,8 +14157,29 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13949
14157
|
? 'array'
|
|
13950
14158
|
: typeof parsed.result,
|
|
13951
14159
|
});
|
|
14160
|
+
const parsedValue =
|
|
14161
|
+
parsed.toolResponse && 'raw' in parsed.toolResponse
|
|
14162
|
+
? parsed.toolResponse.raw
|
|
14163
|
+
: (recordOrNull(parsed.result)?.data ?? parsed.result);
|
|
14164
|
+
const asyncStartPending =
|
|
14165
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
14166
|
+
? classifyAsyncOperationResult(asyncContract, parsedValue)
|
|
14167
|
+
.outcome === 'running'
|
|
14168
|
+
: false;
|
|
14169
|
+
const resolved =
|
|
14170
|
+
asyncContract &&
|
|
14171
|
+
shouldWaitForAsyncOperation &&
|
|
14172
|
+
(parsed.status === 'running' || asyncStartPending)
|
|
14173
|
+
? await this.resolveAsyncOperation({
|
|
14174
|
+
toolId,
|
|
14175
|
+
startInput: input,
|
|
14176
|
+
startResponse: parsed,
|
|
14177
|
+
contract: asyncContract,
|
|
14178
|
+
receiptKey: durableCallReceiptKey,
|
|
14179
|
+
})
|
|
14180
|
+
: parsed;
|
|
13952
14181
|
toolCallSucceeded = true;
|
|
13953
|
-
return
|
|
14182
|
+
return resolved;
|
|
13954
14183
|
}
|
|
13955
14184
|
},
|
|
13956
14185
|
);
|
|
@@ -13977,6 +14206,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13977
14206
|
if (!toolSlotTransferred) {
|
|
13978
14207
|
toolSlot.release();
|
|
13979
14208
|
}
|
|
14209
|
+
asyncCapacityRelease?.();
|
|
13980
14210
|
}
|
|
13981
14211
|
}
|
|
13982
14212
|
|
|
@@ -815,6 +815,13 @@ export interface ContextOptions {
|
|
|
815
815
|
getToolResultMetadata?: (
|
|
816
816
|
toolId: string,
|
|
817
817
|
) => Promise<ToolResultMetadataInput | null> | ToolResultMetadataInput | null;
|
|
818
|
+
/** Canonical provider-job lifecycle executed below scalar and native batch calls. */
|
|
819
|
+
getToolAsyncOperation?: (
|
|
820
|
+
toolId: string,
|
|
821
|
+
) =>
|
|
822
|
+
| Promise<import('./async-operation').AsyncOperationContractWire | null>
|
|
823
|
+
| import('./async-operation').AsyncOperationContractWire
|
|
824
|
+
| null;
|
|
818
825
|
getIntegrationEventWaitHandler?: (
|
|
819
826
|
toolId: string,
|
|
820
827
|
) =>
|
|
@@ -3,6 +3,7 @@ import { bettercontactBatchStrategies } from './bettercontact-batching';
|
|
|
3
3
|
import { DEFAULT_PLAY_RUNTIME_BATCH_STRATEGIES } from './default-batch-strategies';
|
|
4
4
|
import { fullenrichBatchStrategies } from './fullenrich-batching';
|
|
5
5
|
import { opensosdataBatchStrategies } from './opensosdata-batching';
|
|
6
|
+
import { testAsyncBatchStrategies } from './test-async-batching';
|
|
6
7
|
|
|
7
8
|
export const PLAY_RUNTIME_BATCH_OPERATION_REGISTRY: Record<
|
|
8
9
|
string,
|
|
@@ -12,6 +13,7 @@ export const PLAY_RUNTIME_BATCH_OPERATION_REGISTRY: Record<
|
|
|
12
13
|
...bettercontactBatchStrategies,
|
|
13
14
|
...fullenrichBatchStrategies,
|
|
14
15
|
...opensosdataBatchStrategies,
|
|
16
|
+
...testAsyncBatchStrategies,
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export function getPlayRuntimeBatchStrategy(
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineBatchStrategyMap,
|
|
3
|
+
type BatchOperationStrategy,
|
|
4
|
+
} from './batching-types';
|
|
5
|
+
|
|
6
|
+
type TestAsyncLookupPayload = Record<string, unknown> & {
|
|
7
|
+
key: string;
|
|
8
|
+
row_number: number;
|
|
9
|
+
capacity_key: string;
|
|
10
|
+
polls_before_terminal?: number;
|
|
11
|
+
result_page_size?: number;
|
|
12
|
+
pre_attach_delay_ms?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type TestAsyncBatchPayload = Record<string, unknown> & {
|
|
16
|
+
key: string;
|
|
17
|
+
capacity_key: string;
|
|
18
|
+
wait_for_completion: true;
|
|
19
|
+
terminal_outcome: 'completed';
|
|
20
|
+
polls_before_terminal?: number;
|
|
21
|
+
result_page_size?: number;
|
|
22
|
+
pre_attach_delay_ms?: number;
|
|
23
|
+
items: Array<{
|
|
24
|
+
itemKey: string;
|
|
25
|
+
payload: { key: string; row_number: number };
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TestAsyncBatchResult = Record<string, unknown> & {
|
|
30
|
+
items?: Array<{
|
|
31
|
+
itemKey?: string;
|
|
32
|
+
result?: Record<string, unknown>;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function resultItems(
|
|
37
|
+
value: unknown,
|
|
38
|
+
): NonNullable<TestAsyncBatchResult['items']> {
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
|
|
40
|
+
const record = value as Record<string, unknown>;
|
|
41
|
+
if (Array.isArray(record.items)) {
|
|
42
|
+
return record.items as NonNullable<TestAsyncBatchResult['items']>;
|
|
43
|
+
}
|
|
44
|
+
for (const candidate of [record.data, record.result, record.output]) {
|
|
45
|
+
const nested = resultItems(candidate);
|
|
46
|
+
if (nested.length > 0) return nested;
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const testAsyncContractLookupBatchStrategy: BatchOperationStrategy<
|
|
52
|
+
TestAsyncLookupPayload,
|
|
53
|
+
TestAsyncBatchPayload,
|
|
54
|
+
TestAsyncBatchResult,
|
|
55
|
+
Record<string, unknown>
|
|
56
|
+
> = {
|
|
57
|
+
sourceOperation: 'test_async_contract_lookup',
|
|
58
|
+
batchOperation: 'test_async_contract_start',
|
|
59
|
+
kind: 'async_dataset_job',
|
|
60
|
+
maxBatchSize: 3,
|
|
61
|
+
bucketKeyPayloadFields: [
|
|
62
|
+
'capacity_key',
|
|
63
|
+
'polls_before_terminal',
|
|
64
|
+
'result_page_size',
|
|
65
|
+
'pre_attach_delay_ms',
|
|
66
|
+
],
|
|
67
|
+
canBatchWith(left, right) {
|
|
68
|
+
return (
|
|
69
|
+
left.capacity_key === right.capacity_key &&
|
|
70
|
+
left.polls_before_terminal === right.polls_before_terminal &&
|
|
71
|
+
left.result_page_size === right.result_page_size &&
|
|
72
|
+
left.pre_attach_delay_ms === right.pre_attach_delay_ms
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
toBucketKey(payload) {
|
|
76
|
+
return JSON.stringify([
|
|
77
|
+
payload.capacity_key,
|
|
78
|
+
payload.polls_before_terminal ?? null,
|
|
79
|
+
payload.result_page_size ?? null,
|
|
80
|
+
payload.pre_attach_delay_ms ?? null,
|
|
81
|
+
]);
|
|
82
|
+
},
|
|
83
|
+
toItemKey(payload) {
|
|
84
|
+
return payload.key;
|
|
85
|
+
},
|
|
86
|
+
compile(payloads) {
|
|
87
|
+
const first = payloads[0]!;
|
|
88
|
+
const items = payloads.map((payload) => ({
|
|
89
|
+
itemKey: payload.key,
|
|
90
|
+
payload: { key: payload.key, row_number: payload.row_number },
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
batchOperation: 'test_async_contract_start',
|
|
94
|
+
batchPayload: {
|
|
95
|
+
key: `async-contract:${first.capacity_key}:${items.map((item) => item.itemKey).join(',')}`,
|
|
96
|
+
capacity_key: first.capacity_key,
|
|
97
|
+
wait_for_completion: true,
|
|
98
|
+
terminal_outcome: 'completed',
|
|
99
|
+
...(first.polls_before_terminal !== undefined
|
|
100
|
+
? { polls_before_terminal: first.polls_before_terminal }
|
|
101
|
+
: {}),
|
|
102
|
+
...(first.result_page_size !== undefined
|
|
103
|
+
? { result_page_size: first.result_page_size }
|
|
104
|
+
: {}),
|
|
105
|
+
...(first.pre_attach_delay_ms !== undefined
|
|
106
|
+
? { pre_attach_delay_ms: first.pre_attach_delay_ms }
|
|
107
|
+
: {}),
|
|
108
|
+
items,
|
|
109
|
+
},
|
|
110
|
+
items: payloads.map((payload) => ({
|
|
111
|
+
itemKey: payload.key,
|
|
112
|
+
payload,
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
splitResult(fullResult, compiled) {
|
|
117
|
+
const items = resultItems(fullResult);
|
|
118
|
+
if (items.length !== compiled.items.length) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Synthetic async batch returned ${items.length} results for ${compiled.items.length} inputs.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return compiled.items.map((item, index) => {
|
|
124
|
+
const resultItem = items[index];
|
|
125
|
+
if (!resultItem || resultItem.itemKey !== item.itemKey) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Synthetic async batch result ${index} did not preserve item identity ${item.itemKey}.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
itemKey: item.itemKey,
|
|
132
|
+
result: resultItem.result ?? {},
|
|
133
|
+
rawResult: resultItem,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const testAsyncBatchStrategies = defineBatchStrategyMap({
|
|
140
|
+
test_async_contract_lookup: testAsyncContractLookupBatchStrategy,
|
|
141
|
+
});
|