deepline 0.3.64 → 0.3.66
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 +354 -37
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +80 -3
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -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 +81 -20
- package/dist/cli/index.mjs +81 -20
- 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 {
|
|
@@ -193,6 +194,10 @@ import {
|
|
|
193
194
|
type PlayAuthoringRunScope,
|
|
194
195
|
type PlayAuthoringRuntimeContext,
|
|
195
196
|
} from '../plays/authoring-contract';
|
|
197
|
+
import {
|
|
198
|
+
formatCtxFetchHttpFailureDiagnostic,
|
|
199
|
+
tagLogProvenance,
|
|
200
|
+
} from './log-provenance';
|
|
196
201
|
import {
|
|
197
202
|
DURABLE_RECEIPT_WAIT_DELAY_MS,
|
|
198
203
|
DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS,
|
|
@@ -207,6 +212,14 @@ import {
|
|
|
207
212
|
waitForCompletedRuntimeReceipts,
|
|
208
213
|
type DurableReceiptExecutionStore,
|
|
209
214
|
} from './durable-receipt-execution';
|
|
215
|
+
import {
|
|
216
|
+
asyncOperationDurableStartInput,
|
|
217
|
+
asyncOperationShouldWait,
|
|
218
|
+
classifyAsyncOperationResult,
|
|
219
|
+
executeAsyncOperationLifecycle,
|
|
220
|
+
resolveAsyncOperationPolling,
|
|
221
|
+
type AsyncOperationContractWire,
|
|
222
|
+
} from './async-operation';
|
|
210
223
|
import {
|
|
211
224
|
QUERY_RESULT_DATASET_PAGE_SIZE,
|
|
212
225
|
isCustomerDbDatasetTool,
|
|
@@ -442,6 +455,10 @@ const DEFAULT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000 + 30_000;
|
|
|
442
455
|
const FETCH_TRANSPORT_MAX_ATTEMPTS =
|
|
443
456
|
RUNTIME_RELIABILITY_POLICY.egress.fetchMaxAttempts;
|
|
444
457
|
const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
|
|
458
|
+
// Diagnostic logs are retained in the context until terminalization. Keep a
|
|
459
|
+
// representative, deduplicated set so row-heavy continued failures cannot turn
|
|
460
|
+
// customer-safe observability into unbounded runner memory or log traffic.
|
|
461
|
+
const MAX_CTX_FETCH_HTTP_FAILURE_DIAGNOSTICS = 16;
|
|
445
462
|
const CTX_FETCH_HEADERS_TIMEOUT_MS =
|
|
446
463
|
RUNTIME_RELIABILITY_POLICY.egress.fetchHeadersTimeoutMs;
|
|
447
464
|
const CTX_FETCH_BODY_TIMEOUT_MS =
|
|
@@ -1228,6 +1245,27 @@ function publicToolResponseEnvelope(value: unknown): {
|
|
|
1228
1245
|
};
|
|
1229
1246
|
}
|
|
1230
1247
|
|
|
1248
|
+
function materializedAsyncToolResponse(
|
|
1249
|
+
response: ParsedToolExecuteResponse['toolResponse'] | undefined,
|
|
1250
|
+
result: unknown,
|
|
1251
|
+
): ParsedToolExecuteResponse['toolResponse'] {
|
|
1252
|
+
const hasRawV2 =
|
|
1253
|
+
response !== undefined &&
|
|
1254
|
+
Object.prototype.hasOwnProperty.call(response, 'rawV2');
|
|
1255
|
+
const materializedRawV2 =
|
|
1256
|
+
response?.view === 'data'
|
|
1257
|
+
? {
|
|
1258
|
+
...(recordOrNull(response.rawV2) ?? {}),
|
|
1259
|
+
data: result,
|
|
1260
|
+
}
|
|
1261
|
+
: result;
|
|
1262
|
+
return {
|
|
1263
|
+
...(response ?? {}),
|
|
1264
|
+
raw: result,
|
|
1265
|
+
...(hasRawV2 ? { rawV2: materializedRawV2 } : {}),
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1231
1269
|
/**
|
|
1232
1270
|
* A batched provider request returns one envelope for several logical source
|
|
1233
1271
|
* tool calls. Each source call persists only its own item-shaped canonical
|
|
@@ -1384,6 +1422,8 @@ type ToolExecutionApiOptions = {
|
|
|
1384
1422
|
pageSize: number;
|
|
1385
1423
|
totalRows: number;
|
|
1386
1424
|
};
|
|
1425
|
+
/** Internal recursion guard for lifecycle poll/finish calls. */
|
|
1426
|
+
skipAsyncOperationResolution?: boolean;
|
|
1387
1427
|
};
|
|
1388
1428
|
const IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT = 25;
|
|
1389
1429
|
/**
|
|
@@ -2048,9 +2088,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
2048
2088
|
string,
|
|
2049
2089
|
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
|
2050
2090
|
>();
|
|
2091
|
+
private readonly asyncOperationCapacity = new Map<
|
|
2092
|
+
string,
|
|
2093
|
+
{ active: number; waiters: Array<() => void> }
|
|
2094
|
+
>();
|
|
2051
2095
|
#options: ContextOptions;
|
|
2052
2096
|
private readonly executionScope: RunExecutionScope;
|
|
2053
2097
|
private logBuffer: string[] = [];
|
|
2098
|
+
private readonly ctxFetchHttpFailureDiagnosticIdentities = new Set<string>();
|
|
2054
2099
|
private checkpoint: PlayCheckpoint;
|
|
2055
2100
|
private readonly durableCallCacheEpochMs: number;
|
|
2056
2101
|
/**
|
|
@@ -10124,6 +10169,58 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10124
10169
|
if (this.#options.verbose) console.log(line);
|
|
10125
10170
|
}
|
|
10126
10171
|
|
|
10172
|
+
/**
|
|
10173
|
+
* Emit a runtime-authored diagnostic through the same durable log path as
|
|
10174
|
+
* ctx.log without changing the customer-authored log wire format.
|
|
10175
|
+
*/
|
|
10176
|
+
private runtimeDiagnosticLog(message: string): void {
|
|
10177
|
+
assertNoSecretTaint(message, 'runtime diagnostic log');
|
|
10178
|
+
const line = tagLogProvenance(
|
|
10179
|
+
'diagnostic',
|
|
10180
|
+
`[${new Date().toISOString()}] ${this.secretRedactor.redactRegisteredSecrets(message)}`,
|
|
10181
|
+
);
|
|
10182
|
+
this.logBuffer.push(line);
|
|
10183
|
+
this.#options.onLog?.(line);
|
|
10184
|
+
if (this.#options.verbose) console.log(line);
|
|
10185
|
+
}
|
|
10186
|
+
|
|
10187
|
+
/** A URL safe to persist in a customer-visible run log (origin only). */
|
|
10188
|
+
private ctxFetchDiagnosticUrl(url: string): string {
|
|
10189
|
+
try {
|
|
10190
|
+
const parsed = new URL(url);
|
|
10191
|
+
return parsed.origin;
|
|
10192
|
+
} catch {
|
|
10193
|
+
// ctx.fetch already parses its URL before this helper is reachable.
|
|
10194
|
+
return '[invalid-url]';
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
|
|
10198
|
+
private logCtxFetchHttpFailure(input: {
|
|
10199
|
+
key: string;
|
|
10200
|
+
method: string;
|
|
10201
|
+
url: string;
|
|
10202
|
+
httpStatus: number;
|
|
10203
|
+
}): void {
|
|
10204
|
+
const url = this.ctxFetchDiagnosticUrl(input.url);
|
|
10205
|
+
const identity = `${input.key}\u0000${input.method}\u0000${url}\u0000${input.httpStatus}`;
|
|
10206
|
+
if (
|
|
10207
|
+
this.ctxFetchHttpFailureDiagnosticIdentities.has(identity) ||
|
|
10208
|
+
this.ctxFetchHttpFailureDiagnosticIdentities.size >=
|
|
10209
|
+
MAX_CTX_FETCH_HTTP_FAILURE_DIAGNOSTICS
|
|
10210
|
+
) {
|
|
10211
|
+
return;
|
|
10212
|
+
}
|
|
10213
|
+
this.ctxFetchHttpFailureDiagnosticIdentities.add(identity);
|
|
10214
|
+
this.runtimeDiagnosticLog(
|
|
10215
|
+
formatCtxFetchHttpFailureDiagnostic({
|
|
10216
|
+
key: input.key,
|
|
10217
|
+
method: input.method,
|
|
10218
|
+
url,
|
|
10219
|
+
http_status: input.httpStatus,
|
|
10220
|
+
}),
|
|
10221
|
+
);
|
|
10222
|
+
}
|
|
10223
|
+
|
|
10127
10224
|
async sleep(ms: number): Promise<void> {
|
|
10128
10225
|
this.assertInlineChildContract('suspending_child');
|
|
10129
10226
|
const delayMs =
|
|
@@ -10176,6 +10273,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10176
10273
|
|
|
10177
10274
|
const url = input.toString();
|
|
10178
10275
|
const parsedUrl = new URL(url);
|
|
10276
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
10179
10277
|
const urlContainsResolvedSecret =
|
|
10180
10278
|
this.secretRedactor.containsRegisteredSecret(url, {
|
|
10181
10279
|
includeEncoded: true,
|
|
@@ -10267,6 +10365,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10267
10365
|
staleAfterSeconds: options?.staleAfterSeconds,
|
|
10268
10366
|
transient: options?.transient === true,
|
|
10269
10367
|
onRecovered: (output) => {
|
|
10368
|
+
if (!output.ok) {
|
|
10369
|
+
this.logCtxFetchHttpFailure({
|
|
10370
|
+
key: normalizedKey,
|
|
10371
|
+
method,
|
|
10372
|
+
url: output.url || url,
|
|
10373
|
+
httpStatus: output.status,
|
|
10374
|
+
});
|
|
10375
|
+
}
|
|
10270
10376
|
if (!output.ok && this.currentAuthoringContractEdition >= 5) {
|
|
10271
10377
|
throw new CtxFetchHttpError(output);
|
|
10272
10378
|
}
|
|
@@ -10278,7 +10384,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10278
10384
|
this.currentAuthoringContractEdition >= 5
|
|
10279
10385
|
),
|
|
10280
10386
|
execute: async ({ retainExternalCallSlot }) => {
|
|
10281
|
-
const method = (init.method ?? 'GET').toUpperCase();
|
|
10282
10387
|
const secretHeaders = await this.resolveSecretAuth(secretAuth);
|
|
10283
10388
|
const headers: Record<string, string> = {
|
|
10284
10389
|
...normalizeFetchHeaders(init.headers),
|
|
@@ -10313,6 +10418,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10313
10418
|
'output' in existing
|
|
10314
10419
|
) {
|
|
10315
10420
|
this.log(`ctx.fetch(${url}): recovered response from checkpoint`);
|
|
10421
|
+
const checkpointOutput = existing.output as PlayFetchResponse;
|
|
10422
|
+
if (!checkpointOutput.ok) {
|
|
10423
|
+
this.logCtxFetchHttpFailure({
|
|
10424
|
+
key: normalizedKey,
|
|
10425
|
+
method,
|
|
10426
|
+
url: checkpointOutput.url || url,
|
|
10427
|
+
httpStatus: checkpointOutput.status,
|
|
10428
|
+
});
|
|
10429
|
+
}
|
|
10316
10430
|
if (this.durableDirectToolResultsBackedByReceipts) {
|
|
10317
10431
|
// The outer durable receipt is the replay authority in hosted
|
|
10318
10432
|
// runtimes. A legacy checkpoint fetch may seed that receipt
|
|
@@ -10320,7 +10434,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10320
10434
|
// cache for the lifetime of a large map.
|
|
10321
10435
|
delete this.checkpoint.resolvedBoundaries?.[boundaryId];
|
|
10322
10436
|
}
|
|
10323
|
-
return
|
|
10437
|
+
return checkpointOutput;
|
|
10324
10438
|
}
|
|
10325
10439
|
|
|
10326
10440
|
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
@@ -10460,6 +10574,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10460
10574
|
json: this.secretRedactor.redactKnownSecrets(rawJson),
|
|
10461
10575
|
};
|
|
10462
10576
|
|
|
10577
|
+
if (!output.ok) {
|
|
10578
|
+
this.logCtxFetchHttpFailure({
|
|
10579
|
+
key: normalizedKey,
|
|
10580
|
+
method,
|
|
10581
|
+
url: output.url || url,
|
|
10582
|
+
httpStatus: output.status,
|
|
10583
|
+
});
|
|
10584
|
+
}
|
|
10585
|
+
|
|
10463
10586
|
// Edition 5 adopts normal fetch semantics: a non-2xx response is
|
|
10464
10587
|
// a failed durable operation. Throw before checkpoint/receipt
|
|
10465
10588
|
// completion so the failure is never cached. Editions 1–4 retain
|
|
@@ -13123,6 +13246,154 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13123
13246
|
});
|
|
13124
13247
|
}
|
|
13125
13248
|
|
|
13249
|
+
private async acquireAsyncOperationCapacity(input: {
|
|
13250
|
+
toolId: string;
|
|
13251
|
+
provider: string;
|
|
13252
|
+
contract: AsyncOperationContractWire;
|
|
13253
|
+
authScopeDigest: string | null;
|
|
13254
|
+
}): Promise<() => void> {
|
|
13255
|
+
const scopeSuffix =
|
|
13256
|
+
input.contract.capacity.scope === 'organization_action'
|
|
13257
|
+
? input.toolId
|
|
13258
|
+
: input.contract.capacity.scope === 'organization_connection'
|
|
13259
|
+
? (input.authScopeDigest ?? 'default-connection')
|
|
13260
|
+
: input.provider;
|
|
13261
|
+
const key = `${this.#options.orgId ?? 'unknown-org'}:${input.provider}:${input.contract.capacity.scope}:${scopeSuffix}`;
|
|
13262
|
+
let limit = 1;
|
|
13263
|
+
if (input.contract.capacity.limit.kind === 'fixed') {
|
|
13264
|
+
limit = input.contract.capacity.limit.maxConcurrentJobs;
|
|
13265
|
+
} else {
|
|
13266
|
+
const hints =
|
|
13267
|
+
(await this.#options.getToolQueueHints?.(input.toolId)) ?? [];
|
|
13268
|
+
const declaredLimits = hints
|
|
13269
|
+
.map((hint) => hint.maxConcurrency)
|
|
13270
|
+
.filter(
|
|
13271
|
+
(value): value is number =>
|
|
13272
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0,
|
|
13273
|
+
);
|
|
13274
|
+
limit = declaredLimits.length > 0 ? Math.min(...declaredLimits) : 1;
|
|
13275
|
+
}
|
|
13276
|
+
const state = this.asyncOperationCapacity.get(key) ?? {
|
|
13277
|
+
active: 0,
|
|
13278
|
+
waiters: [],
|
|
13279
|
+
};
|
|
13280
|
+
this.asyncOperationCapacity.set(key, state);
|
|
13281
|
+
if (state.active >= limit) {
|
|
13282
|
+
if (input.contract.capacity.onConflict === 'fail') {
|
|
13283
|
+
throw new Error(
|
|
13284
|
+
`Async provider capacity is full for ${input.provider}; the contract allows ${limit} active job(s).`,
|
|
13285
|
+
);
|
|
13286
|
+
}
|
|
13287
|
+
await new Promise<void>((resolve) => state.waiters.push(resolve));
|
|
13288
|
+
}
|
|
13289
|
+
state.active += 1;
|
|
13290
|
+
let released = false;
|
|
13291
|
+
return () => {
|
|
13292
|
+
if (released) return;
|
|
13293
|
+
released = true;
|
|
13294
|
+
state.active = Math.max(0, state.active - 1);
|
|
13295
|
+
state.waiters.shift()?.();
|
|
13296
|
+
if (state.active === 0 && state.waiters.length === 0) {
|
|
13297
|
+
this.asyncOperationCapacity.delete(key);
|
|
13298
|
+
}
|
|
13299
|
+
};
|
|
13300
|
+
}
|
|
13301
|
+
|
|
13302
|
+
private asyncOperationActionReceiptKey(input: {
|
|
13303
|
+
action: string;
|
|
13304
|
+
parentReceiptKey: string | null;
|
|
13305
|
+
jobId: string;
|
|
13306
|
+
phase: string;
|
|
13307
|
+
}): string | null {
|
|
13308
|
+
if (!input.parentReceiptKey) return null;
|
|
13309
|
+
return `${buildDurableToolReceiptPrefix({
|
|
13310
|
+
orgId: this.#options.orgId,
|
|
13311
|
+
toolId: input.action,
|
|
13312
|
+
})}${stableDigest(
|
|
13313
|
+
stableStringify({
|
|
13314
|
+
parentReceiptKey: input.parentReceiptKey,
|
|
13315
|
+
jobId: input.jobId,
|
|
13316
|
+
phase: input.phase,
|
|
13317
|
+
}),
|
|
13318
|
+
)}`;
|
|
13319
|
+
}
|
|
13320
|
+
|
|
13321
|
+
private async resolveAsyncOperation(input: {
|
|
13322
|
+
toolId: string;
|
|
13323
|
+
startInput: Record<string, unknown>;
|
|
13324
|
+
startResponse: ParsedToolExecuteResponse;
|
|
13325
|
+
contract: AsyncOperationContractWire;
|
|
13326
|
+
receiptKey: string | null;
|
|
13327
|
+
}): Promise<ParsedToolExecuteResponse> {
|
|
13328
|
+
const responseValue = (response: ParsedToolExecuteResponse): unknown => {
|
|
13329
|
+
if (response.toolResponse && 'raw' in response.toolResponse) {
|
|
13330
|
+
return response.toolResponse.raw;
|
|
13331
|
+
}
|
|
13332
|
+
const result = recordOrNull(response.result);
|
|
13333
|
+
return result && Object.prototype.hasOwnProperty.call(result, 'data')
|
|
13334
|
+
? result.data
|
|
13335
|
+
: response.result;
|
|
13336
|
+
};
|
|
13337
|
+
const startResult = responseValue(input.startResponse);
|
|
13338
|
+
const lifecycle = await executeAsyncOperationLifecycle({
|
|
13339
|
+
contract: input.contract,
|
|
13340
|
+
startInput: input.startInput,
|
|
13341
|
+
startResult,
|
|
13342
|
+
jobId: input.startResponse.jobId,
|
|
13343
|
+
sleep: async (delayMs) =>
|
|
13344
|
+
await this.sleepWithCheckpointHeartbeat(delayMs),
|
|
13345
|
+
responseValue,
|
|
13346
|
+
executeAction: async (request) => {
|
|
13347
|
+
const receiptKey = this.asyncOperationActionReceiptKey({
|
|
13348
|
+
action: request.action,
|
|
13349
|
+
parentReceiptKey: input.receiptKey,
|
|
13350
|
+
jobId: request.jobId,
|
|
13351
|
+
phase: `${request.phase}:${request.pollAttempt}:${request.page ?? ''}`,
|
|
13352
|
+
});
|
|
13353
|
+
return await this.callToolExecutionAPI(request.action, request.input, {
|
|
13354
|
+
skipAsyncOperationResolution: true,
|
|
13355
|
+
durableCallReceiptKey: receiptKey,
|
|
13356
|
+
providerIdempotencyReceiptKey: receiptKey,
|
|
13357
|
+
providerIdempotencyKey: receiptKey,
|
|
13358
|
+
});
|
|
13359
|
+
},
|
|
13360
|
+
});
|
|
13361
|
+
if (lifecycle.kind === 'timed_out') {
|
|
13362
|
+
const polling = resolveAsyncOperationPolling({
|
|
13363
|
+
contract: input.contract,
|
|
13364
|
+
startInput: input.startInput,
|
|
13365
|
+
});
|
|
13366
|
+
throw new Error(
|
|
13367
|
+
`Async operation ${input.toolId} (${lifecycle.jobId}) did not reach a terminal state within ${polling.timeoutMs}ms.`,
|
|
13368
|
+
);
|
|
13369
|
+
}
|
|
13370
|
+
if (
|
|
13371
|
+
lifecycle.classification.outcome === 'failed' ||
|
|
13372
|
+
lifecycle.classification.outcome === 'cancelled'
|
|
13373
|
+
) {
|
|
13374
|
+
throw new Error(
|
|
13375
|
+
lifecycle.classification.message ??
|
|
13376
|
+
`Async operation ${input.toolId} (${lifecycle.jobId}) ended ${lifecycle.classification.outcome}.`,
|
|
13377
|
+
);
|
|
13378
|
+
}
|
|
13379
|
+
const materialized = lifecycle.materialized!;
|
|
13380
|
+
const selectedResponse =
|
|
13381
|
+
lifecycle.finishResponse ?? lifecycle.pollResponse;
|
|
13382
|
+
return {
|
|
13383
|
+
...selectedResponse,
|
|
13384
|
+
status:
|
|
13385
|
+
lifecycle.classification.outcome === 'no_result' ||
|
|
13386
|
+
materialized.outcome === 'no_result'
|
|
13387
|
+
? 'no_result'
|
|
13388
|
+
: 'completed',
|
|
13389
|
+
result: { data: materialized.result },
|
|
13390
|
+
toolResponse: materializedAsyncToolResponse(
|
|
13391
|
+
selectedResponse.toolResponse,
|
|
13392
|
+
materialized.result,
|
|
13393
|
+
),
|
|
13394
|
+
};
|
|
13395
|
+
}
|
|
13396
|
+
|
|
13126
13397
|
private async callToolExecutionAPI(
|
|
13127
13398
|
toolId: string,
|
|
13128
13399
|
input: Record<string, unknown>,
|
|
@@ -13152,6 +13423,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13152
13423
|
this.currentAuthoringContractEdition,
|
|
13153
13424
|
);
|
|
13154
13425
|
const provider = toolId.split(/[._]/)[0]?.trim() || 'provider';
|
|
13426
|
+
const asyncContract = options?.skipAsyncOperationResolution
|
|
13427
|
+
? null
|
|
13428
|
+
: ((await this.#options.getToolAsyncOperation?.(toolId)) ?? null);
|
|
13429
|
+
const shouldWaitForAsyncOperation = Boolean(
|
|
13430
|
+
asyncContract && asyncOperationShouldWait(asyncContract, input),
|
|
13431
|
+
);
|
|
13432
|
+
const effectiveInput =
|
|
13433
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
13434
|
+
? asyncOperationDurableStartInput(asyncContract, input)
|
|
13435
|
+
: input;
|
|
13155
13436
|
const activityId = `provider:${toolId}`;
|
|
13156
13437
|
let retryActivityEmitted = false;
|
|
13157
13438
|
let toolCallSucceeded = false;
|
|
@@ -13160,39 +13441,51 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13160
13441
|
// execution. Per-provider rate admission is intentionally deferred until
|
|
13161
13442
|
// each physical fetch is ready to leave this process.
|
|
13162
13443
|
const admissionStartedAt = Date.now();
|
|
13163
|
-
const
|
|
13164
|
-
|
|
13444
|
+
const asyncCapacityRelease =
|
|
13445
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
13446
|
+
? await this.acquireAsyncOperationCapacity({
|
|
13447
|
+
toolId,
|
|
13448
|
+
provider,
|
|
13449
|
+
contract: asyncContract,
|
|
13450
|
+
authScopeDigest:
|
|
13451
|
+
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null,
|
|
13452
|
+
})
|
|
13453
|
+
: null;
|
|
13454
|
+
let toolSlot: RuntimeResourceLease;
|
|
13455
|
+
try {
|
|
13456
|
+
toolSlot = await this.resourceGovernor.acquireTool({
|
|
13165
13457
|
orgId: this.#options.orgId ?? null,
|
|
13166
13458
|
providerResourceKey: `tool:${toolId}`,
|
|
13167
13459
|
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
13460
|
});
|
|
13461
|
+
} catch (error) {
|
|
13462
|
+
asyncCapacityRelease?.();
|
|
13463
|
+
if (!(error instanceof ProviderExhaustedError)) throw error;
|
|
13464
|
+
const retryAtMs = Date.parse(error.retryAt);
|
|
13465
|
+
throw createToolHttpError(
|
|
13466
|
+
toolErrorSchemaVersion,
|
|
13467
|
+
error.message,
|
|
13468
|
+
null,
|
|
13469
|
+
429,
|
|
13470
|
+
'repairable',
|
|
13471
|
+
{
|
|
13472
|
+
toolId,
|
|
13473
|
+
provider: error.provider,
|
|
13474
|
+
operation: toolId,
|
|
13475
|
+
code: error.code,
|
|
13476
|
+
origin: 'provider',
|
|
13477
|
+
category: 'rate_limit',
|
|
13478
|
+
retryable: true,
|
|
13479
|
+
statusCode: 429,
|
|
13480
|
+
requestId: null,
|
|
13481
|
+
retryAfterMs: Number.isFinite(retryAtMs)
|
|
13482
|
+
? Math.max(0, retryAtMs - Date.now())
|
|
13483
|
+
: null,
|
|
13484
|
+
networkKind: null,
|
|
13485
|
+
networkScope: null,
|
|
13486
|
+
},
|
|
13487
|
+
);
|
|
13488
|
+
}
|
|
13196
13489
|
if (runtimeReceiptReadTraceEnabled) {
|
|
13197
13490
|
this.log(
|
|
13198
13491
|
`[perf] tool call id=${toolId} phase=governor_admission elapsed_ms=${Date.now() - admissionStartedAt}`,
|
|
@@ -13222,10 +13515,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13222
13515
|
// Snapshot the caller-controlled payload exactly once. The replay
|
|
13223
13516
|
// decision and every physical attempt must refer to the same bytes,
|
|
13224
13517
|
// even if the original object has getters or is later mutated.
|
|
13225
|
-
const toolInputSnapshot = JSON.parse(
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
>;
|
|
13518
|
+
const toolInputSnapshot = JSON.parse(
|
|
13519
|
+
JSON.stringify(effectiveInput),
|
|
13520
|
+
) as Record<string, unknown>;
|
|
13229
13521
|
const retryPolicy = await this.#options
|
|
13230
13522
|
.getToolRetryPolicy?.(toolId, toolInputSnapshot)
|
|
13231
13523
|
.catch(() => null);
|
|
@@ -13865,7 +14157,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13865
14157
|
);
|
|
13866
14158
|
}
|
|
13867
14159
|
if (failure.shouldRetry) {
|
|
13868
|
-
if (
|
|
14160
|
+
if (
|
|
14161
|
+
failure.reason !== 'gateway_invocation_in_progress' &&
|
|
14162
|
+
failure.reason !== 'concurrency_backpressure'
|
|
14163
|
+
) {
|
|
13869
14164
|
invocationAttempt += 1;
|
|
13870
14165
|
}
|
|
13871
14166
|
if (failure.chargeRetryBudget) {
|
|
@@ -13949,8 +14244,29 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13949
14244
|
? 'array'
|
|
13950
14245
|
: typeof parsed.result,
|
|
13951
14246
|
});
|
|
14247
|
+
const parsedValue =
|
|
14248
|
+
parsed.toolResponse && 'raw' in parsed.toolResponse
|
|
14249
|
+
? parsed.toolResponse.raw
|
|
14250
|
+
: (recordOrNull(parsed.result)?.data ?? parsed.result);
|
|
14251
|
+
const asyncStartPending =
|
|
14252
|
+
asyncContract && shouldWaitForAsyncOperation
|
|
14253
|
+
? classifyAsyncOperationResult(asyncContract, parsedValue)
|
|
14254
|
+
.outcome === 'running'
|
|
14255
|
+
: false;
|
|
14256
|
+
const resolved =
|
|
14257
|
+
asyncContract &&
|
|
14258
|
+
shouldWaitForAsyncOperation &&
|
|
14259
|
+
(parsed.status === 'running' || asyncStartPending)
|
|
14260
|
+
? await this.resolveAsyncOperation({
|
|
14261
|
+
toolId,
|
|
14262
|
+
startInput: input,
|
|
14263
|
+
startResponse: parsed,
|
|
14264
|
+
contract: asyncContract,
|
|
14265
|
+
receiptKey: durableCallReceiptKey,
|
|
14266
|
+
})
|
|
14267
|
+
: parsed;
|
|
13952
14268
|
toolCallSucceeded = true;
|
|
13953
|
-
return
|
|
14269
|
+
return resolved;
|
|
13954
14270
|
}
|
|
13955
14271
|
},
|
|
13956
14272
|
);
|
|
@@ -13977,6 +14293,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13977
14293
|
if (!toolSlotTransferred) {
|
|
13978
14294
|
toolSlot.release();
|
|
13979
14295
|
}
|
|
14296
|
+
asyncCapacityRelease?.();
|
|
13980
14297
|
}
|
|
13981
14298
|
}
|
|
13982
14299
|
|
|
@@ -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
|
) =>
|
|
@@ -125,6 +125,85 @@ export function tagLogProvenance(
|
|
|
125
125
|
return `${PROVENANCE_PREFIX}${provenance}${PROVENANCE_SENTINEL}${line}`;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A customer-safe record of a `ctx.fetch` response that reached the server but
|
|
130
|
+
* was not successful. Its URL is destination-origin-only; it deliberately
|
|
131
|
+
* omits paths, query strings, request/response bodies, headers, receipt ids,
|
|
132
|
+
* and row identity: all of those can contain customer data or credentials.
|
|
133
|
+
* This one stable shape is shared by the
|
|
134
|
+
* runtime (emission), finalization (terminal warning), and CLI/tests
|
|
135
|
+
* (inspection), rather than each layer trying to infer an HTTP failure from
|
|
136
|
+
* arbitrary user logs.
|
|
137
|
+
*/
|
|
138
|
+
export type CtxFetchHttpFailureDiagnostic = {
|
|
139
|
+
key: string;
|
|
140
|
+
method: string;
|
|
141
|
+
url: string;
|
|
142
|
+
http_status: number;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Guard terminal-transport diagnostics before they reach customer surfaces. */
|
|
146
|
+
export function isCtxFetchHttpFailureDiagnostic(
|
|
147
|
+
value: unknown,
|
|
148
|
+
): value is CtxFetchHttpFailureDiagnostic {
|
|
149
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const record = value as Record<string, unknown>;
|
|
153
|
+
return (
|
|
154
|
+
typeof record.key === 'string' &&
|
|
155
|
+
typeof record.method === 'string' &&
|
|
156
|
+
typeof record.url === 'string' &&
|
|
157
|
+
typeof record.http_status === 'number' &&
|
|
158
|
+
Number.isInteger(record.http_status)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const CTX_FETCH_HTTP_FAILURE_LOG_PREFIX =
|
|
163
|
+
'[runtime.ctx_fetch_http_failure]';
|
|
164
|
+
|
|
165
|
+
/** Format the canonical durable log line for an observed non-2xx ctx.fetch. */
|
|
166
|
+
export function formatCtxFetchHttpFailureDiagnostic(
|
|
167
|
+
diagnostic: CtxFetchHttpFailureDiagnostic,
|
|
168
|
+
): string {
|
|
169
|
+
return `${CTX_FETCH_HTTP_FAILURE_LOG_PREFIX} ${JSON.stringify(diagnostic)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse only runtime-authored, canonical ctx.fetch diagnostics. Untagged
|
|
174
|
+
* legacy/user lines are never interpreted as evidence, even if they happen to
|
|
175
|
+
* contain the same words.
|
|
176
|
+
*/
|
|
177
|
+
export function parseCtxFetchHttpFailureDiagnostic(
|
|
178
|
+
rawLine: string,
|
|
179
|
+
): CtxFetchHttpFailureDiagnostic | null {
|
|
180
|
+
const tagged = readProvenanceTag(rawLine);
|
|
181
|
+
if (tagged.provenance !== 'diagnostic') return null;
|
|
182
|
+
const index = tagged.line.indexOf(CTX_FETCH_HTTP_FAILURE_LOG_PREFIX);
|
|
183
|
+
if (index === -1) return null;
|
|
184
|
+
const json = tagged.line
|
|
185
|
+
.slice(index + CTX_FETCH_HTTP_FAILURE_LOG_PREFIX.length)
|
|
186
|
+
.trim();
|
|
187
|
+
try {
|
|
188
|
+
const parsed: unknown = JSON.parse(json);
|
|
189
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const record = parsed as Record<string, unknown>;
|
|
193
|
+
if (!isCtxFetchHttpFailureDiagnostic(record)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
key: record.key,
|
|
198
|
+
method: record.method,
|
|
199
|
+
url: record.url,
|
|
200
|
+
http_status: record.http_status,
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
128
207
|
/**
|
|
129
208
|
* Read a structural provenance tag off a line, if present, and return the tag
|
|
130
209
|
* plus the original untagged line. Returns `null` provenance when untagged.
|
|
@@ -141,9 +220,7 @@ export function readProvenanceTag(line: string): {
|
|
|
141
220
|
return { provenance: null, line };
|
|
142
221
|
}
|
|
143
222
|
const candidate = line.slice(PROVENANCE_PREFIX.length, end);
|
|
144
|
-
const provenance = LOG_PROVENANCE_CLASSES.includes(
|
|
145
|
-
candidate as LogProvenance,
|
|
146
|
-
)
|
|
223
|
+
const provenance = LOG_PROVENANCE_CLASSES.includes(candidate as LogProvenance)
|
|
147
224
|
? (candidate as LogProvenance)
|
|
148
225
|
: null;
|
|
149
226
|
return { provenance, line: line.slice(end + 1) };
|
|
@@ -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(
|
|
@@ -17,6 +17,7 @@ import type { PlayRunFailureDetails } from './run-failure';
|
|
|
17
17
|
import type { ToolExecutionErrorSchemaVersion } from '../plays/tool-execution-error';
|
|
18
18
|
import type { ToolResponseContract } from '../plays/tool-response-contract';
|
|
19
19
|
import type { FixtureBehavior } from './fixture-behavior';
|
|
20
|
+
import type { CtxFetchHttpFailureDiagnostic } from './log-provenance';
|
|
20
21
|
|
|
21
22
|
export type PlayRunnerRateStateBackendConfig =
|
|
22
23
|
| {
|
|
@@ -307,6 +308,8 @@ export type PlayRunnerResult =
|
|
|
307
308
|
status: 'completed';
|
|
308
309
|
output: unknown;
|
|
309
310
|
outputWarnings?: PlayRunOutputWarning[];
|
|
311
|
+
/** Customer-safe runtime observations that must survive bounded log tails. */
|
|
312
|
+
runtimeDiagnostics?: CtxFetchHttpFailureDiagnostic[];
|
|
310
313
|
outputRowCount?: number;
|
|
311
314
|
logs: string[];
|
|
312
315
|
stats: Record<string, unknown>;
|