deepline 0.2.53 → 0.2.55
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/client.ts +17 -1
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +43 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +30 -1
- package/dist/bundling-sources/shared_libs/play-runtime/cell-provenance.ts +231 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1094 -128
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +178 -9
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-node-io.ts +634 -0
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-observation.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +251 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-node-scope.ts +160 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +43 -5
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +26 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +83 -0
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +3 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +49 -1
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +375 -29
- package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +636 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-binding.ts +598 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +1645 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +202 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +16 -1
- package/dist/bundling-sources/shared_libs/plays/ts-ast.ts +48 -0
- package/dist/cli/index.js +994 -312
- package/dist/cli/index.mjs +994 -312
- package/dist/{compiler-manifest-Cj3--4ZJ.d.mts → compiler-manifest-Bl8kmLx9.d.mts} +118 -0
- package/dist/{compiler-manifest-Cj3--4ZJ.d.ts → compiler-manifest-Bl8kmLx9.d.ts} +118 -0
- package/dist/index.d.mts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +419 -59
- package/dist/index.mjs +419 -59
- package/dist/install-integrity.json +12 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +1361 -45
- package/package.json +1 -1
|
@@ -237,6 +237,12 @@ import {
|
|
|
237
237
|
createSecretRedactionContext,
|
|
238
238
|
type SecretRedactionContext,
|
|
239
239
|
} from './secret-redaction';
|
|
240
|
+
import {
|
|
241
|
+
buildPlayDocflowNodeErrorPreview,
|
|
242
|
+
buildPlayDocflowNodeInputPreviewMap,
|
|
243
|
+
type PlayDocflowNodeInputCapture,
|
|
244
|
+
} from './docflow-node-io';
|
|
245
|
+
import type { DocflowObservationUpsert } from './docflow-observation';
|
|
240
246
|
import {
|
|
241
247
|
assertNoSecretTaint,
|
|
242
248
|
assertSecretAuthUsesTls,
|
|
@@ -277,7 +283,25 @@ import type {
|
|
|
277
283
|
RuntimeStepProgram,
|
|
278
284
|
RuntimeStepProgramStep,
|
|
279
285
|
RuntimeConditionalStepResolver,
|
|
286
|
+
PlayCellProducerAttempt,
|
|
287
|
+
PlayCellReadRef,
|
|
288
|
+
PlayCellDecision,
|
|
289
|
+
PlayRowReadOrder,
|
|
290
|
+
PlayRowMeta,
|
|
291
|
+
PlaySheetCellProducer,
|
|
280
292
|
} from './ctx-types';
|
|
293
|
+
import {
|
|
294
|
+
MAX_CELL_READ_REFS,
|
|
295
|
+
MAX_ROW_READ_CELLS,
|
|
296
|
+
ROW_META_CELL_KEY,
|
|
297
|
+
cellReadCut,
|
|
298
|
+
hydrateCellReadRefs,
|
|
299
|
+
} from './cell-provenance';
|
|
300
|
+
import {
|
|
301
|
+
buildPlayNodeScope,
|
|
302
|
+
playNodeScopeToWire,
|
|
303
|
+
type PlayNodeScope,
|
|
304
|
+
} from './play-node-scope';
|
|
281
305
|
import {
|
|
282
306
|
StepProgramDatasetBuilder,
|
|
283
307
|
type StepProgramDatasetColumnInput,
|
|
@@ -755,6 +779,59 @@ function finitePositiveInteger(value: unknown): number | null {
|
|
|
755
779
|
return integer !== null && integer > 0 ? integer : null;
|
|
756
780
|
}
|
|
757
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Sources that answered without opening a provider request of their own: a
|
|
784
|
+
* completed-receipt cache hit, a worker-replay checkpoint, and a follower that
|
|
785
|
+
* attached to an in-flight owner. `cached` is false on the in-flight metadata
|
|
786
|
+
* (nothing was cached at attach time), but no request was made either, so
|
|
787
|
+
* classifying it as a live call disagreed with the `reused` marker the sheet
|
|
788
|
+
* already writes and inflated `providerRequests` against `reusedCells`.
|
|
789
|
+
*/
|
|
790
|
+
const REUSED_TOOL_EXECUTION_SOURCES = new Set([
|
|
791
|
+
'cache',
|
|
792
|
+
'checkpoint',
|
|
793
|
+
'in_flight',
|
|
794
|
+
]);
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Classify a settled tool-call result for the cell producer trace (ADR 0018).
|
|
798
|
+
* Unknown shapes settle as `completed`: the call returned, which is the fact
|
|
799
|
+
* the trace records.
|
|
800
|
+
*/
|
|
801
|
+
function toolAttemptOutcomeForResult(
|
|
802
|
+
value: unknown,
|
|
803
|
+
): Exclude<PlayCellProducerAttempt['outcome'], 'running'> {
|
|
804
|
+
if (value == null) return 'no_result';
|
|
805
|
+
if (typeof value !== 'object' || Array.isArray(value)) return 'completed';
|
|
806
|
+
const record = value as Record<string, unknown>;
|
|
807
|
+
const execution = (record._metadata as Record<string, unknown> | undefined)
|
|
808
|
+
?.execution as Record<string, unknown> | undefined;
|
|
809
|
+
const reused =
|
|
810
|
+
execution?.cached === true ||
|
|
811
|
+
execution?.receiptRole === 'follower' ||
|
|
812
|
+
(typeof execution?.source === 'string' &&
|
|
813
|
+
REUSED_TOOL_EXECUTION_SOURCES.has(execution.source));
|
|
814
|
+
if (reused) return 'cached';
|
|
815
|
+
return record.status === 'no_result' ? 'no_result' : 'completed';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Explicit execution location for a queued row-scoped tool call (ADR 0018).
|
|
820
|
+
* `toolIdOverride` lets a native batch report the batch operation it actually
|
|
821
|
+
* sent while keeping the members' column/namespace.
|
|
822
|
+
*/
|
|
823
|
+
function playNodeScopeForToolCallRequest(
|
|
824
|
+
request: ToolCallRequest,
|
|
825
|
+
toolIdOverride?: string,
|
|
826
|
+
): PlayNodeScope | null {
|
|
827
|
+
return buildPlayNodeScope({
|
|
828
|
+
toolId: toolIdOverride ?? request.toolId,
|
|
829
|
+
callKey: request.contextKey,
|
|
830
|
+
column: request.fieldName,
|
|
831
|
+
tableNamespace: request.tableNamespace,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
758
835
|
type ToolExecutionApiOptions = {
|
|
759
836
|
timeoutMs?: number;
|
|
760
837
|
durableCallReceiptKey?: string | null;
|
|
@@ -764,6 +841,12 @@ type ToolExecutionApiOptions = {
|
|
|
764
841
|
beforeProviderCall?: () => Promise<void> | void;
|
|
765
842
|
heartbeatReceipt?: () => Promise<void> | void;
|
|
766
843
|
parkProviderCall?: () => Promise<void> | void;
|
|
844
|
+
/**
|
|
845
|
+
* Explicit graph-node attribution for this provider call (ADR 0018). Carried
|
|
846
|
+
* to the execute route, which stamps it onto the usage event so per-node
|
|
847
|
+
* credits are a query instead of a log-text inference.
|
|
848
|
+
*/
|
|
849
|
+
playNodeScope?: PlayNodeScope | null;
|
|
767
850
|
customerDbDataset?: {
|
|
768
851
|
limit: number;
|
|
769
852
|
offset: number;
|
|
@@ -772,6 +855,11 @@ type ToolExecutionApiOptions = {
|
|
|
772
855
|
};
|
|
773
856
|
};
|
|
774
857
|
const IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT = 25;
|
|
858
|
+
/**
|
|
859
|
+
* Per-cell cap on the durable producer trace (ADR 0018). A cascade has a handful
|
|
860
|
+
* of legs; this only bounds pathological loops, keeping `_cell_meta` small.
|
|
861
|
+
*/
|
|
862
|
+
const MAX_CELL_PRODUCER_ATTEMPTS = 12;
|
|
775
863
|
const BATCH_SIZE_LOG_SAMPLE_LIMIT = 10;
|
|
776
864
|
const STEP_PROGRAM_MAP_DEFINITION = Symbol('deepline.stepProgramMapDefinition');
|
|
777
865
|
|
|
@@ -1435,6 +1523,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
1435
1523
|
}> = [];
|
|
1436
1524
|
private processedRowCount = 0;
|
|
1437
1525
|
private sleepBoundaryIndex = 0;
|
|
1526
|
+
private nextDocflowInvocationSequence = 0;
|
|
1527
|
+
/**
|
|
1528
|
+
* Whether this run captures the docflow trace at all (ADR 0019), decided at
|
|
1529
|
+
* admission and handed down through the signed runtime authority.
|
|
1530
|
+
*
|
|
1531
|
+
* Read at every capture SITE rather than only inside the recorders, because
|
|
1532
|
+
* the cost being gated is mostly in the arguments: `cellReadRefsForRow`,
|
|
1533
|
+
* `effectiveCellReads` and `rowInputReadColumns` each walk the row, and a
|
|
1534
|
+
* recorder that returns early has already paid for them. Off, an ungated org
|
|
1535
|
+
* writes precisely the `_cell_meta` it wrote before this feature existed —
|
|
1536
|
+
* no `reads`, no `decide`, no `_row` order — so nothing downstream has to
|
|
1537
|
+
* know two shapes.
|
|
1538
|
+
*/
|
|
1539
|
+
private get docflowCaptureEnabled(): boolean {
|
|
1540
|
+
return this.#options.docflowEnabled === true;
|
|
1541
|
+
}
|
|
1438
1542
|
/**
|
|
1439
1543
|
* `ctx.customerDb.query` is syntactic sugar over the query_customer_db tool.
|
|
1440
1544
|
* Keep generated tool ids distinct so separate imperative queries retain
|
|
@@ -1764,6 +1868,165 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
1764
1868
|
});
|
|
1765
1869
|
}
|
|
1766
1870
|
|
|
1871
|
+
/** @internal Compiler-injected authored-flow execution marker. */
|
|
1872
|
+
async __deeplineDocflowHit(nodeId: string): Promise<void> {
|
|
1873
|
+
const normalizedNodeId = nodeId.trim();
|
|
1874
|
+
if (!normalizedNodeId) {
|
|
1875
|
+
throw new Error('Docflow runtime node id must be non-empty.');
|
|
1876
|
+
}
|
|
1877
|
+
// These calls are COMPILER-INJECTED, so bundle-time gating cannot reach
|
|
1878
|
+
// them: a prebuilt is compiled once, with its diagram, and then runs for
|
|
1879
|
+
// every org. Without this check an ungated customer running a prebuilt gets
|
|
1880
|
+
// `docflow.node.*` events on the wire and a `step docflow:*` sequence in
|
|
1881
|
+
// `plays run --watch` — the vocabulary of a feature they do not have.
|
|
1882
|
+
// The id is still validated above: a malformed injection is a compiler bug
|
|
1883
|
+
// and stays loud whether or not the run is capturing.
|
|
1884
|
+
if (!this.docflowCaptureEnabled) return;
|
|
1885
|
+
this.emitExecutionEvent({
|
|
1886
|
+
type: 'docflow.node.hit',
|
|
1887
|
+
nodeId: `docflow:${normalizedNodeId}`,
|
|
1888
|
+
at: Date.now(),
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/** @internal Compiler-injected bounded authored-node observation boundary. */
|
|
1893
|
+
__deeplineObserveDocflowNode<T>(
|
|
1894
|
+
nodeId: string,
|
|
1895
|
+
inputCaptures: readonly PlayDocflowNodeInputCapture[],
|
|
1896
|
+
outputPaths: readonly string[],
|
|
1897
|
+
execute: () => T | Promise<T>,
|
|
1898
|
+
): T | Promise<T> {
|
|
1899
|
+
const normalizedNodeId = nodeId.trim();
|
|
1900
|
+
if (!normalizedNodeId) {
|
|
1901
|
+
throw new Error('Docflow runtime node id must be non-empty.');
|
|
1902
|
+
}
|
|
1903
|
+
// Ungated, this boundary is a pass-through: the wrapped work still runs and
|
|
1904
|
+
// still returns what it returned, but nothing is previewed, redacted, or
|
|
1905
|
+
// emitted. Returning before `buildPlayDocflowNodeInputPreviewMap` is the
|
|
1906
|
+
// point — previewing every bound node's inputs is the expensive half, and a
|
|
1907
|
+
// customer outside the rollout should not pay for a capture they cannot
|
|
1908
|
+
// read. Same reasoning as `__deeplineDocflowHit` above: injected at compile
|
|
1909
|
+
// time, so the run is the only place left to decide.
|
|
1910
|
+
if (!this.docflowCaptureEnabled) return execute();
|
|
1911
|
+
const attempt = this.currentRunAttempt;
|
|
1912
|
+
const inputs = buildPlayDocflowNodeInputPreviewMap(inputCaptures, {
|
|
1913
|
+
redactor: this.secretRedactor,
|
|
1914
|
+
});
|
|
1915
|
+
const invocationSeq = this.nextDocflowInvocationSequence++;
|
|
1916
|
+
const invocationId = `${attempt}:${invocationSeq}`;
|
|
1917
|
+
const observationNodeId = `docflow:${normalizedNodeId}`;
|
|
1918
|
+
this.emitExecutionEvent({
|
|
1919
|
+
type: 'docflow.node.started',
|
|
1920
|
+
nodeId: observationNodeId,
|
|
1921
|
+
attempt,
|
|
1922
|
+
invocationId,
|
|
1923
|
+
inputs: inputs.values,
|
|
1924
|
+
...(inputs.truncated ? { inputsTruncated: true } : {}),
|
|
1925
|
+
at: Date.now(),
|
|
1926
|
+
});
|
|
1927
|
+
this.emitDocflowObservation({
|
|
1928
|
+
runId: this.currentGovernance.currentRunId,
|
|
1929
|
+
attempt,
|
|
1930
|
+
nodeId: observationNodeId,
|
|
1931
|
+
invocationSeq,
|
|
1932
|
+
invocationId,
|
|
1933
|
+
status: 'started',
|
|
1934
|
+
inputs: inputs.values,
|
|
1935
|
+
inputsTruncated: inputs.truncated,
|
|
1936
|
+
at: Date.now(),
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
const complete = (result: T): void => {
|
|
1940
|
+
const outputRoots = new Set(
|
|
1941
|
+
outputPaths
|
|
1942
|
+
.map((path) => path.trim())
|
|
1943
|
+
.filter((path) => path && path !== '$output')
|
|
1944
|
+
.map((path) => path.split('.')[0]),
|
|
1945
|
+
);
|
|
1946
|
+
const outputs = buildPlayDocflowNodeInputPreviewMap(
|
|
1947
|
+
outputPaths
|
|
1948
|
+
.map((path) => path.trim())
|
|
1949
|
+
.filter(Boolean)
|
|
1950
|
+
.map((path) => ({
|
|
1951
|
+
path,
|
|
1952
|
+
readRoot: () => result,
|
|
1953
|
+
properties:
|
|
1954
|
+
path === '$output'
|
|
1955
|
+
? []
|
|
1956
|
+
: outputRoots.size > 1
|
|
1957
|
+
? path.split('.')
|
|
1958
|
+
: path.split('.').slice(1),
|
|
1959
|
+
})),
|
|
1960
|
+
{ redactor: this.secretRedactor },
|
|
1961
|
+
);
|
|
1962
|
+
this.emitExecutionEvent({
|
|
1963
|
+
type: 'docflow.node.completed',
|
|
1964
|
+
nodeId: observationNodeId,
|
|
1965
|
+
attempt,
|
|
1966
|
+
invocationId,
|
|
1967
|
+
inputs: inputs.values,
|
|
1968
|
+
...(inputs.truncated ? { inputsTruncated: true } : {}),
|
|
1969
|
+
outputs: outputs.values,
|
|
1970
|
+
...(outputs.truncated ? { outputsTruncated: true } : {}),
|
|
1971
|
+
at: Date.now(),
|
|
1972
|
+
});
|
|
1973
|
+
this.emitDocflowObservation({
|
|
1974
|
+
runId: this.currentGovernance.currentRunId,
|
|
1975
|
+
attempt,
|
|
1976
|
+
nodeId: observationNodeId,
|
|
1977
|
+
invocationSeq,
|
|
1978
|
+
invocationId,
|
|
1979
|
+
status: 'completed',
|
|
1980
|
+
inputs: inputs.values,
|
|
1981
|
+
inputsTruncated: inputs.truncated,
|
|
1982
|
+
outputs: outputs.values,
|
|
1983
|
+
outputsTruncated: outputs.truncated,
|
|
1984
|
+
at: Date.now(),
|
|
1985
|
+
});
|
|
1986
|
+
};
|
|
1987
|
+
const fail = (error: unknown): void => {
|
|
1988
|
+
const errorPreview = buildPlayDocflowNodeErrorPreview(
|
|
1989
|
+
error,
|
|
1990
|
+
this.secretRedactor,
|
|
1991
|
+
);
|
|
1992
|
+
this.emitExecutionEvent({
|
|
1993
|
+
type: 'docflow.node.failed',
|
|
1994
|
+
nodeId: observationNodeId,
|
|
1995
|
+
attempt,
|
|
1996
|
+
invocationId,
|
|
1997
|
+
inputs: inputs.values,
|
|
1998
|
+
...(inputs.truncated ? { inputsTruncated: true } : {}),
|
|
1999
|
+
error: errorPreview,
|
|
2000
|
+
at: Date.now(),
|
|
2001
|
+
});
|
|
2002
|
+
this.emitDocflowObservation({
|
|
2003
|
+
runId: this.currentGovernance.currentRunId,
|
|
2004
|
+
attempt,
|
|
2005
|
+
nodeId: observationNodeId,
|
|
2006
|
+
invocationSeq,
|
|
2007
|
+
invocationId,
|
|
2008
|
+
status: 'failed',
|
|
2009
|
+
inputs: inputs.values,
|
|
2010
|
+
inputsTruncated: inputs.truncated,
|
|
2011
|
+
error: errorPreview,
|
|
2012
|
+
at: Date.now(),
|
|
2013
|
+
});
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
try {
|
|
2017
|
+
const result = execute();
|
|
2018
|
+
if (result instanceof Promise) {
|
|
2019
|
+
void result.then(complete, fail);
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
complete(result);
|
|
2023
|
+
return result;
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
fail(error);
|
|
2026
|
+
throw error;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1767
2030
|
private durableBoundaryId(localId: string): string {
|
|
1768
2031
|
// Durable boundaries live in one checkpoint for the whole root execution.
|
|
1769
2032
|
// Nested plays and concurrent child calls therefore need a stable run scope
|
|
@@ -1907,6 +2170,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
1907
2170
|
...(existing?.cellMetaPatch ?? {}),
|
|
1908
2171
|
...(update.cellMetaPatch ?? {}),
|
|
1909
2172
|
},
|
|
2173
|
+
// Row-grain provenance is re-stated whole on every patch, so the later
|
|
2174
|
+
// record always supersedes: it can only have grown (ADR 0019).
|
|
2175
|
+
...((update.rowMetaPatch ?? existing?.rowMetaPatch)
|
|
2176
|
+
? {
|
|
2177
|
+
rowMetaPatch: {
|
|
2178
|
+
...(existing?.rowMetaPatch ?? {}),
|
|
2179
|
+
...(update.rowMetaPatch ?? {}),
|
|
2180
|
+
},
|
|
2181
|
+
}
|
|
2182
|
+
: {}),
|
|
1910
2183
|
};
|
|
1911
2184
|
this.activeMapCheckpointUpdates.set(checkpointKey, next);
|
|
1912
2185
|
}
|
|
@@ -1931,7 +2204,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
1931
2204
|
if (!this.#options.onExecutionEvent) {
|
|
1932
2205
|
return;
|
|
1933
2206
|
}
|
|
1934
|
-
|
|
2207
|
+
try {
|
|
2208
|
+
const pending = this.#options.onExecutionEvent(event);
|
|
2209
|
+
if (pending && typeof pending.then === 'function') {
|
|
2210
|
+
void pending.catch(() => {
|
|
2211
|
+
// Execution events are observability projections. Their transport
|
|
2212
|
+
// must never change authored play behavior or mask its result.
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
} catch {
|
|
2216
|
+
// Keep synchronous observer failures outside the authored execution.
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
/**
|
|
2221
|
+
* Durable docflow observation write (ADR 0016 rule 2). The sink is present
|
|
2222
|
+
* only for instrumented plays and posts to the receipt gateway. It is
|
|
2223
|
+
* strictly fire-and-forget: a synchronous throw or a rejected promise is
|
|
2224
|
+
* swallowed (loudly logged by the sink) so a durable-write failure can never
|
|
2225
|
+
* fail or slow the authored play body. The live event emission above is the
|
|
2226
|
+
* authoritative low-latency path and is unaffected.
|
|
2227
|
+
*/
|
|
2228
|
+
private emitDocflowObservation(observation: DocflowObservationUpsert): void {
|
|
2229
|
+
if (!this.#options.onDocflowObservation) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
try {
|
|
2233
|
+
const pending = this.#options.onDocflowObservation(observation);
|
|
2234
|
+
if (pending && typeof pending.then === 'function') {
|
|
2235
|
+
void pending.catch(() => {
|
|
2236
|
+
// Swallowed here; the sink is responsible for loud logging.
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
} catch {
|
|
2240
|
+
// Never let an observation write escape into authored execution.
|
|
2241
|
+
}
|
|
1935
2242
|
}
|
|
1936
2243
|
|
|
1937
2244
|
private async resolveSecretAuth(auth: SecretAuth | undefined) {
|
|
@@ -3346,6 +3653,418 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3346
3653
|
return this.currentExecutionScope.receipt.ownerAttempt;
|
|
3347
3654
|
}
|
|
3348
3655
|
|
|
3656
|
+
/**
|
|
3657
|
+
* Explicit tool-node lifecycle for non-row-scoped calls (ADR 0018). Replaces
|
|
3658
|
+
* the stdout-regex inference the progress reporter used to run.
|
|
3659
|
+
*/
|
|
3660
|
+
private emitToolCallLifecycle(
|
|
3661
|
+
phase: 'started' | 'settled',
|
|
3662
|
+
toolId: string,
|
|
3663
|
+
callKey: string | null,
|
|
3664
|
+
settlement?: {
|
|
3665
|
+
outcome: 'completed' | 'no_result' | 'failed' | 'cached';
|
|
3666
|
+
durationMs?: number | null;
|
|
3667
|
+
error?: string | null;
|
|
3668
|
+
},
|
|
3669
|
+
): void {
|
|
3670
|
+
if (phase === 'started') {
|
|
3671
|
+
this.emitExecutionEvent({
|
|
3672
|
+
type: 'tool.call.started',
|
|
3673
|
+
toolId,
|
|
3674
|
+
callKey,
|
|
3675
|
+
at: Date.now(),
|
|
3676
|
+
});
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
if (!settlement) return;
|
|
3680
|
+
this.emitExecutionEvent({
|
|
3681
|
+
type: 'tool.call.settled',
|
|
3682
|
+
toolId,
|
|
3683
|
+
callKey,
|
|
3684
|
+
outcome: settlement.outcome,
|
|
3685
|
+
...(settlement.durationMs !== undefined
|
|
3686
|
+
? { durationMs: settlement.durationMs }
|
|
3687
|
+
: {}),
|
|
3688
|
+
...(settlement.error ? { error: settlement.error } : {}),
|
|
3689
|
+
at: Date.now(),
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
/**
|
|
3694
|
+
* Direct (non-row-scoped) provider call with explicit node lifecycle. The
|
|
3695
|
+
* failure branch must emit `tool.call.settled` before rethrowing, otherwise a
|
|
3696
|
+
* failed tool node would stay `running` in the snapshot forever.
|
|
3697
|
+
*/
|
|
3698
|
+
private async executeDirectToolCall(input: {
|
|
3699
|
+
toolId: string;
|
|
3700
|
+
callKey: string;
|
|
3701
|
+
startedAt: number;
|
|
3702
|
+
input: Record<string, unknown>;
|
|
3703
|
+
options: ToolExecutionApiOptions;
|
|
3704
|
+
}): Promise<ParsedToolExecuteResponse> {
|
|
3705
|
+
// The caller owns settlement: it pairs started/settled in a finally so a
|
|
3706
|
+
// throw anywhere in the direct path — transport, result wrapping, receipt
|
|
3707
|
+
// ownership — settles the node exactly once instead of stranding it.
|
|
3708
|
+
return await this.callToolExecutionAPI(
|
|
3709
|
+
input.toolId,
|
|
3710
|
+
input.input,
|
|
3711
|
+
input.options,
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
/**
|
|
3716
|
+
* Bounded per-cell producer trace (ADR 0018).
|
|
3717
|
+
*
|
|
3718
|
+
* Returns the cell's accumulated attempts, with `attempt` appended when a new
|
|
3719
|
+
* one starts. Attempt records are returned by reference so a caller can
|
|
3720
|
+
* settle `outcome`/`durationMs` in place before the next patch re-states the
|
|
3721
|
+
* array. Storage is `activeMapCellMeta`, which is scoped to the running map
|
|
3722
|
+
* and released with it, so this adds no run-lifetime structure.
|
|
3723
|
+
*/
|
|
3724
|
+
/** Last recorded producer for a cell, so a later patch cannot erase it. */
|
|
3725
|
+
private resolveCellProducer(
|
|
3726
|
+
key: string | null,
|
|
3727
|
+
fieldName: string,
|
|
3728
|
+
): PlaySheetCellProducer | undefined {
|
|
3729
|
+
const existingCell = key
|
|
3730
|
+
? this.activeMapCellMeta?.get(key)?.[fieldName]
|
|
3731
|
+
: undefined;
|
|
3732
|
+
const producer =
|
|
3733
|
+
existingCell &&
|
|
3734
|
+
typeof existingCell === 'object' &&
|
|
3735
|
+
!Array.isArray(existingCell)
|
|
3736
|
+
? (existingCell as { producer?: unknown }).producer
|
|
3737
|
+
: undefined;
|
|
3738
|
+
return producer as PlaySheetCellProducer | undefined;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
private resolveCellProducers(
|
|
3742
|
+
key: string | null,
|
|
3743
|
+
fieldName: string,
|
|
3744
|
+
attempt?: PlayCellProducerAttempt,
|
|
3745
|
+
): {
|
|
3746
|
+
producers: PlayCellProducerAttempt[] | undefined;
|
|
3747
|
+
attemptsDropped: number;
|
|
3748
|
+
} {
|
|
3749
|
+
if (!key) {
|
|
3750
|
+
return {
|
|
3751
|
+
producers: attempt ? [attempt] : undefined,
|
|
3752
|
+
attemptsDropped: 0,
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
const existingCell = this.activeCellMetaRecord(key, fieldName);
|
|
3756
|
+
const existingProducers = existingCell?.producers;
|
|
3757
|
+
const current = Array.isArray(existingProducers)
|
|
3758
|
+
? (existingProducers as PlayCellProducerAttempt[])
|
|
3759
|
+
: [];
|
|
3760
|
+
const droppedSoFar =
|
|
3761
|
+
typeof existingCell?.attemptsDropped === 'number'
|
|
3762
|
+
? existingCell.attemptsDropped
|
|
3763
|
+
: 0;
|
|
3764
|
+
if (!attempt) {
|
|
3765
|
+
return {
|
|
3766
|
+
producers: current.length > 0 ? current : undefined,
|
|
3767
|
+
attemptsDropped: droppedSoFar,
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
const next = [...current, attempt];
|
|
3771
|
+
if (next.length <= MAX_CELL_PRODUCER_ATTEMPTS) {
|
|
3772
|
+
return { producers: next, attemptsDropped: droppedSoFar };
|
|
3773
|
+
}
|
|
3774
|
+
// Keep the newest attempts — a cascade's decisive legs are its last ones —
|
|
3775
|
+
// but count what fell off. Without the count a capped cell's logicalCalls
|
|
3776
|
+
// reads as a complete total when it is a floor.
|
|
3777
|
+
const dropped = next.length - MAX_CELL_PRODUCER_ATTEMPTS;
|
|
3778
|
+
return {
|
|
3779
|
+
producers: next.slice(dropped),
|
|
3780
|
+
attemptsDropped: droppedSoFar + dropped,
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
/**
|
|
3785
|
+
* Carry this run's durable producer trace into a re-executed row (ADR 0018).
|
|
3786
|
+
*
|
|
3787
|
+
* `activeMapCellMeta` starts empty on a worker replay, and the durable jsonb
|
|
3788
|
+
* merge replaces a cell's `producers` array wholesale, so a re-executed cell
|
|
3789
|
+
* would erase the attempts the earlier execution recorded — silently
|
|
3790
|
+
* under-counting `logicalCalls` and losing the only record a failed leg ever
|
|
3791
|
+
* gets. The row already arrives carrying its durable cell meta (it is what
|
|
3792
|
+
* `previousCell` is built from), so seeding costs no extra round trip.
|
|
3793
|
+
*
|
|
3794
|
+
* Only cells stamped with the CURRENT run are seeded. A previous run's
|
|
3795
|
+
* attempts are that run's facts; appending them here would inflate this run.
|
|
3796
|
+
* Seeding read-modify-writes nothing durable: the append still lands through
|
|
3797
|
+
* the attempt-fenced write, so a fenced-out attempt cannot corrupt the trace.
|
|
3798
|
+
*/
|
|
3799
|
+
private seedActiveCellProducersFromDurableRow(
|
|
3800
|
+
rowKey: string | null,
|
|
3801
|
+
baseRow: Record<string, unknown>,
|
|
3802
|
+
): void {
|
|
3803
|
+
if (!rowKey || !this.activeMapCellMeta) return;
|
|
3804
|
+
const durable = baseRow[DEEPLINE_CELL_META_FIELD];
|
|
3805
|
+
if (!durable || typeof durable !== 'object' || Array.isArray(durable)) {
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
for (const [fieldName, rawCell] of Object.entries(
|
|
3809
|
+
durable as Record<string, unknown>,
|
|
3810
|
+
)) {
|
|
3811
|
+
if (!rawCell || typeof rawCell !== 'object' || Array.isArray(rawCell)) {
|
|
3812
|
+
continue;
|
|
3813
|
+
}
|
|
3814
|
+
const cell = rawCell as Record<string, unknown>;
|
|
3815
|
+
if (cell.runId !== this.currentRunId) continue;
|
|
3816
|
+
if (!Array.isArray(cell.producers) || cell.producers.length === 0) {
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
const existingRow = this.activeMapCellMeta.get(rowKey);
|
|
3820
|
+
const existingCell = this.activeCellMetaRecord(rowKey, fieldName);
|
|
3821
|
+
if (existingCell?.producers !== undefined) continue;
|
|
3822
|
+
this.activeMapCellMeta.set(rowKey, {
|
|
3823
|
+
...(existingRow ?? {}),
|
|
3824
|
+
[fieldName]: {
|
|
3825
|
+
...(existingCell ?? {}),
|
|
3826
|
+
producers: cell.producers,
|
|
3827
|
+
...(typeof cell.attemptsDropped === 'number'
|
|
3828
|
+
? { attemptsDropped: cell.attemptsDropped }
|
|
3829
|
+
: {}),
|
|
3830
|
+
},
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
/** Existing `_cell_meta` record for a cell inside the running map, if any. */
|
|
3836
|
+
private activeCellMetaRecord(
|
|
3837
|
+
key: string | null,
|
|
3838
|
+
fieldName: string,
|
|
3839
|
+
): Record<string, unknown> | null {
|
|
3840
|
+
const existingCell = key
|
|
3841
|
+
? this.activeMapCellMeta?.get(key)?.[fieldName]
|
|
3842
|
+
: undefined;
|
|
3843
|
+
return existingCell &&
|
|
3844
|
+
typeof existingCell === 'object' &&
|
|
3845
|
+
!Array.isArray(existingCell)
|
|
3846
|
+
? (existingCell as Record<string, unknown>)
|
|
3847
|
+
: null;
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
/**
|
|
3851
|
+
* Provenance capture is a field on writes that already happen (ADR 0019), so
|
|
3852
|
+
* a record made *before* the cell's next patch is seeded into the running
|
|
3853
|
+
* map's cell meta and re-stated by that patch. No extra emit, no extra round
|
|
3854
|
+
* trip, and the record is released with the map like the producer trace.
|
|
3855
|
+
*/
|
|
3856
|
+
private recordCellProvenance(
|
|
3857
|
+
key: string | null,
|
|
3858
|
+
fieldName: string,
|
|
3859
|
+
record: { reads?: PlayCellReadRef[]; decide?: PlayCellDecision },
|
|
3860
|
+
): void {
|
|
3861
|
+
if (!key || !this.activeMapCellMeta) return;
|
|
3862
|
+
if (record.reads === undefined && record.decide === undefined) return;
|
|
3863
|
+
const existingRow = this.activeMapCellMeta.get(key);
|
|
3864
|
+
const existingCell = this.activeCellMetaRecord(key, fieldName);
|
|
3865
|
+
this.activeMapCellMeta.set(key, {
|
|
3866
|
+
...(existingRow ?? {}),
|
|
3867
|
+
[fieldName]: {
|
|
3868
|
+
...(existingCell ?? {}),
|
|
3869
|
+
...(record.reads !== undefined ? { reads: record.reads } : {}),
|
|
3870
|
+
...(record.decide !== undefined ? { decide: record.decide } : {}),
|
|
3871
|
+
},
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
/** Row-grain read order for the running map's row, if one was recorded. */
|
|
3876
|
+
private resolveRowReadOrder(
|
|
3877
|
+
key: string | null,
|
|
3878
|
+
): PlayRowReadOrder | undefined {
|
|
3879
|
+
const rowMeta = this.activeCellMetaRecord(key, ROW_META_CELL_KEY);
|
|
3880
|
+
const reads = rowMeta?.reads;
|
|
3881
|
+
return reads && typeof reads === 'object' && !Array.isArray(reads)
|
|
3882
|
+
? (reads as PlayRowReadOrder)
|
|
3883
|
+
: undefined;
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
/**
|
|
3887
|
+
* Open one cell against the row's read order (ADR 0019).
|
|
3888
|
+
*
|
|
3889
|
+
* The order lives once per row under the reserved `_row` key instead of once
|
|
3890
|
+
* per cell. A written column is appended before its own cell runs, so its
|
|
3891
|
+
* position in the list *is* its cut point — everything before it was
|
|
3892
|
+
* available to it, itself and everything after it was not. That makes the
|
|
3893
|
+
* common case a single string append with no per-cell record at all. Cells
|
|
3894
|
+
* the list cannot place (non-persisted fields, and anything past the column
|
|
3895
|
+
* cap) fall back to an explicit cut, and cells past the cell cap are counted
|
|
3896
|
+
* as dropped rather than silently reading as having read nothing.
|
|
3897
|
+
*/
|
|
3898
|
+
private recordRowReadCell(
|
|
3899
|
+
key: string | null,
|
|
3900
|
+
fieldName: string,
|
|
3901
|
+
persisted: boolean,
|
|
3902
|
+
): void {
|
|
3903
|
+
if (!key || !this.activeMapCellMeta) return;
|
|
3904
|
+
const order = this.resolveRowReadOrder(key);
|
|
3905
|
+
if (!order || order.columns.length === 0) return;
|
|
3906
|
+
if (cellReadCut(order, fieldName) !== null) return;
|
|
3907
|
+
if (persisted && order.columns.length < MAX_CELL_READ_REFS) {
|
|
3908
|
+
this.mergeRowMeta(key, {
|
|
3909
|
+
reads: { ...order, columns: [...order.columns, fieldName] },
|
|
3910
|
+
});
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
const upto = order.upto ?? {};
|
|
3914
|
+
if (Object.keys(upto).length >= MAX_ROW_READ_CELLS) {
|
|
3915
|
+
this.mergeRowMeta(key, {
|
|
3916
|
+
reads: { ...order, droppedCells: (order.droppedCells ?? 0) + 1 },
|
|
3917
|
+
});
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
this.mergeRowMeta(key, {
|
|
3921
|
+
reads: { ...order, upto: { ...upto, [fieldName]: order.columns.length } },
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
/** Seed the row's read order with the columns the row arrived carrying. */
|
|
3926
|
+
private recordRowReadColumns(
|
|
3927
|
+
key: string | null,
|
|
3928
|
+
columns: readonly string[],
|
|
3929
|
+
): void {
|
|
3930
|
+
if (!key || !this.activeMapCellMeta || columns.length === 0) return;
|
|
3931
|
+
const order = this.resolveRowReadOrder(key);
|
|
3932
|
+
const merged = order ? [...order.columns] : [];
|
|
3933
|
+
for (const column of columns) {
|
|
3934
|
+
if (merged.length >= MAX_CELL_READ_REFS) break;
|
|
3935
|
+
if (merged.includes(column)) continue;
|
|
3936
|
+
merged.push(column);
|
|
3937
|
+
}
|
|
3938
|
+
if (order && merged.length === order.columns.length) return;
|
|
3939
|
+
this.mergeRowMeta(key, {
|
|
3940
|
+
reads: { ...(order ?? {}), columns: merged },
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
/** Merge a row-grain record under the reserved `_row` cell-meta key. */
|
|
3945
|
+
private mergeRowMeta(key: string, record: PlayRowMeta): void {
|
|
3946
|
+
if (!this.activeMapCellMeta) return;
|
|
3947
|
+
const existingRow = this.activeMapCellMeta.get(key);
|
|
3948
|
+
const existingMeta = this.activeCellMetaRecord(key, ROW_META_CELL_KEY);
|
|
3949
|
+
this.activeMapCellMeta.set(key, {
|
|
3950
|
+
...(existingRow ?? {}),
|
|
3951
|
+
[ROW_META_CELL_KEY]: { ...(existingMeta ?? {}), ...record },
|
|
3952
|
+
});
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
/**
|
|
3956
|
+
* Explicit per-cell read array, re-stated so a later patch cannot erase it.
|
|
3957
|
+
*
|
|
3958
|
+
* Only nested step-program cells carry one: their read-set includes sibling
|
|
3959
|
+
* steps, which are not part of the row's column order, so the row-grain cut
|
|
3960
|
+
* point cannot express it. Dataset columns resolve through the row order.
|
|
3961
|
+
*/
|
|
3962
|
+
private resolveCellReads(
|
|
3963
|
+
key: string | null,
|
|
3964
|
+
fieldName: string,
|
|
3965
|
+
): PlayCellReadRef[] | undefined {
|
|
3966
|
+
const reads = this.activeCellMetaRecord(key, fieldName)?.reads;
|
|
3967
|
+
return Array.isArray(reads) ? (reads as PlayCellReadRef[]) : undefined;
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
/** The read-set a cell saw, from its explicit array or the row order. */
|
|
3971
|
+
private effectiveCellReads(
|
|
3972
|
+
key: string | null,
|
|
3973
|
+
fieldName: string,
|
|
3974
|
+
): PlayCellReadRef[] {
|
|
3975
|
+
return (
|
|
3976
|
+
this.resolveCellReads(key, fieldName) ??
|
|
3977
|
+
hydrateCellReadRefs(this.resolveRowReadOrder(key), fieldName, '')?.map(
|
|
3978
|
+
(ref) => ({ column: ref.column }),
|
|
3979
|
+
) ??
|
|
3980
|
+
[]
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
/** Last recorded branch decision for a cell, re-stated on every patch. */
|
|
3985
|
+
private resolveCellDecision(
|
|
3986
|
+
key: string | null,
|
|
3987
|
+
fieldName: string,
|
|
3988
|
+
): PlayCellDecision | undefined {
|
|
3989
|
+
const decide = this.activeCellMetaRecord(key, fieldName)?.decide;
|
|
3990
|
+
return decide && typeof decide === 'object' && !Array.isArray(decide)
|
|
3991
|
+
? (decide as PlayCellDecision)
|
|
3992
|
+
: undefined;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
/**
|
|
3996
|
+
* Bounded read-set for one cell (ADR 0019).
|
|
3997
|
+
*
|
|
3998
|
+
* The runtime hands a column resolver the whole row, so the read-set it
|
|
3999
|
+
* witnesses is the row's columns at eval time — not a per-property access
|
|
4000
|
+
* trace. Recording the resolved column list as-is is the honest form: it is a
|
|
4001
|
+
* superset of what the resolver touched, and it is an observation rather than
|
|
4002
|
+
* a declaration. Per-property precision needs sandbox proxy tracking, which
|
|
4003
|
+
* is deliberately out of this wave.
|
|
4004
|
+
*
|
|
4005
|
+
* Only enumerable columns are recorded. CSV alias projections are
|
|
4006
|
+
* non-enumerable by construction, so they read as absent rather than as a
|
|
4007
|
+
* column the runtime can name.
|
|
4008
|
+
*
|
|
4009
|
+
* Refs carry no `table` or `rowKey` because every read here is same-row and
|
|
4010
|
+
* same-table; the reader fills both in from the cell's own address. Writing
|
|
4011
|
+
* the row key into each ref put a mapped row over the retained-row memory
|
|
4012
|
+
* budget, and the budget is not the thing that moves.
|
|
4013
|
+
*/
|
|
4014
|
+
private cellReadRefsForRow(
|
|
4015
|
+
rowKey: string | null,
|
|
4016
|
+
rowSources: readonly Record<string, unknown>[],
|
|
4017
|
+
targetFieldName: string,
|
|
4018
|
+
/**
|
|
4019
|
+
* Columns this map will write but has not written yet in this run. On a
|
|
4020
|
+
* rerun the row arrives carrying their previous values, which is
|
|
4021
|
+
* `previousCell` — the same fact the self-exclusion below rejects, and
|
|
4022
|
+
* recording it would manufacture a cycle between two columns that each
|
|
4023
|
+
* saw the other's stale value.
|
|
4024
|
+
*/
|
|
4025
|
+
pendingOutputColumns?: ReadonlySet<string>,
|
|
4026
|
+
): PlayCellReadRef[] | undefined {
|
|
4027
|
+
if (!rowKey) return undefined;
|
|
4028
|
+
const refs: PlayCellReadRef[] = [];
|
|
4029
|
+
const seen = new Set<string>();
|
|
4030
|
+
for (const source of rowSources) {
|
|
4031
|
+
for (const column of Object.keys(source)) {
|
|
4032
|
+
if (refs.length >= MAX_CELL_READ_REFS) break;
|
|
4033
|
+
// A cell never reads itself: on a rerun the target column is present in
|
|
4034
|
+
// the row carrying the *previous* run's value, which is a different
|
|
4035
|
+
// fact (`previousCell`), not an input to this computation.
|
|
4036
|
+
if (column === targetFieldName) continue;
|
|
4037
|
+
if (pendingOutputColumns?.has(column)) continue;
|
|
4038
|
+
if (!shouldPersistMapCellField(column)) continue;
|
|
4039
|
+
if (column.startsWith('__deepline')) continue;
|
|
4040
|
+
if (seen.has(column)) continue;
|
|
4041
|
+
seen.add(column);
|
|
4042
|
+
refs.push({ column });
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
return refs.length > 0 ? refs : undefined;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
/**
|
|
4049
|
+
* The row's input columns — everything the first cell of the row could read.
|
|
4050
|
+
* Columns this map writes are excluded even when the row already carries a
|
|
4051
|
+
* value for them, because on a rerun that value is the previous run's.
|
|
4052
|
+
*/
|
|
4053
|
+
private rowInputReadColumns(
|
|
4054
|
+
baseRow: Record<string, unknown>,
|
|
4055
|
+
datasetColumns: ReadonlySet<string>,
|
|
4056
|
+
): string[] {
|
|
4057
|
+
const columns: string[] = [];
|
|
4058
|
+
for (const column of Object.keys(baseRow)) {
|
|
4059
|
+
if (columns.length >= MAX_CELL_READ_REFS) break;
|
|
4060
|
+
if (datasetColumns.has(column)) continue;
|
|
4061
|
+
if (!shouldPersistMapCellField(column)) continue;
|
|
4062
|
+
if (column.startsWith('__deepline')) continue;
|
|
4063
|
+
columns.push(column);
|
|
4064
|
+
}
|
|
4065
|
+
return columns;
|
|
4066
|
+
}
|
|
4067
|
+
|
|
3349
4068
|
private emitScopedFieldMetaUpdate(input: {
|
|
3350
4069
|
rowId: number;
|
|
3351
4070
|
key: string | null;
|
|
@@ -3375,6 +4094,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3375
4094
|
toolId?: string | null;
|
|
3376
4095
|
runId?: string | null;
|
|
3377
4096
|
} | null;
|
|
4097
|
+
/**
|
|
4098
|
+
* A newly started producer attempt for this cell (ADR 0018). The record is
|
|
4099
|
+
* appended to the cell's bounded `producers` trace and kept by reference so
|
|
4100
|
+
* the caller can settle its outcome/duration in place.
|
|
4101
|
+
*/
|
|
4102
|
+
producerAttempt?: PlayCellProducerAttempt;
|
|
3378
4103
|
dataPatch?: Record<string, unknown>;
|
|
3379
4104
|
}): void {
|
|
3380
4105
|
if (!input.fieldName) {
|
|
@@ -3389,6 +4114,46 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3389
4114
|
return;
|
|
3390
4115
|
}
|
|
3391
4116
|
|
|
4117
|
+
// Every patch for a cell re-states its producer trace. The in-memory cell
|
|
4118
|
+
// patch merge replaces a field wholesale, so carrying the array forward is
|
|
4119
|
+
// what keeps a waterfall's losing legs alive through the cell's completion
|
|
4120
|
+
// patch (which deliberately omits `producer`).
|
|
4121
|
+
//
|
|
4122
|
+
// Everything from here to `rowReads` is the attribution capture the docflow
|
|
4123
|
+
// campaign added (ADR 0018 producers, ADR 0019 reads/decide). Resolved as a
|
|
4124
|
+
// block behind the run's rollout answer so that an ungated run emits the
|
|
4125
|
+
// cell patch main emitted, key for key — including OMITTING `producer` when
|
|
4126
|
+
// the caller passed none, which is the one pre-existing key whose behavior
|
|
4127
|
+
// the re-statement rule changed.
|
|
4128
|
+
const capture = this.docflowCaptureEnabled;
|
|
4129
|
+
const { producers, attemptsDropped } = capture
|
|
4130
|
+
? this.resolveCellProducers(
|
|
4131
|
+
input.key,
|
|
4132
|
+
input.fieldName,
|
|
4133
|
+
input.producerAttempt,
|
|
4134
|
+
)
|
|
4135
|
+
: { producers: undefined, attemptsDropped: 0 };
|
|
4136
|
+
// The durable jsonb merge preserves keys a patch omits; the in-memory cell
|
|
4137
|
+
// patch merge replaces the field wholesale. Re-state the winning producer
|
|
4138
|
+
// so the cell's terminal write does not erase it (ADR 0018).
|
|
4139
|
+
const producer =
|
|
4140
|
+
input.producer !== undefined
|
|
4141
|
+
? input.producer
|
|
4142
|
+
: capture
|
|
4143
|
+
? this.resolveCellProducer(input.key, input.fieldName)
|
|
4144
|
+
: undefined;
|
|
4145
|
+
// Same re-statement discipline for the provenance capture (ADR 0019): both
|
|
4146
|
+
// were seeded before the cell resolved, and the wholesale field replace
|
|
4147
|
+
// would otherwise drop them on the cell's terminal patch.
|
|
4148
|
+
const reads = capture
|
|
4149
|
+
? this.resolveCellReads(input.key, input.fieldName)
|
|
4150
|
+
: undefined;
|
|
4151
|
+
const decide = capture
|
|
4152
|
+
? this.resolveCellDecision(input.key, input.fieldName)
|
|
4153
|
+
: undefined;
|
|
4154
|
+
// The row's read order is row-grain, so it rides the patch once under the
|
|
4155
|
+
// reserved key rather than being copied onto every cell.
|
|
4156
|
+
const rowReads = capture ? this.resolveRowReadOrder(input.key) : undefined;
|
|
3392
4157
|
this.emitScopedRowUpdate(input.key, input.tableNamespace, {
|
|
3393
4158
|
rowId: input.rowId,
|
|
3394
4159
|
status: input.rowStatus,
|
|
@@ -3396,13 +4161,27 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3396
4161
|
provider: input.provider ?? null,
|
|
3397
4162
|
error: input.error ?? null,
|
|
3398
4163
|
dataPatch: input.dataPatch ?? {},
|
|
4164
|
+
...(rowReads ? { rowMetaPatch: { reads: rowReads } } : {}),
|
|
3399
4165
|
cellMetaPatch: {
|
|
3400
4166
|
[input.fieldName]: {
|
|
3401
4167
|
status: input.status,
|
|
3402
4168
|
stage: input.stage ?? null,
|
|
3403
4169
|
provider: input.provider ?? null,
|
|
3404
4170
|
error: input.error ?? null,
|
|
3405
|
-
...(
|
|
4171
|
+
...(producer !== undefined ? { producer } : {}),
|
|
4172
|
+
// Only attribution-bearing cells carry the run stamp and the trace.
|
|
4173
|
+
// Both ride the retained-row payload, so paying for them on every
|
|
4174
|
+
// status-only cell patch would move the map memory budget.
|
|
4175
|
+
...(producers !== undefined
|
|
4176
|
+
? {
|
|
4177
|
+
runId: this.currentRunId,
|
|
4178
|
+
producers,
|
|
4179
|
+
// Loud under-count: a capped trace states what it lost.
|
|
4180
|
+
...(attemptsDropped > 0 ? { attemptsDropped } : {}),
|
|
4181
|
+
}
|
|
4182
|
+
: {}),
|
|
4183
|
+
...(reads !== undefined ? { reads } : {}),
|
|
4184
|
+
...(decide !== undefined ? { decide } : {}),
|
|
3406
4185
|
...(input.reused !== undefined ? { reused: input.reused } : {}),
|
|
3407
4186
|
...(input.completedAt !== undefined
|
|
3408
4187
|
? { completedAt: input.completedAt }
|
|
@@ -3416,53 +4195,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3416
4195
|
});
|
|
3417
4196
|
}
|
|
3418
4197
|
|
|
3419
|
-
private emitCellUpdate(input: {
|
|
3420
|
-
rowId: number;
|
|
3421
|
-
key: string | null;
|
|
3422
|
-
tableNamespace: string | null;
|
|
3423
|
-
columnName: string;
|
|
3424
|
-
status:
|
|
3425
|
-
| 'queued'
|
|
3426
|
-
| 'running'
|
|
3427
|
-
| 'completed'
|
|
3428
|
-
| 'failed'
|
|
3429
|
-
| 'cached'
|
|
3430
|
-
| 'missed'
|
|
3431
|
-
| 'skipped';
|
|
3432
|
-
rowStatus?: PlayRowUpdate['status'];
|
|
3433
|
-
stage?: string | null;
|
|
3434
|
-
provider?: string | null;
|
|
3435
|
-
error?: string | null;
|
|
3436
|
-
producer?: {
|
|
3437
|
-
kind: 'play' | 'tool' | 'code';
|
|
3438
|
-
id?: string | null;
|
|
3439
|
-
displayName?: string | null;
|
|
3440
|
-
playId?: string | null;
|
|
3441
|
-
toolId?: string | null;
|
|
3442
|
-
runId?: string | null;
|
|
3443
|
-
} | null;
|
|
3444
|
-
value?: unknown;
|
|
3445
|
-
}): void {
|
|
3446
|
-
this.emitScopedRowUpdate(input.key, input.tableNamespace, {
|
|
3447
|
-
rowId: input.rowId,
|
|
3448
|
-
status: input.rowStatus,
|
|
3449
|
-
stage: input.stage ?? null,
|
|
3450
|
-
provider: input.provider ?? null,
|
|
3451
|
-
error: input.error ?? null,
|
|
3452
|
-
dataPatch:
|
|
3453
|
-
input.value === undefined ? {} : { [input.columnName]: input.value },
|
|
3454
|
-
cellMetaPatch: {
|
|
3455
|
-
[input.columnName]: {
|
|
3456
|
-
status: input.status,
|
|
3457
|
-
stage: input.stage ?? null,
|
|
3458
|
-
provider: input.provider ?? null,
|
|
3459
|
-
error: input.error ?? null,
|
|
3460
|
-
...(input.producer !== undefined ? { producer: input.producer } : {}),
|
|
3461
|
-
},
|
|
3462
|
-
},
|
|
3463
|
-
});
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
4198
|
private isCompletedFieldValue(value: unknown): boolean {
|
|
3467
4199
|
return (
|
|
3468
4200
|
value !== null &&
|
|
@@ -4327,6 +5059,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4327
5059
|
options.onRowError,
|
|
4328
5060
|
);
|
|
4329
5061
|
validateOptionalPlayAuthoringField('ctx.dataset.run.mode', options.mode);
|
|
5062
|
+
validateOptionalPlayAuthoringField(
|
|
5063
|
+
'ctx.dataset.run.undrawnColumns',
|
|
5064
|
+
options.undrawnColumns,
|
|
5065
|
+
);
|
|
4330
5066
|
if (options.key !== undefined) {
|
|
4331
5067
|
validatePlayAuthoringField('ctx.dataset.run.key', options.key);
|
|
4332
5068
|
}
|
|
@@ -5732,6 +6468,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
5732
6468
|
): Promise<FieldMapRunResult> {
|
|
5733
6469
|
const fieldEntries = Object.entries(definition);
|
|
5734
6470
|
const datasetColumnNames = fieldEntries.map(([fieldName]) => fieldName);
|
|
6471
|
+
/** Columns this map writes; excluded from a row's input read order. */
|
|
6472
|
+
const datasetColumnSet = new Set(datasetColumnNames);
|
|
5735
6473
|
const visibleFields = fieldEntries
|
|
5736
6474
|
.map(([fieldName]) => fieldName)
|
|
5737
6475
|
.filter((fieldName) => shouldPersistMapCellField(fieldName));
|
|
@@ -6046,6 +6784,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6046
6784
|
const rowIndex = executionRowIndex(idx);
|
|
6047
6785
|
const computedFields: Record<string, unknown> = {};
|
|
6048
6786
|
let activeFieldName: string | null = null;
|
|
6787
|
+
// ADR 0019: the row's read order, computed once. Every cell reads a
|
|
6788
|
+
// prefix of it — the inputs, then each column this run wrote — so this
|
|
6789
|
+
// loop appends rather than rebuilding a per-cell set (which was
|
|
6790
|
+
// O(columns squared) in both time and allocations).
|
|
6791
|
+
if (this.docflowCaptureEnabled) {
|
|
6792
|
+
this.recordRowReadColumns(
|
|
6793
|
+
rowKey,
|
|
6794
|
+
this.rowInputReadColumns(baseRow, datasetColumnSet),
|
|
6795
|
+
);
|
|
6796
|
+
}
|
|
6797
|
+
if (this.docflowCaptureEnabled) {
|
|
6798
|
+
this.seedActiveCellProducersFromDurableRow(rowKey, baseRow);
|
|
6799
|
+
}
|
|
6049
6800
|
|
|
6050
6801
|
// Global row slot keeps concurrent maps in the same run under rowMax. The
|
|
6051
6802
|
// worker pool below enforces this map's requested/default concurrency.
|
|
@@ -6055,6 +6806,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6055
6806
|
try {
|
|
6056
6807
|
for (const [fieldName, resolver] of fieldEntries) {
|
|
6057
6808
|
activeFieldName = fieldName;
|
|
6809
|
+
// Recorded before the first patch for the cell, so every later patch
|
|
6810
|
+
// (tool legs, completion) re-states the row order for free.
|
|
6811
|
+
this.recordRowReadCell(
|
|
6812
|
+
rowKey,
|
|
6813
|
+
fieldName,
|
|
6814
|
+
shouldPersistMapCellField(fieldName),
|
|
6815
|
+
);
|
|
6058
6816
|
this.emitScopedFieldMetaUpdate({
|
|
6059
6817
|
rowId: idx,
|
|
6060
6818
|
key: rowKey,
|
|
@@ -6495,8 +7253,27 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6495
7253
|
): Promise<unknown> {
|
|
6496
7254
|
let currentRow = cloneCsvAliasedRow(row);
|
|
6497
7255
|
const produced: Record<string, unknown> = {};
|
|
7256
|
+
// Steps this program has not run yet. Hoisted per program, not rebuilt per
|
|
7257
|
+
// step, and shrunk as each step produces.
|
|
7258
|
+
const pendingStepNames = new Set(program.steps.map((step) => step.name));
|
|
6498
7259
|
for (const step of program.steps) {
|
|
6499
7260
|
const stepPath = [...path, step.name];
|
|
7261
|
+
// ADR 0019: a step's read-set is the row as it stands *now*, which
|
|
7262
|
+
// includes every earlier step's output. Sibling steps are not part of the
|
|
7263
|
+
// row's column order, so a step cell keeps an explicit array where a
|
|
7264
|
+
// dataset column resolves through the row-grain cut point. Recorded
|
|
7265
|
+
// before the step runs so the step's own cell patches re-state it.
|
|
7266
|
+
const stepRowStore = rowContext.getStore();
|
|
7267
|
+
if (this.docflowCaptureEnabled && stepRowStore?.rowKey) {
|
|
7268
|
+
this.recordCellProvenance(stepRowStore.rowKey, stepPath.join('.'), {
|
|
7269
|
+
reads: this.cellReadRefsForRow(
|
|
7270
|
+
stepRowStore.rowKey,
|
|
7271
|
+
[currentRow],
|
|
7272
|
+
stepPath.join('.'),
|
|
7273
|
+
pendingStepNames,
|
|
7274
|
+
),
|
|
7275
|
+
});
|
|
7276
|
+
}
|
|
6500
7277
|
const runStep = async () =>
|
|
6501
7278
|
options?.checkpointSteps
|
|
6502
7279
|
? await this.step(
|
|
@@ -6554,10 +7331,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6554
7331
|
});
|
|
6555
7332
|
}
|
|
6556
7333
|
produced[step.name] = value;
|
|
7334
|
+
pendingStepNames.delete(step.name);
|
|
6557
7335
|
currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
|
|
6558
7336
|
continue;
|
|
6559
7337
|
}
|
|
6560
7338
|
produced[step.name] = value;
|
|
7339
|
+
pendingStepNames.delete(step.name);
|
|
6561
7340
|
const rowStore = rowContext.getStore();
|
|
6562
7341
|
const fieldName = stepPath.join('.');
|
|
6563
7342
|
const patchFieldName = runtimeSheetPatchFieldName(fieldName);
|
|
@@ -6615,6 +7394,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6615
7394
|
|
|
6616
7395
|
if (isRuntimeConditionalStepResolver(resolver)) {
|
|
6617
7396
|
const shouldRun = await resolver.when(currentRow, index);
|
|
7397
|
+
// ADR 0019: the per-row branch this evaluator selected, written onto the
|
|
7398
|
+
// cell the decision governs. `run`/`else` is the runtime's own vocabulary
|
|
7399
|
+
// for a `runIf` conditional — the only per-row branch construct the
|
|
7400
|
+
// runtime evaluates. A branch inside authored code is invisible here and
|
|
7401
|
+
// degrades to the transform's read-set, which is the honest floor.
|
|
7402
|
+
if (this.docflowCaptureEnabled && store?.rowKey) {
|
|
7403
|
+
// The evaluator was handed the same row the resolver would get, so the
|
|
7404
|
+
// decision's read-set is the cell's read-set. Reusing the seeded trace
|
|
7405
|
+
// keeps the two consistent and costs no second pass over the row.
|
|
7406
|
+
this.recordCellProvenance(store.rowKey, nestedFieldName, {
|
|
7407
|
+
decide: {
|
|
7408
|
+
branch: shouldRun ? 'run' : 'else',
|
|
7409
|
+
reads: this.effectiveCellReads(store.rowKey, nestedFieldName),
|
|
7410
|
+
},
|
|
7411
|
+
});
|
|
7412
|
+
}
|
|
6618
7413
|
if (!shouldRun) {
|
|
6619
7414
|
const elseValue = Object.prototype.hasOwnProperty.call(
|
|
6620
7415
|
resolver,
|
|
@@ -6680,6 +7475,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6680
7475
|
rowIdentity: (row: Record<string, unknown>, index: number) => string,
|
|
6681
7476
|
): Promise<Array<Record<string, unknown>>> {
|
|
6682
7477
|
const results: Array<Record<string, unknown>> = [];
|
|
7478
|
+
const pureDatasetColumnSet = new Set(
|
|
7479
|
+
fieldEntries.map(([fieldName]) => fieldName),
|
|
7480
|
+
);
|
|
6683
7481
|
this.pureMapExecutionActive = true;
|
|
6684
7482
|
|
|
6685
7483
|
try {
|
|
@@ -6688,6 +7486,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6688
7486
|
const baseRow = this.toOutputRow(item);
|
|
6689
7487
|
const computedFields: Record<string, unknown> = {};
|
|
6690
7488
|
let activeFieldName: string | null = null;
|
|
7489
|
+
// The pure path emits one consolidated patch per row, so the read
|
|
7490
|
+
// trace is stamped straight onto that patch (ADR 0019) instead of
|
|
7491
|
+
// riding the seed-and-re-state path the row loop uses.
|
|
7492
|
+
const pureRowKey = rowIdentity(baseRow, index);
|
|
6691
7493
|
|
|
6692
7494
|
// Pure rows complete in microseconds, so interim per-field updates
|
|
6693
7495
|
// carry no live value. Accumulate every field's data/meta patch and
|
|
@@ -6698,9 +7500,39 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6698
7500
|
const rowDataPatch: Record<string, unknown> = {};
|
|
6699
7501
|
const rowCellMetaPatch: NonNullable<PlayRowUpdate['cellMetaPatch']> =
|
|
6700
7502
|
{};
|
|
7503
|
+
// ADR 0019 row-grain read order, built once per row on the consolidated
|
|
7504
|
+
// patch this path already emits.
|
|
7505
|
+
//
|
|
7506
|
+
// Ungated this stays empty, which is the gate: the per-field branch
|
|
7507
|
+
// below already treats an empty order as "nothing was available to
|
|
7508
|
+
// read" and does no work, and the two `length > 0` guards on the
|
|
7509
|
+
// consolidated patches omit `rowMetaPatch` entirely. This path never
|
|
7510
|
+
// touches `activeMapCellMeta`, so it has to be stopped here or not at
|
|
7511
|
+
// all.
|
|
7512
|
+
const pureReadColumns = this.docflowCaptureEnabled
|
|
7513
|
+
? this.rowInputReadColumns(baseRow, pureDatasetColumnSet)
|
|
7514
|
+
: [];
|
|
7515
|
+
const pureReadUpto: Record<string, number> = {};
|
|
6701
7516
|
try {
|
|
6702
7517
|
for (const [fieldName, resolver] of fieldEntries) {
|
|
6703
7518
|
activeFieldName = fieldName;
|
|
7519
|
+
// A written column joins the order before its own cell runs, so
|
|
7520
|
+
// its position is its cut; anything the list cannot place states
|
|
7521
|
+
// an explicit one.
|
|
7522
|
+
if (pureReadColumns.length === 0) {
|
|
7523
|
+
// Nothing was available to read.
|
|
7524
|
+
} else if (
|
|
7525
|
+
shouldPersistMapCellField(fieldName) &&
|
|
7526
|
+
pureReadColumns.length < MAX_CELL_READ_REFS &&
|
|
7527
|
+
!pureReadColumns.includes(fieldName)
|
|
7528
|
+
) {
|
|
7529
|
+
pureReadColumns.push(fieldName);
|
|
7530
|
+
} else if (
|
|
7531
|
+
Object.keys(pureReadUpto).length < MAX_ROW_READ_CELLS &&
|
|
7532
|
+
!pureReadColumns.includes(fieldName)
|
|
7533
|
+
) {
|
|
7534
|
+
pureReadUpto[fieldName] = pureReadColumns.length;
|
|
7535
|
+
}
|
|
6704
7536
|
const value = await this.resolveMapFieldValue(
|
|
6705
7537
|
resolver,
|
|
6706
7538
|
item,
|
|
@@ -6725,20 +7557,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6725
7557
|
this.toPublicOutputRow(cloneCsvAliasedRow(baseRow, computedFields)),
|
|
6726
7558
|
);
|
|
6727
7559
|
|
|
6728
|
-
const rowKey = rowIdentity(baseRow, index);
|
|
6729
7560
|
activeFieldName = null;
|
|
6730
|
-
this.emitScopedRowUpdate(
|
|
7561
|
+
this.emitScopedRowUpdate(pureRowKey, tableNamespace, {
|
|
6731
7562
|
rowId: index,
|
|
6732
7563
|
status: 'completed',
|
|
6733
7564
|
stage: 'completed',
|
|
6734
7565
|
provider: null,
|
|
6735
7566
|
error: null,
|
|
6736
7567
|
dataPatch: rowDataPatch,
|
|
7568
|
+
...(pureReadColumns.length > 0
|
|
7569
|
+
? {
|
|
7570
|
+
rowMetaPatch: {
|
|
7571
|
+
reads: {
|
|
7572
|
+
columns: pureReadColumns,
|
|
7573
|
+
...(Object.keys(pureReadUpto).length > 0
|
|
7574
|
+
? { upto: pureReadUpto }
|
|
7575
|
+
: {}),
|
|
7576
|
+
},
|
|
7577
|
+
},
|
|
7578
|
+
}
|
|
7579
|
+
: {}),
|
|
6737
7580
|
cellMetaPatch: rowCellMetaPatch,
|
|
6738
7581
|
});
|
|
6739
7582
|
} catch (error) {
|
|
6740
|
-
|
|
6741
|
-
this.emitScopedRowUpdate(rowKey, tableNamespace, {
|
|
7583
|
+
this.emitScopedRowUpdate(pureRowKey, tableNamespace, {
|
|
6742
7584
|
rowId: index,
|
|
6743
7585
|
status: 'failed',
|
|
6744
7586
|
stage: 'failed',
|
|
@@ -6747,6 +7589,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6747
7589
|
// Carry the cells that completed before the failure — previously
|
|
6748
7590
|
// they had already been emitted as individual field updates.
|
|
6749
7591
|
dataPatch: rowDataPatch,
|
|
7592
|
+
...(pureReadColumns.length > 0
|
|
7593
|
+
? {
|
|
7594
|
+
rowMetaPatch: {
|
|
7595
|
+
reads: {
|
|
7596
|
+
columns: pureReadColumns,
|
|
7597
|
+
...(Object.keys(pureReadUpto).length > 0
|
|
7598
|
+
? { upto: pureReadUpto }
|
|
7599
|
+
: {}),
|
|
7600
|
+
},
|
|
7601
|
+
},
|
|
7602
|
+
}
|
|
7603
|
+
: {}),
|
|
6750
7604
|
cellMetaPatch: {
|
|
6751
7605
|
...rowCellMetaPatch,
|
|
6752
7606
|
[String(activeFieldName ?? '__unknown')]: {
|
|
@@ -7239,89 +8093,150 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7239
8093
|
);
|
|
7240
8094
|
if (cached) {
|
|
7241
8095
|
this.log(`Calling tool: ${toolId} recovered from checkpoint`);
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
8096
|
+
// Started and settled are paired in a finally: an uncovered throw
|
|
8097
|
+
// between them strands the tool node `running` forever in the
|
|
8098
|
+
// snapshot, which is exactly the lossy state ADR 0018 removed.
|
|
8099
|
+
this.emitToolCallLifecycle('started', toolId, normalizedKey);
|
|
8100
|
+
let settled = false;
|
|
8101
|
+
try {
|
|
8102
|
+
const recovered = await this.wrapToolExecutionResult({
|
|
8103
|
+
toolId,
|
|
8104
|
+
status: cached.result.result == null ? 'no_result' : 'completed',
|
|
8105
|
+
result: cached.result.result,
|
|
8106
|
+
requestInput: input,
|
|
8107
|
+
execution: toolExecutionMetadataForOutcome({
|
|
8108
|
+
kind: 'checkpoint',
|
|
8109
|
+
cacheKey: cached.cacheKey,
|
|
8110
|
+
}),
|
|
8111
|
+
});
|
|
8112
|
+
settled = true;
|
|
8113
|
+
this.emitToolCallLifecycle('settled', toolId, normalizedKey, {
|
|
8114
|
+
outcome: 'cached',
|
|
8115
|
+
durationMs: 0,
|
|
8116
|
+
});
|
|
8117
|
+
return recovered;
|
|
8118
|
+
} finally {
|
|
8119
|
+
if (!settled) {
|
|
8120
|
+
this.emitToolCallLifecycle('settled', toolId, normalizedKey, {
|
|
8121
|
+
outcome: 'failed',
|
|
8122
|
+
durationMs: 0,
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
7252
8126
|
}
|
|
7253
8127
|
this.log(
|
|
7254
8128
|
toolCachePolicy.force
|
|
7255
8129
|
? `Calling tool: ${toolId} (force)`
|
|
7256
8130
|
: `Calling tool: ${toolId}`,
|
|
7257
8131
|
);
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
8132
|
+
// ADR 0018: the tool node's lifecycle is an explicit typed event, not a
|
|
8133
|
+
// regex over this log line.
|
|
8134
|
+
const directCallStartedAt = Date.now();
|
|
8135
|
+
this.emitToolCallLifecycle('started', toolId, normalizedKey);
|
|
8136
|
+
// Settled exactly once, including on paths that throw before the
|
|
8137
|
+
// explicit settle below (result wrapping, receipt-ownership failures,
|
|
8138
|
+
// authoring validation). `executeDirectToolCall` settles its own
|
|
8139
|
+
// transport failure and marks this handled.
|
|
8140
|
+
let directSettled = false;
|
|
8141
|
+
const settleDirectCall = (
|
|
8142
|
+
outcome: 'completed' | 'no_result' | 'failed' | 'cached',
|
|
8143
|
+
error?: unknown,
|
|
8144
|
+
) => {
|
|
8145
|
+
if (directSettled) return;
|
|
8146
|
+
directSettled = true;
|
|
8147
|
+
this.emitToolCallLifecycle('settled', toolId, normalizedKey, {
|
|
8148
|
+
outcome,
|
|
8149
|
+
durationMs: Date.now() - directCallStartedAt,
|
|
8150
|
+
...(error !== undefined
|
|
8151
|
+
? { error: this.formatRuntimeError(error) }
|
|
8152
|
+
: {}),
|
|
8153
|
+
});
|
|
8154
|
+
};
|
|
8155
|
+
try {
|
|
8156
|
+
const directReceiptLeaseId = context?.leaseId?.trim() || null;
|
|
8157
|
+
if (!logicalCallId) {
|
|
8158
|
+
throw new Error(
|
|
8159
|
+
'Direct tool execution requires a stable logical call identity.',
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8162
|
+
const physicalDirectKey = cacheableToolResult
|
|
8163
|
+
? directCacheKey
|
|
8164
|
+
: `${buildDurableToolReceiptPrefix({
|
|
8165
|
+
orgId: this.#options.orgId ?? 'unknown-org',
|
|
8166
|
+
toolId,
|
|
8167
|
+
})}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
|
|
8168
|
+
const execution = await this.executeDirectToolCall({
|
|
7282
8169
|
toolId,
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
8170
|
+
callKey: normalizedKey,
|
|
8171
|
+
startedAt: directCallStartedAt,
|
|
8172
|
+
input,
|
|
8173
|
+
options: {
|
|
8174
|
+
durableCallReceiptKey: physicalDirectKey,
|
|
8175
|
+
playNodeScope: buildPlayNodeScope({
|
|
8176
|
+
toolId,
|
|
8177
|
+
callKey: normalizedKey,
|
|
8178
|
+
}),
|
|
8179
|
+
executionAuthScopeDigest,
|
|
8180
|
+
providerIdempotencyKey: cacheableToolResult
|
|
8181
|
+
? this.providerIdempotencyKeyForToolCall({
|
|
8182
|
+
cacheKey: directCacheKey,
|
|
8183
|
+
force: toolCachePolicy.force,
|
|
8184
|
+
leaseId: directReceiptLeaseId,
|
|
8185
|
+
logicalCallId,
|
|
8186
|
+
})
|
|
8187
|
+
: physicalDirectKey,
|
|
8188
|
+
timeoutMs: resolveToolRuntimeTimeoutMs(
|
|
8189
|
+
toolId,
|
|
8190
|
+
options?.timeoutMs,
|
|
8191
|
+
this.currentAuthoringContractEdition,
|
|
8192
|
+
),
|
|
8193
|
+
...(directReceiptLeaseId &&
|
|
8194
|
+
(this.#options.heartbeatRuntimeStepReceipts ||
|
|
8195
|
+
this.#options.getRuntimeStepReceipt ||
|
|
8196
|
+
this.#options.getRuntimeStepReceipts)
|
|
8197
|
+
? {
|
|
8198
|
+
beforeProviderCall: async () => {
|
|
8199
|
+
await this.assertRuntimeToolReceiptOwnership([
|
|
8200
|
+
{
|
|
8201
|
+
callId: 'direct',
|
|
8202
|
+
cacheKey: directCacheKey,
|
|
8203
|
+
receiptKey: directCacheKey,
|
|
8204
|
+
receiptLeaseId: directReceiptLeaseId,
|
|
8205
|
+
rowId: -1,
|
|
8206
|
+
toolId,
|
|
8207
|
+
input,
|
|
8208
|
+
},
|
|
8209
|
+
]);
|
|
7301
8210
|
},
|
|
7302
|
-
|
|
7303
|
-
},
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
})
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
8211
|
+
}
|
|
8212
|
+
: {}),
|
|
8213
|
+
},
|
|
8214
|
+
});
|
|
8215
|
+
const wrapped = await this.wrapToolExecutionResult({
|
|
8216
|
+
toolId,
|
|
8217
|
+
status: execution.status,
|
|
8218
|
+
jobId: execution.jobId,
|
|
8219
|
+
result: execution.result,
|
|
8220
|
+
metadata: execution.metadata,
|
|
8221
|
+
meta: execution.meta,
|
|
8222
|
+
requestInput: input,
|
|
8223
|
+
execution: toolExecutionMetadataForOutcome({
|
|
8224
|
+
kind: 'live',
|
|
8225
|
+
cacheKey: directCacheKey,
|
|
8226
|
+
}),
|
|
8227
|
+
});
|
|
8228
|
+
settleDirectCall(toolAttemptOutcomeForResult(wrapped));
|
|
8229
|
+
if (cacheableToolResult) {
|
|
8230
|
+
this.cacheToolResult(toolId, directCacheKey, wrapped, 'direct');
|
|
8231
|
+
this.#options.onBatchComplete?.(this.checkpoint);
|
|
8232
|
+
}
|
|
8233
|
+
return wrapped;
|
|
8234
|
+
} catch (error) {
|
|
8235
|
+
settleDirectCall('failed', error);
|
|
8236
|
+
throw error;
|
|
8237
|
+
} finally {
|
|
8238
|
+
settleDirectCall('failed');
|
|
7323
8239
|
}
|
|
7324
|
-
return wrapped;
|
|
7325
8240
|
}
|
|
7326
8241
|
|
|
7327
8242
|
const rowId = store.rowId;
|
|
@@ -7361,7 +8276,27 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7361
8276
|
});
|
|
7362
8277
|
}
|
|
7363
8278
|
|
|
7364
|
-
|
|
8279
|
+
// ADR 0018: record this leg before it runs. `producer` still names the
|
|
8280
|
+
// last leg (the usual cascade winner); `producers` keeps every leg, which
|
|
8281
|
+
// is the only durable record a losing leg ever gets — a failed tool call
|
|
8282
|
+
// writes no receipt at all.
|
|
8283
|
+
const producerAttemptStartedAt = Date.now();
|
|
8284
|
+
// Deliberately minimal: `displayName` is derivable from `toolId`, and
|
|
8285
|
+
// this record is retained per row until the map's persistence barrier.
|
|
8286
|
+
const producerAttempt: PlayCellProducerAttempt = {
|
|
8287
|
+
kind: 'tool',
|
|
8288
|
+
id: normalizedKey,
|
|
8289
|
+
toolId,
|
|
8290
|
+
at: producerAttemptStartedAt,
|
|
8291
|
+
outcome: 'running',
|
|
8292
|
+
};
|
|
8293
|
+
const settleProducerAttempt = (
|
|
8294
|
+
outcome: PlayCellProducerAttempt['outcome'],
|
|
8295
|
+
) => {
|
|
8296
|
+
producerAttempt.outcome = outcome;
|
|
8297
|
+
producerAttempt.durationMs = Date.now() - producerAttemptStartedAt;
|
|
8298
|
+
};
|
|
8299
|
+
const pendingToolCall = new Promise((resolve, reject) => {
|
|
7365
8300
|
this.toolCallResolvers.set(callId, { resolve, reject });
|
|
7366
8301
|
this.emitScopedFieldMetaUpdate({
|
|
7367
8302
|
rowId,
|
|
@@ -7379,6 +8314,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7379
8314
|
toolId,
|
|
7380
8315
|
displayName: displayNameFromProducerId(toolId),
|
|
7381
8316
|
},
|
|
8317
|
+
producerAttempt,
|
|
7382
8318
|
dataPatch: {},
|
|
7383
8319
|
});
|
|
7384
8320
|
const timeoutMs = resolveToolRuntimeTimeoutMs(
|
|
@@ -7396,6 +8332,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7396
8332
|
forceFailedRefresh: toolCachePolicy.forceFailedRefresh,
|
|
7397
8333
|
rowId,
|
|
7398
8334
|
fieldName,
|
|
8335
|
+
contextKey: normalizedKey,
|
|
7399
8336
|
toolId,
|
|
7400
8337
|
input,
|
|
7401
8338
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
@@ -7407,6 +8344,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7407
8344
|
description: normalizeStepDescription(options?.description),
|
|
7408
8345
|
});
|
|
7409
8346
|
});
|
|
8347
|
+
return await pendingToolCall.then(
|
|
8348
|
+
(value) => {
|
|
8349
|
+
settleProducerAttempt(toolAttemptOutcomeForResult(value));
|
|
8350
|
+
return value;
|
|
8351
|
+
},
|
|
8352
|
+
(error: unknown) => {
|
|
8353
|
+
settleProducerAttempt('failed');
|
|
8354
|
+
throw error;
|
|
8355
|
+
},
|
|
8356
|
+
);
|
|
7410
8357
|
};
|
|
7411
8358
|
|
|
7412
8359
|
if (store || !cacheableToolResult) {
|
|
@@ -8845,6 +9792,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8845
9792
|
beforeProviderCall: () =>
|
|
8846
9793
|
this.assertRuntimeToolReceiptOwnership([owner]),
|
|
8847
9794
|
durableCallReceiptKey: receiptKey,
|
|
9795
|
+
playNodeScope: playNodeScopeForToolCallRequest(owner),
|
|
8848
9796
|
executionAuthScopeDigest: owner.executionAuthScopeDigest,
|
|
8849
9797
|
receiptLeaseExpiresAt: owner.receiptLeaseExpiresAt,
|
|
8850
9798
|
heartbeatReceipt: () =>
|
|
@@ -9096,6 +10044,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9096
10044
|
batch.batchPayload,
|
|
9097
10045
|
{
|
|
9098
10046
|
durableCallReceiptKey: aggregateReceiptKey,
|
|
10047
|
+
// A native batch is one physical provider request for
|
|
10048
|
+
// many rows of the same column, so the batch's node scope
|
|
10049
|
+
// is its members' shared scope.
|
|
10050
|
+
playNodeScope: batch.memberRequests[0]
|
|
10051
|
+
? playNodeScopeForToolCallRequest(
|
|
10052
|
+
batch.memberRequests[0],
|
|
10053
|
+
batch.batchOperation,
|
|
10054
|
+
)
|
|
10055
|
+
: null,
|
|
9099
10056
|
executionAuthScopeDigest:
|
|
9100
10057
|
batch.memberRequests[0]?.executionAuthScopeDigest ??
|
|
9101
10058
|
null,
|
|
@@ -9284,6 +10241,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9284
10241
|
{
|
|
9285
10242
|
beforeProviderCall: () =>
|
|
9286
10243
|
this.assertRuntimeToolReceiptOwnership([request]),
|
|
10244
|
+
playNodeScope: playNodeScopeForToolCallRequest(request),
|
|
9287
10245
|
...(request.receiptKey
|
|
9288
10246
|
? {
|
|
9289
10247
|
durableCallReceiptKey: request.receiptKey,
|
|
@@ -9588,6 +10546,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9588
10546
|
const deeplineRequestId = providerIdempotencyKey
|
|
9589
10547
|
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
9590
10548
|
: `ctx-tool-${crypto.randomUUID()}`;
|
|
10549
|
+
// Every runtime provider call carries at least a tool-scoped
|
|
10550
|
+
// attribution. Callers that know the execution location (row-scoped
|
|
10551
|
+
// map cells, batched owners) supply the fuller scope.
|
|
10552
|
+
const playNodeScope =
|
|
10553
|
+
options?.playNodeScope ?? buildPlayNodeScope({ toolId });
|
|
9591
10554
|
const serializedRequestBody = (invocationAttempt: number) =>
|
|
9592
10555
|
JSON.stringify({
|
|
9593
10556
|
payload: toolInputSnapshot,
|
|
@@ -9631,6 +10594,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9631
10594
|
...(providerIdempotencyKey
|
|
9632
10595
|
? { provider_idempotency_key: providerIdempotencyKey }
|
|
9633
10596
|
: {}),
|
|
10597
|
+
...(playNodeScope
|
|
10598
|
+
? { play_node_scope: playNodeScopeToWire(playNodeScope) }
|
|
10599
|
+
: {}),
|
|
9634
10600
|
},
|
|
9635
10601
|
...(this.#options.integrationMode
|
|
9636
10602
|
? { integration_mode: this.#options.integrationMode }
|