deepline 0.2.9 → 0.2.10
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/activity-observation.ts +22 -5
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +23 -20
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +470 -258
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +52 -22
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +38 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +19 -6
- package/dist/cli/index.js +12 -5
- package/dist/cli/index.mjs +12 -5
- package/dist/index.js +12 -5
- package/dist/index.mjs +12 -5
- package/package.json +1 -1
|
@@ -1331,6 +1331,8 @@ export class PlayContextImpl {
|
|
|
1331
1331
|
#options: ContextOptions;
|
|
1332
1332
|
private readonly executionScope: RunExecutionScope;
|
|
1333
1333
|
private logBuffer: string[] = [];
|
|
1334
|
+
private fixtureProviderPacingBypassLogged = false;
|
|
1335
|
+
private fixtureProviderPacingEnforcementLogged = false;
|
|
1334
1336
|
private checkpoint: PlayCheckpoint;
|
|
1335
1337
|
/**
|
|
1336
1338
|
* Durable tool receipts are the replay/cache authority for the execution
|
|
@@ -4329,6 +4331,18 @@ export class PlayContextImpl {
|
|
|
4329
4331
|
// streaming and sheet-backed.
|
|
4330
4332
|
const materializedResultRowLimit = resolveMaterializeLimitCap();
|
|
4331
4333
|
const immediateMaterializedRows: Record<string, unknown>[] = [];
|
|
4334
|
+
let immediateMaterializedResidentBytes = 0;
|
|
4335
|
+
let immediateMaterializedCacheEnabled = true;
|
|
4336
|
+
|
|
4337
|
+
const disableImmediateMaterializedCache = (reason: string) => {
|
|
4338
|
+
if (!immediateMaterializedCacheEnabled) return;
|
|
4339
|
+
immediateMaterializedRows.length = 0;
|
|
4340
|
+
immediateMaterializedResidentBytes = 0;
|
|
4341
|
+
immediateMaterializedCacheEnabled = false;
|
|
4342
|
+
this.log(
|
|
4343
|
+
`Dataset ${normalizedMapNamespace} result cache disabled (${reason}); using the durable runtime sheet.`,
|
|
4344
|
+
);
|
|
4345
|
+
};
|
|
4332
4346
|
|
|
4333
4347
|
const persistMapRows = async (rows: PersistableMapRow[]) => {
|
|
4334
4348
|
if (!this.#options.onMapRowsCompleted || rows.length === 0) {
|
|
@@ -4630,8 +4644,47 @@ export class PlayContextImpl {
|
|
|
4630
4644
|
for (const row of mapResult.completedRows) {
|
|
4631
4645
|
const materializedRow = this.toMaterializedOutputRow(row.data);
|
|
4632
4646
|
if (previewRows.length < 5) previewRows.push(materializedRow);
|
|
4633
|
-
if (
|
|
4634
|
-
|
|
4647
|
+
if (
|
|
4648
|
+
immediateMaterializedCacheEnabled &&
|
|
4649
|
+
immediateMaterializedRows.length < materializedResultRowLimit
|
|
4650
|
+
) {
|
|
4651
|
+
const estimatedRowResidentBytes =
|
|
4652
|
+
runtimeMapJsonByteLength(materializedRow) *
|
|
4653
|
+
NODE_RUNTIME_MAP_MATERIALIZED_MEMORY_MULTIPLIER +
|
|
4654
|
+
NODE_RUNTIME_MAP_ROW_OVERHEAD_BYTES;
|
|
4655
|
+
const observedRowCount = immediateMaterializedRows.length + 1;
|
|
4656
|
+
const observedResidentBytes =
|
|
4657
|
+
immediateMaterializedResidentBytes + estimatedRowResidentBytes;
|
|
4658
|
+
// `net_new` deliberately reports only admitted rows, so its
|
|
4659
|
+
// running total cannot predict the next page. Do not pre-scan an
|
|
4660
|
+
// unknown-count dataset just to estimate this cache. Instead,
|
|
4661
|
+
// bound the cache and active source page together while that page
|
|
4662
|
+
// remains resident.
|
|
4663
|
+
const projectedResidentBytes =
|
|
4664
|
+
options?.mode === 'net_new'
|
|
4665
|
+
? observedResidentBytes +
|
|
4666
|
+
pageMemoryEstimate.estimatedResidentBytes
|
|
4667
|
+
: Math.ceil(
|
|
4668
|
+
(observedResidentBytes / observedRowCount) *
|
|
4669
|
+
Math.max(observedRowCount, totalInputCount - failedCount),
|
|
4670
|
+
);
|
|
4671
|
+
if (
|
|
4672
|
+
projectedResidentBytes > mapMemoryLimits.materializedBudgetBytes
|
|
4673
|
+
) {
|
|
4674
|
+
// All-or-nothing cache: retaining a prefix that cannot fit once
|
|
4675
|
+
// the known input finishes gives authored code no complete fast
|
|
4676
|
+
// path. More importantly, that prefix would overlap the next
|
|
4677
|
+
// active page and can exhaust a fixed-size sandbox before the
|
|
4678
|
+
// cache reaches its own independent budget. Project from the
|
|
4679
|
+
// rows observed so far and move to the already-durable sheet
|
|
4680
|
+
// early instead of waiting for resident memory to reach a cliff.
|
|
4681
|
+
disableImmediateMaterializedCache(
|
|
4682
|
+
`projected ${projectedResidentBytes} bytes exceeds ${mapMemoryLimits.materializedBudgetBytes}-byte budget`,
|
|
4683
|
+
);
|
|
4684
|
+
} else {
|
|
4685
|
+
immediateMaterializedRows.push(materializedRow);
|
|
4686
|
+
immediateMaterializedResidentBytes = observedResidentBytes;
|
|
4687
|
+
}
|
|
4635
4688
|
}
|
|
4636
4689
|
}
|
|
4637
4690
|
executedCount += rowsToExecute.length;
|
|
@@ -4738,6 +4791,7 @@ export class PlayContextImpl {
|
|
|
4738
4791
|
materializedResultRowLimit,
|
|
4739
4792
|
);
|
|
4740
4793
|
if (
|
|
4794
|
+
immediateMaterializedCacheEnabled &&
|
|
4741
4795
|
immediateMaterializedRows.length === availableResultRows &&
|
|
4742
4796
|
(limit !== undefined || successfulCount <= materializedResultRowLimit)
|
|
4743
4797
|
) {
|
|
@@ -4808,6 +4862,7 @@ export class PlayContextImpl {
|
|
|
4808
4862
|
},
|
|
4809
4863
|
previewRows,
|
|
4810
4864
|
residentRows:
|
|
4865
|
+
immediateMaterializedCacheEnabled &&
|
|
4811
4866
|
immediateMaterializedRows.length === successfulCount
|
|
4812
4867
|
? immediateMaterializedRows
|
|
4813
4868
|
: null,
|
|
@@ -4828,8 +4883,9 @@ export class PlayContextImpl {
|
|
|
4828
4883
|
peek: async (limit) =>
|
|
4829
4884
|
limit <= 0
|
|
4830
4885
|
? []
|
|
4831
|
-
:
|
|
4832
|
-
|
|
4886
|
+
: immediateMaterializedCacheEnabled &&
|
|
4887
|
+
immediateMaterializedRows.length >=
|
|
4888
|
+
Math.min(limit, successfulCount)
|
|
4833
4889
|
? immediateMaterializedRows.slice(0, limit)
|
|
4834
4890
|
: await readRuntimeBackedMapRows({
|
|
4835
4891
|
limit,
|
|
@@ -4840,7 +4896,10 @@ export class PlayContextImpl {
|
|
|
4840
4896
|
iterate: () =>
|
|
4841
4897
|
({
|
|
4842
4898
|
async *[Symbol.asyncIterator]() {
|
|
4843
|
-
if (
|
|
4899
|
+
if (
|
|
4900
|
+
immediateMaterializedCacheEnabled &&
|
|
4901
|
+
immediateMaterializedRows.length === successfulCount
|
|
4902
|
+
) {
|
|
4844
4903
|
for (const row of immediateMaterializedRows) yield row;
|
|
4845
4904
|
return;
|
|
4846
4905
|
}
|
|
@@ -7945,6 +8004,113 @@ export class PlayContextImpl {
|
|
|
7945
8004
|
|
|
7946
8005
|
// ——— Batched tool call execution ———
|
|
7947
8006
|
|
|
8007
|
+
private startPendingToolReceiptHeartbeat(requests: ToolCallRequest[]): {
|
|
8008
|
+
stop: () => void;
|
|
8009
|
+
leaseLost: () => unknown | null;
|
|
8010
|
+
} | null {
|
|
8011
|
+
const heartbeatReceipts = this.#options.heartbeatRuntimeStepReceipts;
|
|
8012
|
+
const ownedRequests = requests.filter(
|
|
8013
|
+
(request) => request.receiptKey && request.receiptLeaseId,
|
|
8014
|
+
);
|
|
8015
|
+
if (!heartbeatReceipts || ownedRequests.length === 0) return null;
|
|
8016
|
+
|
|
8017
|
+
const earliestLeaseExpiry = ownedRequests.reduce<string | null>(
|
|
8018
|
+
(earliest, request) => {
|
|
8019
|
+
const expiresAt = request.receiptLeaseExpiresAt ?? null;
|
|
8020
|
+
if (!expiresAt) return earliest;
|
|
8021
|
+
if (!earliest) return expiresAt;
|
|
8022
|
+
return Date.parse(expiresAt) < Date.parse(earliest)
|
|
8023
|
+
? expiresAt
|
|
8024
|
+
: earliest;
|
|
8025
|
+
},
|
|
8026
|
+
null,
|
|
8027
|
+
);
|
|
8028
|
+
let leaseLost: unknown | null = null;
|
|
8029
|
+
const supervisor = createRuntimeReceiptHeartbeatSupervisor({
|
|
8030
|
+
intervalMs: runtimeLeaseHeartbeatIntervalFromExpiry({
|
|
8031
|
+
leaseExpiresAt: earliestLeaseExpiry,
|
|
8032
|
+
fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
8033
|
+
}),
|
|
8034
|
+
heartbeat: async () => {
|
|
8035
|
+
// Receipt claims are acquired in bulk before the provider-shaped
|
|
8036
|
+
// dispatcher admits every call. Renew the undispatched tail as well as
|
|
8037
|
+
// in-flight calls; otherwise a slow provider can let later claims
|
|
8038
|
+
// expire while they are still waiting behind the concurrency gate.
|
|
8039
|
+
const unsettled = ownedRequests.filter((request) =>
|
|
8040
|
+
this.toolCallResolvers.has(request.callId),
|
|
8041
|
+
);
|
|
8042
|
+
if (unsettled.length === 0) return 'terminal';
|
|
8043
|
+
const byLeaseId = new Map<string, Map<string, ToolCallRequest[]>>();
|
|
8044
|
+
for (const request of unsettled) {
|
|
8045
|
+
const leaseId = request.receiptLeaseId!;
|
|
8046
|
+
const byReceiptKey = byLeaseId.get(leaseId) ?? new Map();
|
|
8047
|
+
const receiptKey = request.receiptKey!;
|
|
8048
|
+
const group = byReceiptKey.get(receiptKey) ?? [];
|
|
8049
|
+
group.push(request);
|
|
8050
|
+
byReceiptKey.set(receiptKey, group);
|
|
8051
|
+
byLeaseId.set(leaseId, byReceiptKey);
|
|
8052
|
+
}
|
|
8053
|
+
await Promise.all(
|
|
8054
|
+
[...byLeaseId].map(async ([leaseId, byReceiptKey]) => {
|
|
8055
|
+
// One content-addressed receipt can have many same-run followers.
|
|
8056
|
+
// The store renews unique receipt rows, not request positions, so
|
|
8057
|
+
// duplicate keys in one heartbeat would yield null duplicate
|
|
8058
|
+
// positions and falsely stop the supervisor as if ownership were
|
|
8059
|
+
// lost.
|
|
8060
|
+
const entries = [...byReceiptKey];
|
|
8061
|
+
const keys = entries.map(([receiptKey]) => receiptKey);
|
|
8062
|
+
const receipts = await heartbeatReceipts({
|
|
8063
|
+
runId: this.currentReceiptOwnerRunId,
|
|
8064
|
+
runAttempt: this.currentRunAttempt,
|
|
8065
|
+
leaseId,
|
|
8066
|
+
keys,
|
|
8067
|
+
});
|
|
8068
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
8069
|
+
const [receiptKey, requests] = entries[index]!;
|
|
8070
|
+
// Completion can race this bulk heartbeat response. A settled
|
|
8071
|
+
// receipt group no longer needs ownership and must not turn that
|
|
8072
|
+
// race into a false lease-loss failure.
|
|
8073
|
+
if (
|
|
8074
|
+
!requests.some((request) =>
|
|
8075
|
+
this.toolCallResolvers.has(request.callId),
|
|
8076
|
+
)
|
|
8077
|
+
) {
|
|
8078
|
+
continue;
|
|
8079
|
+
}
|
|
8080
|
+
if (
|
|
8081
|
+
!this.runtimeToolReceiptStillOwned(
|
|
8082
|
+
receipts[index] ?? null,
|
|
8083
|
+
leaseId,
|
|
8084
|
+
)
|
|
8085
|
+
) {
|
|
8086
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
8087
|
+
receiptKey,
|
|
8088
|
+
runId: this.currentReceiptOwnerRunId,
|
|
8089
|
+
leaseId,
|
|
8090
|
+
});
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
}),
|
|
8094
|
+
);
|
|
8095
|
+
return 'active';
|
|
8096
|
+
},
|
|
8097
|
+
isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
|
|
8098
|
+
onLeaseLost: (error) => {
|
|
8099
|
+
leaseLost ??= error;
|
|
8100
|
+
},
|
|
8101
|
+
onTransientFailure: (error) => {
|
|
8102
|
+
this.log(
|
|
8103
|
+
`Pending tool receipt heartbeat transport failed; retrying: ${error instanceof Error ? error.message : String(error)}`,
|
|
8104
|
+
);
|
|
8105
|
+
},
|
|
8106
|
+
});
|
|
8107
|
+
supervisor.start();
|
|
8108
|
+
return {
|
|
8109
|
+
stop: () => supervisor.stop(),
|
|
8110
|
+
leaseLost: () => leaseLost,
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
|
|
7948
8114
|
private async executeBatchedToolCalls(
|
|
7949
8115
|
queuedToolCalls: ToolCallRequest[],
|
|
7950
8116
|
): Promise<void> {
|
|
@@ -8615,277 +8781,290 @@ export class PlayContextImpl {
|
|
|
8615
8781
|
recordToolStep(durableRecoveredRequests);
|
|
8616
8782
|
}
|
|
8617
8783
|
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8784
|
+
const pendingReceiptHeartbeat =
|
|
8785
|
+
this.startPendingToolReceiptHeartbeat(pendingRequests);
|
|
8786
|
+
try {
|
|
8787
|
+
if (pendingRequests.length > 0) {
|
|
8788
|
+
const strategy =
|
|
8789
|
+
this.#options.getBatchOperationStrategy?.(toolId) ?? null;
|
|
8790
|
+
|
|
8791
|
+
if (strategy) {
|
|
8792
|
+
const compiledBatches = compileRequestsWithStrategy({
|
|
8793
|
+
requests: pendingRequests,
|
|
8794
|
+
strategy,
|
|
8795
|
+
getPayload: (request: ToolCallRequest) => request.input,
|
|
8796
|
+
});
|
|
8628
8797
|
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
this.persistenceLatch.
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8798
|
+
const batchSize =
|
|
8799
|
+
compiledBatches.length > 0
|
|
8800
|
+
? await this.resourceGovernor.suggestedToolParallelism(
|
|
8801
|
+
compiledBatches[0]!.batchOperation,
|
|
8802
|
+
this.governor.policy.pacing
|
|
8803
|
+
.workerToolBatchDefaultParallelism,
|
|
8804
|
+
)
|
|
8805
|
+
: this.governor.policy.pacing
|
|
8806
|
+
.workerToolBatchDefaultParallelism;
|
|
8807
|
+
await executeChunkedRequests({
|
|
8808
|
+
requests: compiledBatches,
|
|
8809
|
+
batchSize,
|
|
8810
|
+
execute: async (batch) => {
|
|
8811
|
+
// Circuit breaker: skip dispatching this batch's provider call
|
|
8812
|
+
// once a persistence failure has occurred in this run.
|
|
8813
|
+
if (this.persistenceLatch.tripped) {
|
|
8814
|
+
this.persistenceLatch.preventedCallCount +=
|
|
8815
|
+
batch.memberRequests.length;
|
|
8816
|
+
throw new RuntimePersistenceCircuitOpenError(
|
|
8817
|
+
this.persistenceLatch,
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
const receiptKeys = batch.memberRequests.map(
|
|
8821
|
+
(request) => request.cacheKey,
|
|
8648
8822
|
);
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
prefix: 'batch',
|
|
8657
|
-
aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
|
|
8658
|
-
orgId: this.#options.orgId,
|
|
8659
|
-
toolId: batch.batchOperation,
|
|
8660
|
-
}),
|
|
8661
|
-
},
|
|
8662
|
-
);
|
|
8663
|
-
return await this.callToolAPI(
|
|
8664
|
-
batch.batchOperation,
|
|
8665
|
-
batch.batchPayload,
|
|
8666
|
-
{
|
|
8667
|
-
durableCallReceiptKey: aggregateReceiptKey,
|
|
8668
|
-
executionAuthScopeDigest:
|
|
8669
|
-
batch.memberRequests[0]?.executionAuthScopeDigest ?? null,
|
|
8670
|
-
providerIdempotencyKey:
|
|
8671
|
-
buildDurableToolAggregateProviderIdempotencyKey({
|
|
8672
|
-
aggregateReceiptKey,
|
|
8673
|
-
receiptKeys,
|
|
8674
|
-
providerIdempotencyKeys: batch.memberRequests.map(
|
|
8675
|
-
(request) =>
|
|
8676
|
-
this.providerIdempotencyKeyForToolCall({
|
|
8677
|
-
cacheKey: request.cacheKey,
|
|
8678
|
-
force: request.force === true,
|
|
8679
|
-
leaseId: request.receiptLeaseId,
|
|
8680
|
-
}),
|
|
8681
|
-
),
|
|
8823
|
+
const aggregateReceiptKey =
|
|
8824
|
+
buildDurableToolAggregateReceiptKey({
|
|
8825
|
+
receiptKeys,
|
|
8826
|
+
prefix: 'batch',
|
|
8827
|
+
aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
|
|
8828
|
+
orgId: this.#options.orgId,
|
|
8829
|
+
toolId: batch.batchOperation,
|
|
8682
8830
|
}),
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8831
|
+
});
|
|
8832
|
+
return await this.callToolAPI(
|
|
8833
|
+
batch.batchOperation,
|
|
8834
|
+
batch.batchPayload,
|
|
8835
|
+
{
|
|
8836
|
+
durableCallReceiptKey: aggregateReceiptKey,
|
|
8837
|
+
executionAuthScopeDigest:
|
|
8838
|
+
batch.memberRequests[0]?.executionAuthScopeDigest ??
|
|
8839
|
+
null,
|
|
8840
|
+
providerIdempotencyKey:
|
|
8841
|
+
buildDurableToolAggregateProviderIdempotencyKey({
|
|
8842
|
+
aggregateReceiptKey,
|
|
8843
|
+
receiptKeys,
|
|
8844
|
+
providerIdempotencyKeys: batch.memberRequests.map(
|
|
8845
|
+
(request) =>
|
|
8846
|
+
this.providerIdempotencyKeyForToolCall({
|
|
8847
|
+
cacheKey: request.cacheKey,
|
|
8848
|
+
force: request.force === true,
|
|
8849
|
+
leaseId: request.receiptLeaseId,
|
|
8850
|
+
}),
|
|
8851
|
+
),
|
|
8852
|
+
}),
|
|
8853
|
+
receiptLeaseExpiresAt: batch.memberRequests.reduce<
|
|
8854
|
+
string | null
|
|
8855
|
+
>((earliest, request) => {
|
|
8856
|
+
const expiresAt = request.receiptLeaseExpiresAt ?? null;
|
|
8857
|
+
if (!expiresAt) return earliest;
|
|
8858
|
+
if (!earliest) return expiresAt;
|
|
8859
|
+
return Date.parse(expiresAt) < Date.parse(earliest)
|
|
8860
|
+
? expiresAt
|
|
8861
|
+
: earliest;
|
|
8862
|
+
}, null),
|
|
8863
|
+
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
|
|
8702
8864
|
batch.memberRequests,
|
|
8703
8865
|
),
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8866
|
+
beforeProviderCall: () =>
|
|
8867
|
+
this.assertRuntimeToolReceiptOwnership(
|
|
8868
|
+
batch.memberRequests,
|
|
8869
|
+
),
|
|
8870
|
+
heartbeatReceipt: () =>
|
|
8871
|
+
this.renewRuntimeToolReceiptOwnership(
|
|
8872
|
+
batch.memberRequests,
|
|
8873
|
+
),
|
|
8874
|
+
},
|
|
8875
|
+
);
|
|
8876
|
+
},
|
|
8877
|
+
onChunkComplete: async (chunkResults) => {
|
|
8878
|
+
for (const entry of chunkResults) {
|
|
8879
|
+
if (entry.error !== undefined) {
|
|
8880
|
+
for (const request of entry.request.memberRequests) {
|
|
8881
|
+
await rejectWithLiveFollowers(request, entry.error);
|
|
8882
|
+
}
|
|
8883
|
+
continue;
|
|
8884
|
+
}
|
|
8885
|
+
const splitResults =
|
|
8886
|
+
entry.result != null
|
|
8887
|
+
? entry.request.splitResults(entry.result)
|
|
8888
|
+
: entry.request.memberRequests.map(() => null);
|
|
8889
|
+
const resolvedResults =
|
|
8890
|
+
await this.resolveToolCallBatchResults(
|
|
8891
|
+
toolId,
|
|
8892
|
+
entry.request.memberRequests.map((request, index) => ({
|
|
8893
|
+
request,
|
|
8894
|
+
result: splitResults[index] ?? null,
|
|
8895
|
+
})),
|
|
8896
|
+
);
|
|
8897
|
+
|
|
8898
|
+
for (
|
|
8899
|
+
let index = 0;
|
|
8900
|
+
index < entry.request.memberRequests.length;
|
|
8901
|
+
index += 1
|
|
8902
|
+
) {
|
|
8903
|
+
const request = entry.request.memberRequests[index]!;
|
|
8904
|
+
if (resolvedResults[index] != null) {
|
|
8905
|
+
successfulLiveStepCallIds.add(request.callId);
|
|
8906
|
+
}
|
|
8907
|
+
resolveLiveFollowers(request, resolvedResults[index]);
|
|
8712
8908
|
}
|
|
8713
|
-
continue;
|
|
8714
8909
|
}
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8910
|
+
|
|
8911
|
+
recordToolStep(
|
|
8912
|
+
chunkResults.flatMap(
|
|
8913
|
+
(entry) => entry.request.memberRequests,
|
|
8914
|
+
),
|
|
8915
|
+
);
|
|
8916
|
+
this.#options.onBatchComplete?.(this.checkpoint);
|
|
8917
|
+
},
|
|
8918
|
+
});
|
|
8919
|
+
} else {
|
|
8920
|
+
const completionBuffer: Array<{
|
|
8921
|
+
request: ToolCallRequest;
|
|
8922
|
+
result: unknown | null;
|
|
8923
|
+
metadata?: ToolResultMetadataInput | null;
|
|
8924
|
+
jobId?: string;
|
|
8925
|
+
meta?: Record<string, unknown>;
|
|
8926
|
+
resolve: (value: unknown) => void;
|
|
8927
|
+
reject: (error: unknown) => void;
|
|
8928
|
+
}> = [];
|
|
8929
|
+
const completionFailedCallIds = new Set<string>();
|
|
8930
|
+
let completionFlushScheduled = false;
|
|
8931
|
+
const flushCompletionBuffer = async (): Promise<void> => {
|
|
8932
|
+
const entries = completionBuffer.splice(0);
|
|
8933
|
+
if (entries.length === 0) return;
|
|
8934
|
+
try {
|
|
8719
8935
|
const resolvedResults =
|
|
8720
8936
|
await this.resolveToolCallBatchResults(
|
|
8721
8937
|
toolId,
|
|
8722
|
-
|
|
8723
|
-
request,
|
|
8724
|
-
result:
|
|
8938
|
+
entries.map((entry) => ({
|
|
8939
|
+
request: entry.request,
|
|
8940
|
+
result: entry.result,
|
|
8941
|
+
metadata: entry.metadata,
|
|
8942
|
+
jobId: entry.jobId,
|
|
8943
|
+
meta: entry.meta,
|
|
8725
8944
|
})),
|
|
8726
8945
|
);
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
) {
|
|
8733
|
-
const request = entry.request.memberRequests[index]!;
|
|
8734
|
-
if (resolvedResults[index] != null) {
|
|
8735
|
-
successfulLiveStepCallIds.add(request.callId);
|
|
8946
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
8947
|
+
const entry = entries[index]!;
|
|
8948
|
+
const result = resolvedResults[index];
|
|
8949
|
+
if (result != null) {
|
|
8950
|
+
successfulLiveStepCallIds.add(entry.request.callId);
|
|
8736
8951
|
}
|
|
8737
|
-
resolveLiveFollowers(request,
|
|
8952
|
+
resolveLiveFollowers(entry.request, result);
|
|
8953
|
+
entry.resolve(result);
|
|
8738
8954
|
}
|
|
8739
|
-
}
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
});
|
|
8747
|
-
} else {
|
|
8748
|
-
const completionBuffer: Array<{
|
|
8749
|
-
request: ToolCallRequest;
|
|
8750
|
-
result: unknown | null;
|
|
8751
|
-
metadata?: ToolResultMetadataInput | null;
|
|
8752
|
-
jobId?: string;
|
|
8753
|
-
meta?: Record<string, unknown>;
|
|
8754
|
-
resolve: (value: unknown) => void;
|
|
8755
|
-
reject: (error: unknown) => void;
|
|
8756
|
-
}> = [];
|
|
8757
|
-
const completionFailedCallIds = new Set<string>();
|
|
8758
|
-
let completionFlushScheduled = false;
|
|
8759
|
-
const flushCompletionBuffer = async (): Promise<void> => {
|
|
8760
|
-
const entries = completionBuffer.splice(0);
|
|
8761
|
-
if (entries.length === 0) return;
|
|
8762
|
-
try {
|
|
8763
|
-
const resolvedResults = await this.resolveToolCallBatchResults(
|
|
8764
|
-
toolId,
|
|
8765
|
-
entries.map((entry) => ({
|
|
8766
|
-
request: entry.request,
|
|
8767
|
-
result: entry.result,
|
|
8768
|
-
metadata: entry.metadata,
|
|
8769
|
-
jobId: entry.jobId,
|
|
8770
|
-
meta: entry.meta,
|
|
8771
|
-
})),
|
|
8772
|
-
);
|
|
8773
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
8774
|
-
const entry = entries[index]!;
|
|
8775
|
-
const result = resolvedResults[index];
|
|
8776
|
-
if (result != null) {
|
|
8777
|
-
successfulLiveStepCallIds.add(entry.request.callId);
|
|
8955
|
+
} catch (error) {
|
|
8956
|
+
for (const entry of entries) {
|
|
8957
|
+
completionFailedCallIds.add(entry.request.callId);
|
|
8958
|
+
await this.rejectToolCall(toolId, entry.request, error, {
|
|
8959
|
+
persistReceiptFailure: false,
|
|
8960
|
+
});
|
|
8961
|
+
entry.reject(error);
|
|
8778
8962
|
}
|
|
8779
|
-
resolveLiveFollowers(entry.request, result);
|
|
8780
|
-
entry.resolve(result);
|
|
8781
8963
|
}
|
|
8782
|
-
}
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8964
|
+
};
|
|
8965
|
+
const enqueueCompletion = (
|
|
8966
|
+
request: ToolCallRequest,
|
|
8967
|
+
execution: ParsedToolExecuteResponse,
|
|
8968
|
+
): Promise<unknown> =>
|
|
8969
|
+
new Promise((resolve, reject) => {
|
|
8970
|
+
completionBuffer.push({
|
|
8971
|
+
request,
|
|
8972
|
+
result: execution.result ?? null,
|
|
8973
|
+
metadata: execution.metadata ?? null,
|
|
8974
|
+
jobId: execution.jobId,
|
|
8975
|
+
meta: execution.meta,
|
|
8976
|
+
resolve,
|
|
8977
|
+
reject,
|
|
8787
8978
|
});
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
execution: ParsedToolExecuteResponse,
|
|
8795
|
-
): Promise<unknown> =>
|
|
8796
|
-
new Promise((resolve, reject) => {
|
|
8797
|
-
completionBuffer.push({
|
|
8798
|
-
request,
|
|
8799
|
-
result: execution.result ?? null,
|
|
8800
|
-
metadata: execution.metadata ?? null,
|
|
8801
|
-
jobId: execution.jobId,
|
|
8802
|
-
meta: execution.meta,
|
|
8803
|
-
resolve,
|
|
8804
|
-
reject,
|
|
8979
|
+
if (completionFlushScheduled) return;
|
|
8980
|
+
completionFlushScheduled = true;
|
|
8981
|
+
setTimeout(() => {
|
|
8982
|
+
completionFlushScheduled = false;
|
|
8983
|
+
void flushCompletionBuffer();
|
|
8984
|
+
}, 0);
|
|
8805
8985
|
});
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
this.governor.policy.concurrency.toolCalls;
|
|
8824
|
-
const shapedToolParallelism =
|
|
8825
|
-
await this.resourceGovernor.suggestedToolParallelism(
|
|
8826
|
-
toolId,
|
|
8986
|
+
// Seed the dispatch width from the governor's provider-shaped
|
|
8987
|
+
// parallelism instead of the flat policy.concurrency.toolCalls
|
|
8988
|
+
// ceiling. The flat width launched every pending row at once into
|
|
8989
|
+
// the pacer, so unhinted providers 429'd on the first burst before
|
|
8990
|
+
// AIMD could halve the rate. The shaped estimate is derived from the
|
|
8991
|
+
// provider's RPS/maxConcurrency pacing rules (see
|
|
8992
|
+
// suggestedParallelism in governor.ts), floored at 1 and capped by
|
|
8993
|
+
// the global tool-call concurrency ceiling. The pacer still gates
|
|
8994
|
+
// each in-flight call, so this only trims the launch burst.
|
|
8995
|
+
const toolCallConcurrencyCeiling =
|
|
8996
|
+
this.governor.policy.concurrency.toolCalls;
|
|
8997
|
+
const shapedToolParallelism =
|
|
8998
|
+
await this.resourceGovernor.suggestedToolParallelism(
|
|
8999
|
+
toolId,
|
|
9000
|
+
toolCallConcurrencyCeiling,
|
|
9001
|
+
);
|
|
9002
|
+
const dispatchWidth = Math.min(
|
|
8827
9003
|
toolCallConcurrencyCeiling,
|
|
9004
|
+
Math.max(1, shapedToolParallelism),
|
|
8828
9005
|
);
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
9006
|
+
const dispatchResults = await dispatchBoundedSettled(
|
|
9007
|
+
pendingRequests,
|
|
9008
|
+
dispatchWidth,
|
|
9009
|
+
async (request) => {
|
|
9010
|
+
// Circuit breaker: do not dispatch this provider call once a
|
|
9011
|
+
// persistence failure has occurred in this run.
|
|
9012
|
+
if (this.persistenceLatch.tripped) {
|
|
9013
|
+
this.persistenceLatch.preventedCallCount += 1;
|
|
9014
|
+
throw new RuntimePersistenceCircuitOpenError(
|
|
9015
|
+
this.persistenceLatch,
|
|
9016
|
+
);
|
|
9017
|
+
}
|
|
9018
|
+
const execution = await this.callToolExecutionAPI(
|
|
9019
|
+
toolId,
|
|
9020
|
+
request.input,
|
|
9021
|
+
{
|
|
9022
|
+
beforeProviderCall: () =>
|
|
9023
|
+
this.assertRuntimeToolReceiptOwnership([request]),
|
|
9024
|
+
...(request.receiptKey
|
|
9025
|
+
? {
|
|
9026
|
+
durableCallReceiptKey: request.receiptKey,
|
|
9027
|
+
executionAuthScopeDigest:
|
|
9028
|
+
request.executionAuthScopeDigest,
|
|
9029
|
+
providerIdempotencyKey:
|
|
9030
|
+
this.providerIdempotencyKeyForToolCall({
|
|
9031
|
+
cacheKey: request.receiptKey,
|
|
9032
|
+
force: request.force === true,
|
|
9033
|
+
leaseId: request.receiptLeaseId,
|
|
9034
|
+
}),
|
|
9035
|
+
receiptLeaseExpiresAt:
|
|
9036
|
+
request.receiptLeaseExpiresAt,
|
|
9037
|
+
heartbeatReceipt: () =>
|
|
9038
|
+
this.renewRuntimeToolReceiptOwnership([request]),
|
|
9039
|
+
}
|
|
9040
|
+
: {}),
|
|
9041
|
+
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
|
|
9042
|
+
request,
|
|
9043
|
+
]),
|
|
9044
|
+
},
|
|
8843
9045
|
);
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
leaseId: request.receiptLeaseId,
|
|
8861
|
-
}),
|
|
8862
|
-
receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
|
|
8863
|
-
heartbeatReceipt: () =>
|
|
8864
|
-
this.renewRuntimeToolReceiptOwnership([request]),
|
|
8865
|
-
}
|
|
8866
|
-
: {}),
|
|
8867
|
-
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
|
|
8868
|
-
request,
|
|
8869
|
-
]),
|
|
8870
|
-
},
|
|
8871
|
-
);
|
|
8872
|
-
return await enqueueCompletion(request, execution);
|
|
8873
|
-
},
|
|
8874
|
-
);
|
|
8875
|
-
const failedEntries = dispatchResults.filter(
|
|
8876
|
-
(entry) =>
|
|
8877
|
-
entry.error !== undefined &&
|
|
8878
|
-
!completionFailedCallIds.has(entry.job.callId),
|
|
8879
|
-
);
|
|
8880
|
-
await Promise.allSettled(
|
|
8881
|
-
failedEntries.map((entry) =>
|
|
8882
|
-
rejectWithLiveFollowers(entry.job, entry.error),
|
|
8883
|
-
),
|
|
8884
|
-
);
|
|
8885
|
-
recordToolStep(pendingRequests);
|
|
8886
|
-
this.#options.onBatchComplete?.(this.checkpoint);
|
|
9046
|
+
return await enqueueCompletion(request, execution);
|
|
9047
|
+
},
|
|
9048
|
+
);
|
|
9049
|
+
const failedEntries = dispatchResults.filter(
|
|
9050
|
+
(entry) =>
|
|
9051
|
+
entry.error !== undefined &&
|
|
9052
|
+
!completionFailedCallIds.has(entry.job.callId),
|
|
9053
|
+
);
|
|
9054
|
+
await Promise.allSettled(
|
|
9055
|
+
failedEntries.map((entry) =>
|
|
9056
|
+
rejectWithLiveFollowers(entry.job, entry.error),
|
|
9057
|
+
),
|
|
9058
|
+
);
|
|
9059
|
+
recordToolStep(pendingRequests);
|
|
9060
|
+
this.#options.onBatchComplete?.(this.checkpoint);
|
|
9061
|
+
}
|
|
8887
9062
|
}
|
|
9063
|
+
} finally {
|
|
9064
|
+
pendingReceiptHeartbeat?.stop();
|
|
8888
9065
|
}
|
|
9066
|
+
const pendingReceiptLeaseLost = pendingReceiptHeartbeat?.leaseLost();
|
|
9067
|
+
if (pendingReceiptLeaseLost) throw pendingReceiptLeaseLost;
|
|
8889
9068
|
if (durableExistingRunningHandler) {
|
|
8890
9069
|
await durableExistingRunningHandler;
|
|
8891
9070
|
}
|
|
@@ -9321,11 +9500,44 @@ export class PlayContextImpl {
|
|
|
9321
9500
|
// is the fetch itself, so independently delayed runners cannot
|
|
9322
9501
|
// compress real provider arrivals after their tickets.
|
|
9323
9502
|
const protectionHeaders = await this.vercelProtectionHeaders();
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9503
|
+
// Use the mode carried by this physical execution request.
|
|
9504
|
+
// A restored execution scope may predate integration-mode
|
|
9505
|
+
// authority metadata, while the signed launch and every tool
|
|
9506
|
+
// request still carry options.integrationMode. Pacing the
|
|
9507
|
+
// latter as live would make fixture runs both slow and
|
|
9508
|
+
// unauditable even though the app never dispatches a provider
|
|
9509
|
+
// request.
|
|
9510
|
+
const fixtureExecution =
|
|
9511
|
+
this.#options.integrationMode === 'fixture';
|
|
9512
|
+
const enforceFixtureProviderPacing =
|
|
9513
|
+
fixtureExecution &&
|
|
9514
|
+
this.#options.enforceFixtureProviderPacing === true;
|
|
9515
|
+
const fixtureOnlyExecution =
|
|
9516
|
+
fixtureExecution && !enforceFixtureProviderPacing;
|
|
9517
|
+
if (
|
|
9518
|
+
fixtureOnlyExecution &&
|
|
9519
|
+
!this.fixtureProviderPacingBypassLogged
|
|
9520
|
+
) {
|
|
9521
|
+
this.fixtureProviderPacingBypassLogged = true;
|
|
9522
|
+
this.log(
|
|
9523
|
+
'Fixture mode: provider pacing bypassed because no provider request is dispatched.',
|
|
9524
|
+
);
|
|
9525
|
+
}
|
|
9526
|
+
if (
|
|
9527
|
+
enforceFixtureProviderPacing &&
|
|
9528
|
+
!this.fixtureProviderPacingEnforcementLogged
|
|
9529
|
+
) {
|
|
9530
|
+
this.fixtureProviderPacingEnforcementLogged = true;
|
|
9531
|
+
this.log(
|
|
9532
|
+
'Fixture mode: provider pacing explicitly enforced for production-parity testing.',
|
|
9533
|
+
);
|
|
9534
|
+
}
|
|
9535
|
+
const providerPermit = fixtureOnlyExecution
|
|
9536
|
+
? { release() {} }
|
|
9537
|
+
: await this.resourceGovernor.acquireProviderPermit({
|
|
9538
|
+
toolId,
|
|
9539
|
+
signal: abortController?.signal,
|
|
9540
|
+
});
|
|
9329
9541
|
const integrationFetchStartedAt = Date.now();
|
|
9330
9542
|
providerCallStartedAt = integrationFetchStartedAt;
|
|
9331
9543
|
try {
|