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
|
@@ -46,6 +46,20 @@ export type RuntimeAuthorityDescriptor = {
|
|
|
46
46
|
synthetic?: boolean | null;
|
|
47
47
|
actorUserId?: string | null;
|
|
48
48
|
actorEmail?: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Whether THIS run may capture the docflow provenance trace.
|
|
51
|
+
*
|
|
52
|
+
* Resolved once, server-side, at run admission — the only place that has both
|
|
53
|
+
* an actor and the rollout catalog. The runtime executes in a sandbox or on
|
|
54
|
+
* the customer's machine with no auth context and must never ask: capture is
|
|
55
|
+
* per-row on the hottest path there is, so the answer has to arrive as a fact
|
|
56
|
+
* it was handed, like `maxCreditsPerRun` and `integrationMode` beside it.
|
|
57
|
+
*
|
|
58
|
+
* Absent means OFF. A launch queued before this field existed, a scheduler
|
|
59
|
+
* that drops unknown keys, a caller that forgets to thread it — all land on
|
|
60
|
+
* the pre-feature behavior, which is the whole point of the switch.
|
|
61
|
+
*/
|
|
62
|
+
docflowEnabled?: boolean | null;
|
|
49
63
|
capabilities: RuntimeExecutionCapability[];
|
|
50
64
|
};
|
|
51
65
|
|
|
@@ -128,6 +142,10 @@ export function requireRuntimeAuthorityDescriptor(
|
|
|
128
142
|
synthetic: descriptor.synthetic === true,
|
|
129
143
|
actorUserId: descriptor.actorUserId ?? null,
|
|
130
144
|
actorEmail: descriptor.actorEmail ?? null,
|
|
145
|
+
// `=== true`, not `?? false`: the normalizer is what a durable launch is
|
|
146
|
+
// read back through, so anything that is not an explicit yes — missing,
|
|
147
|
+
// null, a string that survived a JSON round trip — denies.
|
|
148
|
+
docflowEnabled: descriptor.docflowEnabled === true,
|
|
131
149
|
capabilities,
|
|
132
150
|
};
|
|
133
151
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PlayRunLifecycleStatus } from './run-lifecycle-policy';
|
|
2
|
+
import type { PlayDocflowNodeIoState } from './docflow-node-io';
|
|
2
3
|
import type {
|
|
3
4
|
PlayActivityObservation,
|
|
4
5
|
PlayRunActivityProjection,
|
|
@@ -28,6 +29,36 @@ export type PlayVisualNodeProgressSnapshot = {
|
|
|
28
29
|
startedAt?: number | null;
|
|
29
30
|
completedAt?: number | null;
|
|
30
31
|
artifactTableNamespace?: string | null;
|
|
32
|
+
nodeIo?: PlayDocflowNodeIoState;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Observed provider usage for one graph node (ADR 0018). Server-computed at
|
|
37
|
+
* read time from the Runtime Sheet producer trace plus the node-scoped usage
|
|
38
|
+
* events; never derived in the browser and never from event counters.
|
|
39
|
+
*/
|
|
40
|
+
export type PlayVisualNodeToolUsageSnapshot = {
|
|
41
|
+
toolId: string;
|
|
42
|
+
provider: string;
|
|
43
|
+
column?: string | null;
|
|
44
|
+
/** Per-row logical invocations, including losing cascade legs. */
|
|
45
|
+
logicalCalls: number;
|
|
46
|
+
/** Physical provider requests (one native batch answers many rows). */
|
|
47
|
+
providerRequests: number;
|
|
48
|
+
winningCells: number;
|
|
49
|
+
reusedCells: number;
|
|
50
|
+
failedCalls: number;
|
|
51
|
+
credits: number;
|
|
52
|
+
deeplineCostUsd: number;
|
|
53
|
+
durationMsTotal: number | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type PlayVisualNodeUsageSnapshot = {
|
|
57
|
+
tools: PlayVisualNodeToolUsageSnapshot[];
|
|
58
|
+
totalCredits: number;
|
|
59
|
+
totalProviderRequests: number;
|
|
60
|
+
totalLogicalCalls: number;
|
|
61
|
+
totalDurationMs: number | null;
|
|
31
62
|
};
|
|
32
63
|
|
|
33
64
|
export type PlayVisualNodeStateSnapshot = {
|
|
@@ -39,6 +70,8 @@ export type PlayVisualNodeStateSnapshot = {
|
|
|
39
70
|
startedAt?: number | null;
|
|
40
71
|
completedAt?: number | null;
|
|
41
72
|
updatedAt?: number | null;
|
|
73
|
+
nodeIo?: PlayDocflowNodeIoState;
|
|
74
|
+
usage?: PlayVisualNodeUsageSnapshot;
|
|
42
75
|
};
|
|
43
76
|
|
|
44
77
|
export type PlayRunLiveSnapshot = {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Play Runtime Log Provenance.
|
|
3
|
+
*
|
|
4
|
+
* A play run emits a stream of log / step / progress lines from very different
|
|
5
|
+
* places: the customer's own play code, the runtime's step lifecycle, replays
|
|
6
|
+
* that re-run already-completed work, scheduler/sandbox internals, warnings,
|
|
7
|
+
* and receipt/billing plumbing. On the wire they are all plain strings in one
|
|
8
|
+
* ordered log buffer (`ctx.getLogs()` → `play.run.log`), so historically each
|
|
9
|
+
* consumer re-derived "is this line noise?" with its own ad-hoc substring
|
|
10
|
+
* match (`isInternalLogLine` in the dashboard, the `[worker] ...` regex ladder
|
|
11
|
+
* in the SDK watch renderer). Those drifted apart and leaked internal churn to
|
|
12
|
+
* users.
|
|
13
|
+
*
|
|
14
|
+
* This module is the single source of truth. It defines:
|
|
15
|
+
* 1. `LogProvenance` — a CLOSED discriminated set naming WHO/WHY emitted a
|
|
16
|
+
* line. Adding a class is a compile error everywhere that must route it.
|
|
17
|
+
* 2. `LOG_PROVENANCE_POLICY` — the one table mapping each class to the
|
|
18
|
+
* surfaces it reaches (`watch`, `ui`, `debug`). Exhaustive over the union.
|
|
19
|
+
* 3. A structural tag carrier (`PROVENANCE_PREFIX`) so NEW emissions carry
|
|
20
|
+
* their class inline in the log string without breaking persisted-log
|
|
21
|
+
* readers, plus a legacy classifier (`classifyLegacyLogLine`) for old
|
|
22
|
+
* untagged lines.
|
|
23
|
+
*
|
|
24
|
+
* Do NOT re-implement provenance decisions with ad-hoc string matching
|
|
25
|
+
* elsewhere. Import `classifyLogLine` + `logProvenanceReaches`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Closed set of provenance classes. Each names WHO emitted the line and WHY.
|
|
30
|
+
* Fewer, sharper classes on purpose — a consumer only needs to know which
|
|
31
|
+
* surface a line belongs on, not its exact emitter.
|
|
32
|
+
*/
|
|
33
|
+
export type LogProvenance =
|
|
34
|
+
/** Play code output: `ctx.log(...)` and captured `[console.*]` from the
|
|
35
|
+
* play body. The customer wrote this; it is always in-scope everywhere. */
|
|
36
|
+
| 'user'
|
|
37
|
+
/** Runtime step/node lifecycle and map progress narration the runtime emits
|
|
38
|
+
* to explain forward progress. The useful backbone of a `--watch` run. */
|
|
39
|
+
| 'lifecycle'
|
|
40
|
+
/** Deterministic re-execution echoes: a resumed/replayed attempt re-runs
|
|
41
|
+
* already-durable work and reprints the same lines ("recovered from
|
|
42
|
+
* checkpoint", re-narrated step transitions). Real signal only once; the
|
|
43
|
+
* replay copy is churn. */
|
|
44
|
+
| 'replay'
|
|
45
|
+
/** Scheduler / sandbox / worker internals: run-file prep, sandbox
|
|
46
|
+
* lifecycle, `[perf]` timings, batch-drain bookkeeping. Operator-facing. */
|
|
47
|
+
| 'infra'
|
|
48
|
+
/** Warnings and diagnostics: `[warn]`, `[error]`, `[runtime.*_failure]`,
|
|
49
|
+
* and forward-looking hints. Surfaced because a human may need to act. */
|
|
50
|
+
| 'diagnostic'
|
|
51
|
+
/** Billing / receipt / ledger plumbing: receipt reads, completion sinks.
|
|
52
|
+
* Never customer-facing. */
|
|
53
|
+
| 'receipt';
|
|
54
|
+
|
|
55
|
+
/** Every provenance class, for exhaustiveness checks and tests. */
|
|
56
|
+
export const LOG_PROVENANCE_CLASSES: readonly LogProvenance[] = [
|
|
57
|
+
'user',
|
|
58
|
+
'lifecycle',
|
|
59
|
+
'replay',
|
|
60
|
+
'infra',
|
|
61
|
+
'diagnostic',
|
|
62
|
+
'receipt',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/** Rendering surfaces a log line can reach. */
|
|
66
|
+
export type LogSurface =
|
|
67
|
+
/** The CLI `deepline plays run --watch` progress stream. */
|
|
68
|
+
| 'watch'
|
|
69
|
+
/** The dashboard run-detail log tail. */
|
|
70
|
+
| 'ui'
|
|
71
|
+
/** Verbose / `--debug` / internal inspection only. */
|
|
72
|
+
| 'debug';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* THE policy. Maps each provenance class to the surfaces it reaches. Exhaustive
|
|
76
|
+
* over `LogProvenance` by construction (a `Record`), so adding a class forces a
|
|
77
|
+
* routing decision here at compile time.
|
|
78
|
+
*
|
|
79
|
+
* Design intent:
|
|
80
|
+
* - `watch` and `ui` carry what a person running/inspecting the play needs:
|
|
81
|
+
* their own output, real step transitions, and warnings that matter.
|
|
82
|
+
* - `replay`, `infra`, and `receipt` are internal churn — `debug` only. This
|
|
83
|
+
* is what silences the repeated `step docflow:*` replay passes and the
|
|
84
|
+
* scary-looking scaffolding on healthy runs.
|
|
85
|
+
* - `debug` is a superset: everything is inspectable in verbose mode.
|
|
86
|
+
*/
|
|
87
|
+
export const LOG_PROVENANCE_POLICY: Record<
|
|
88
|
+
LogProvenance,
|
|
89
|
+
Readonly<Record<LogSurface, boolean>>
|
|
90
|
+
> = {
|
|
91
|
+
user: { watch: true, ui: true, debug: true },
|
|
92
|
+
lifecycle: { watch: true, ui: true, debug: true },
|
|
93
|
+
replay: { watch: false, ui: false, debug: true },
|
|
94
|
+
infra: { watch: false, ui: false, debug: true },
|
|
95
|
+
diagnostic: { watch: true, ui: true, debug: true },
|
|
96
|
+
receipt: { watch: false, ui: false, debug: true },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** True when a line of the given provenance should render on the surface. */
|
|
100
|
+
export function logProvenanceReaches(
|
|
101
|
+
provenance: LogProvenance,
|
|
102
|
+
surface: LogSurface,
|
|
103
|
+
): boolean {
|
|
104
|
+
return LOG_PROVENANCE_POLICY[provenance][surface];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Structural carrier for NEW emissions. A tagged line looks like:
|
|
109
|
+
* `prov:replay<original line>`
|
|
110
|
+
* The sentinel is a control char that never appears in human log text, so old
|
|
111
|
+
* readers that don't know about it see a harmless invisible prefix rather than
|
|
112
|
+
* a mangled message, and `stripProvenanceTag` recovers the exact original.
|
|
113
|
+
*
|
|
114
|
+
* We deliberately do NOT put the tag inside the `[...]` bracket space that
|
|
115
|
+
* `formatPlayLogLine` / the dashboard timestamp parser rely on.
|
|
116
|
+
*/
|
|
117
|
+
const PROVENANCE_SENTINEL = '';
|
|
118
|
+
const PROVENANCE_PREFIX = `${PROVENANCE_SENTINEL}prov:`;
|
|
119
|
+
|
|
120
|
+
/** Stamp a provenance class onto a log line for the wire. */
|
|
121
|
+
export function tagLogProvenance(
|
|
122
|
+
provenance: LogProvenance,
|
|
123
|
+
line: string,
|
|
124
|
+
): string {
|
|
125
|
+
return `${PROVENANCE_PREFIX}${provenance}${PROVENANCE_SENTINEL}${line}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read a structural provenance tag off a line, if present, and return the tag
|
|
130
|
+
* plus the original untagged line. Returns `null` provenance when untagged.
|
|
131
|
+
*/
|
|
132
|
+
export function readProvenanceTag(line: string): {
|
|
133
|
+
provenance: LogProvenance | null;
|
|
134
|
+
line: string;
|
|
135
|
+
} {
|
|
136
|
+
if (!line.startsWith(PROVENANCE_PREFIX)) {
|
|
137
|
+
return { provenance: null, line };
|
|
138
|
+
}
|
|
139
|
+
const end = line.indexOf(PROVENANCE_SENTINEL, PROVENANCE_PREFIX.length);
|
|
140
|
+
if (end === -1) {
|
|
141
|
+
return { provenance: null, line };
|
|
142
|
+
}
|
|
143
|
+
const candidate = line.slice(PROVENANCE_PREFIX.length, end);
|
|
144
|
+
const provenance = LOG_PROVENANCE_CLASSES.includes(
|
|
145
|
+
candidate as LogProvenance,
|
|
146
|
+
)
|
|
147
|
+
? (candidate as LogProvenance)
|
|
148
|
+
: null;
|
|
149
|
+
return { provenance, line: line.slice(end + 1) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Remove any provenance tag, yielding the original human-readable line. */
|
|
153
|
+
export function stripProvenanceTag(line: string): string {
|
|
154
|
+
return readProvenanceTag(line).line;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Legacy classifier for old persisted log strings that carry NO structural
|
|
159
|
+
* tag. Mirrors — and supersedes — the two ad-hoc filters that used to live in
|
|
160
|
+
* `PlayRunDetailPanel.isInternalLogLine` and `formatPlayLogLine`. The patterns
|
|
161
|
+
* are the historical `[worker] ...` / `[event] ...` / `[perf] ...` shapes plus
|
|
162
|
+
* the checkpoint-replay marker. Order matters: most specific first.
|
|
163
|
+
*
|
|
164
|
+
* A legacy line the classifier can't place defaults to `user` — old runs
|
|
165
|
+
* predate structural tagging, and the safe default for an unrecognized line is
|
|
166
|
+
* to show it (never silently drop a line we can't prove is internal).
|
|
167
|
+
*/
|
|
168
|
+
export function classifyLegacyLogLine(line: string): LogProvenance {
|
|
169
|
+
const message = stripLeadingTimestamp(line);
|
|
170
|
+
|
|
171
|
+
// Replay / recovery echoes.
|
|
172
|
+
if (/recovered (?:from checkpoint|response from checkpoint)/i.test(message)) {
|
|
173
|
+
return 'replay';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Receipt / ledger plumbing.
|
|
177
|
+
if (/^\[perf\] runtime receipt\b/i.test(message)) {
|
|
178
|
+
return 'receipt';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Scheduler / sandbox / worker internals.
|
|
182
|
+
if (
|
|
183
|
+
/^\[perf\] runtime (?:map|state)\b/i.test(message) ||
|
|
184
|
+
/\[worker\] picked up run\b/.test(message) ||
|
|
185
|
+
/\[worker\] heartbeat\b/.test(message) ||
|
|
186
|
+
/\[worker\] progress completedRows=\d+ totalRows=\d+ rowUpdates=\d+/.test(
|
|
187
|
+
message,
|
|
188
|
+
) ||
|
|
189
|
+
/\[worker\] step started\b/.test(message) ||
|
|
190
|
+
/\[worker\] Preparing run files\b/.test(message) ||
|
|
191
|
+
/\[worker\] Run files ready\b/.test(message) ||
|
|
192
|
+
/\[worker\] Runtime ready\b/.test(message) ||
|
|
193
|
+
/\[worker\] Sandbox (?:starting|create start|create done|workspace ready|upload start|runner uploaded)\b/.test(
|
|
194
|
+
message,
|
|
195
|
+
) ||
|
|
196
|
+
/^\[event\] play\.step\.progress\b/.test(message) ||
|
|
197
|
+
/^\[event\] play\.run\.snapshot\b/.test(message) ||
|
|
198
|
+
/^\[event\] play\.sheet\.summary\b/.test(message)
|
|
199
|
+
) {
|
|
200
|
+
return 'infra';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Warnings / diagnostics.
|
|
204
|
+
if (
|
|
205
|
+
/^\[warn\]/i.test(message) ||
|
|
206
|
+
/^\[error\]/i.test(message) ||
|
|
207
|
+
/^\[runtime\.[a-z_]*(?:failure|error)\]/i.test(message)
|
|
208
|
+
) {
|
|
209
|
+
return 'diagnostic';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return 'user';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Classify any log line — structurally tagged (new) or legacy (old) — into its
|
|
217
|
+
* provenance class, returning the class and the original human-readable line.
|
|
218
|
+
* This is the entry point every surface should use.
|
|
219
|
+
*/
|
|
220
|
+
export function classifyLogLine(line: string): {
|
|
221
|
+
provenance: LogProvenance;
|
|
222
|
+
line: string;
|
|
223
|
+
} {
|
|
224
|
+
const tagged = readProvenanceTag(line);
|
|
225
|
+
if (tagged.provenance !== null) {
|
|
226
|
+
return { provenance: tagged.provenance, line: tagged.line };
|
|
227
|
+
}
|
|
228
|
+
return { provenance: classifyLegacyLogLine(tagged.line), line: tagged.line };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True when a raw (possibly tagged) log line should render on the surface. */
|
|
232
|
+
export function logLineReaches(line: string, surface: LogSurface): boolean {
|
|
233
|
+
return logProvenanceReaches(classifyLogLine(line).provenance, surface);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Strip ONLY a leading timestamp bracket, if present. `ctx.log` stamps
|
|
238
|
+
* `[<ISO>] ` and the dashboard timeline stringifier prepends `[<ISO>] `
|
|
239
|
+
* followed by a `[<source>]` bracket. We must keep the `[<source>]` bracket
|
|
240
|
+
* (`[worker]`, `[event]`) because the legacy classifier's patterns key on it —
|
|
241
|
+
* so we only remove a leading bracket whose contents parse as a date.
|
|
242
|
+
*/
|
|
243
|
+
function stripLeadingTimestamp(line: string): string {
|
|
244
|
+
const match = line.match(/^\[([^\]]+)\]\s*([\s\S]*)$/);
|
|
245
|
+
if (!match) {
|
|
246
|
+
return line;
|
|
247
|
+
}
|
|
248
|
+
const inner = match[1] ?? '';
|
|
249
|
+
const isTimestamp = !Number.isNaN(new Date(inner).getTime());
|
|
250
|
+
return isTimestamp ? (match[2] ?? line) : line;
|
|
251
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Play Node Scope — the explicit execution location stamped onto every provider
|
|
3
|
+
* tool call so usage/cost facts can be attributed back to a graph node.
|
|
4
|
+
*
|
|
5
|
+
* See ADR 0018 (Observed Provider Attribution). Before this module the only
|
|
6
|
+
* durable link between a provider call and a play graph node was a stdout regex
|
|
7
|
+
* (`Calling tool: <id>`), which collapsed N calls into one node transition and
|
|
8
|
+
* could not distinguish two nodes using the same tool. The scope below travels
|
|
9
|
+
* with the outbound `/api/v2/integrations/<tool>/execute` request and lands on
|
|
10
|
+
* `usageEvents.stepBlockId`, which already exists and is already written.
|
|
11
|
+
*
|
|
12
|
+
* The scope is deliberately made of facts the runtime *knows* at the call site:
|
|
13
|
+
* the tool id, the `ctx.tools.execute` id, and — for row-scoped calls — the
|
|
14
|
+
* runtime sheet column and artifact table namespace. It never guesses a graph
|
|
15
|
+
* node id; node resolution is a read-time join against the static pipeline and
|
|
16
|
+
* degrades to an explicit `unattributed` bucket instead of a wrong node.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Execution location of a single logical `ctx.tools.execute` invocation. */
|
|
20
|
+
export type PlayNodeScope = {
|
|
21
|
+
/** Provider tool id (`dropleads_search_people`). Always present. */
|
|
22
|
+
toolId: string;
|
|
23
|
+
/** The author-supplied `ctx.tools.execute(<id>, ...)` key, when known. */
|
|
24
|
+
callKey: string | null;
|
|
25
|
+
/** Runtime Sheet column the call fed, for row-scoped calls. */
|
|
26
|
+
column: string | null;
|
|
27
|
+
/** Artifact table namespace of the enclosing map, for row-scoped calls. */
|
|
28
|
+
tableNamespace: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Wire form of {@link PlayNodeScope} on the tool-execute request metadata. */
|
|
32
|
+
export type PlayNodeScopeWire = {
|
|
33
|
+
tool_id: string;
|
|
34
|
+
call_key?: string;
|
|
35
|
+
column?: string;
|
|
36
|
+
table_namespace?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PLAY_NODE_SCOPE_ENCODING_VERSION = 'pn1';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Node id used when a usage fact carries no resolvable scope. Old events
|
|
43
|
+
* written before this contract, and calls the read path cannot place on the
|
|
44
|
+
* graph, aggregate here rather than being dropped or guessed onto a node.
|
|
45
|
+
*/
|
|
46
|
+
export const UNATTRIBUTED_PLAY_NODE_ID = 'unattributed';
|
|
47
|
+
|
|
48
|
+
/** Per-component cap. Keeps the encoded key comfortably under Convex limits. */
|
|
49
|
+
const MAX_COMPONENT_LENGTH = 128;
|
|
50
|
+
|
|
51
|
+
function normalizeComponent(value: unknown): string | null {
|
|
52
|
+
if (typeof value !== 'string') return null;
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (!trimmed) return null;
|
|
55
|
+
return trimmed.length > MAX_COMPONENT_LENGTH
|
|
56
|
+
? trimmed.slice(0, MAX_COMPONENT_LENGTH)
|
|
57
|
+
: trimmed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a scope from raw call-site facts. Returns null when there is no tool
|
|
62
|
+
* id, because a scope without a tool identity cannot be joined to anything.
|
|
63
|
+
*/
|
|
64
|
+
export function buildPlayNodeScope(input: {
|
|
65
|
+
toolId: unknown;
|
|
66
|
+
callKey?: unknown;
|
|
67
|
+
column?: unknown;
|
|
68
|
+
tableNamespace?: unknown;
|
|
69
|
+
}): PlayNodeScope | null {
|
|
70
|
+
const toolId = normalizeComponent(input.toolId);
|
|
71
|
+
if (!toolId) return null;
|
|
72
|
+
return {
|
|
73
|
+
toolId,
|
|
74
|
+
callKey: normalizeComponent(input.callKey),
|
|
75
|
+
column: normalizeComponent(input.column),
|
|
76
|
+
tableNamespace: normalizeComponent(input.tableNamespace),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function playNodeScopeToWire(scope: PlayNodeScope): PlayNodeScopeWire {
|
|
81
|
+
return {
|
|
82
|
+
tool_id: scope.toolId,
|
|
83
|
+
...(scope.callKey ? { call_key: scope.callKey } : {}),
|
|
84
|
+
...(scope.column ? { column: scope.column } : {}),
|
|
85
|
+
...(scope.tableNamespace ? { table_namespace: scope.tableNamespace } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function playNodeScopeFromWire(value: unknown): PlayNodeScope | null {
|
|
90
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
91
|
+
const wire = value as Record<string, unknown>;
|
|
92
|
+
return buildPlayNodeScope({
|
|
93
|
+
toolId: wire.tool_id ?? wire.toolId,
|
|
94
|
+
callKey: wire.call_key ?? wire.callKey,
|
|
95
|
+
column: wire.column,
|
|
96
|
+
tableNamespace: wire.table_namespace ?? wire.tableNamespace,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function encodeComponent(value: string): string {
|
|
101
|
+
return encodeURIComponent(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Durable key form written to `usageEvents.stepBlockId`.
|
|
106
|
+
*
|
|
107
|
+
* `pn1|tool=<t>|call=<c>|col=<f>|ns=<n>` with percent-encoded components and
|
|
108
|
+
* empty components omitted. Stable and parseable in both directions so a read
|
|
109
|
+
* path never has to pattern-match free text.
|
|
110
|
+
*/
|
|
111
|
+
export function encodePlayNodeScope(scope: PlayNodeScope): string {
|
|
112
|
+
const parts = [
|
|
113
|
+
`${PLAY_NODE_SCOPE_ENCODING_VERSION}`,
|
|
114
|
+
`tool=${encodeComponent(scope.toolId)}`,
|
|
115
|
+
];
|
|
116
|
+
if (scope.callKey) parts.push(`call=${encodeComponent(scope.callKey)}`);
|
|
117
|
+
if (scope.column) parts.push(`col=${encodeComponent(scope.column)}`);
|
|
118
|
+
if (scope.tableNamespace) {
|
|
119
|
+
parts.push(`ns=${encodeComponent(scope.tableNamespace)}`);
|
|
120
|
+
}
|
|
121
|
+
return parts.join('|');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse a `stepBlockId` back into a scope. Returns null for legacy workflow
|
|
126
|
+
* block ids and for anything that is not this encoding — callers must treat
|
|
127
|
+
* null as "unattributed", never as an error.
|
|
128
|
+
*/
|
|
129
|
+
export function decodePlayNodeScope(value: unknown): PlayNodeScope | null {
|
|
130
|
+
if (typeof value !== 'string') return null;
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (!trimmed.startsWith(`${PLAY_NODE_SCOPE_ENCODING_VERSION}|`)) return null;
|
|
133
|
+
const fields: Record<string, string> = {};
|
|
134
|
+
for (const part of trimmed.split('|').slice(1)) {
|
|
135
|
+
const separator = part.indexOf('=');
|
|
136
|
+
if (separator <= 0) continue;
|
|
137
|
+
const key = part.slice(0, separator);
|
|
138
|
+
try {
|
|
139
|
+
fields[key] = decodeURIComponent(part.slice(separator + 1));
|
|
140
|
+
} catch {
|
|
141
|
+
// A malformed component must not poison the whole rollup.
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return buildPlayNodeScope({
|
|
146
|
+
toolId: fields.tool,
|
|
147
|
+
callKey: fields.call,
|
|
148
|
+
column: fields.col,
|
|
149
|
+
tableNamespace: fields.ns,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Short human label stored beside the key in `usageEvents.stepAlias`. */
|
|
154
|
+
export function playNodeScopeAlias(scope: PlayNodeScope): string {
|
|
155
|
+
if (scope.column && scope.tableNamespace) {
|
|
156
|
+
return `${scope.tableNamespace}.${scope.column}`;
|
|
157
|
+
}
|
|
158
|
+
if (scope.column) return scope.column;
|
|
159
|
+
return scope.callKey ?? scope.toolId;
|
|
160
|
+
}
|
|
@@ -123,6 +123,12 @@ export interface PlayRunnerContextConfig {
|
|
|
123
123
|
runtimeTestFaultHeader?: string | null;
|
|
124
124
|
vercelProtectionBypassToken?: string | null;
|
|
125
125
|
integrationMode?: 'live' | 'eval_stub' | 'fixture';
|
|
126
|
+
/**
|
|
127
|
+
* Docflow rollout answer for this run, stamped at admission onto the signed
|
|
128
|
+
* runtime authority and relayed here. Absent means OFF, so a runner reading a
|
|
129
|
+
* launch queued before the field existed captures nothing extra.
|
|
130
|
+
*/
|
|
131
|
+
docflowEnabled?: boolean;
|
|
126
132
|
/** Validated internal fixture-only provider response simulation. */
|
|
127
133
|
fixtureBehavior?: FixtureBehavior | null;
|
|
128
134
|
/** Preview/dev test seam that applies provider pacing to fixture responses. */
|
|
@@ -13,6 +13,7 @@ const RUNTIME_RUNNER_LOST_RE = /\bRUNTIME_RUNNER_LOST\b/i;
|
|
|
13
13
|
const RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_RE =
|
|
14
14
|
/\bRUNTIME_SANDBOX_INSPECTION_UNAVAILABLE\b/i;
|
|
15
15
|
const RUNTIME_SANDBOX_LOST_RE = /\bRUNTIME_SANDBOX_LOST\b/i;
|
|
16
|
+
const RUNTIME_SANDBOX_START_FAILED_RE = /\bRUNTIME_SANDBOX_START_FAILED\b/i;
|
|
16
17
|
const RUNTIME_SANDBOX_OOM_RE =
|
|
17
18
|
/\bRUNTIME_SANDBOX_OOM\b|(?:javascript heap out of memory|fatal error:.*(?:heap|allocation).*memory)/i;
|
|
18
19
|
const RUNTIME_SANDBOX_KILLED_RE = /\bRUNTIME_SANDBOX_KILLED\b/i;
|
|
@@ -39,6 +40,11 @@ export const RUNTIME_RUNNER_LOST_MESSAGE =
|
|
|
39
40
|
'The execution runner stopped reporting liveness before returning a terminal result. Completed work is preserved, but Deepline did not automatically replay the run because provider side effects may already exist. Re-run only when it is safe to do so.';
|
|
40
41
|
export const RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_MESSAGE =
|
|
41
42
|
'The play stopped after Deepline could not verify the execution sandbox state. Completed row state was preserved. Re-run from the persisted rows; if this keeps happening, contact Deepline support with the run ID.';
|
|
43
|
+
// The sandbox was accepted but the runner never reported liveness, so the play
|
|
44
|
+
// body never began. Safe to retry without the side-effect caveat RUNNER_LOST
|
|
45
|
+
// carries: nothing ran, so no provider call can already exist.
|
|
46
|
+
export const RUNTIME_SANDBOX_START_FAILED_MESSAGE =
|
|
47
|
+
'The execution sandbox never finished starting, so this play never began running. Re-run the same command; if this keeps happening, contact Deepline support with the run ID.';
|
|
42
48
|
|
|
43
49
|
export const WORKSPACE_STORAGE_NOT_READY_CODE = 'WORKSPACE_STORAGE_NOT_READY';
|
|
44
50
|
|
|
@@ -332,6 +338,27 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
|
|
|
332
338
|
...(causes.length > 0 ? { causes } : {}),
|
|
333
339
|
};
|
|
334
340
|
}
|
|
341
|
+
// Before the generic fallthrough could claim it. Without this branch the
|
|
342
|
+
// whole minted string — sandbox and command UUIDs, `runner_heartbeat_not_observed`,
|
|
343
|
+
// the embedded `schedulerReadinessResponses` JSON, `exit_code_file_unavailable`
|
|
344
|
+
// — became the run's public `message` verbatim, and every surface that prints
|
|
345
|
+
// a failure printed the scheduler talking to itself. The detail is not
|
|
346
|
+
// swallowed: it stays on `cause`, which is what the run's logs and the CLI's
|
|
347
|
+
// `errors[]` carry.
|
|
348
|
+
if (RUNTIME_SANDBOX_START_FAILED_RE.test(rawCause)) {
|
|
349
|
+
const stack = error instanceof Error ? boundedFailureStack(error) : null;
|
|
350
|
+
const causes = error instanceof Error ? failureCauseTexts(error) : [];
|
|
351
|
+
return {
|
|
352
|
+
code: 'RUNTIME_SANDBOX_START_FAILED',
|
|
353
|
+
phase: 'infrastructure',
|
|
354
|
+
message: RUNTIME_SANDBOX_START_FAILED_MESSAGE,
|
|
355
|
+
retryable: true,
|
|
356
|
+
cause,
|
|
357
|
+
...(error instanceof Error ? { name: error.name || 'Error' } : {}),
|
|
358
|
+
...(stack ? { stack } : {}),
|
|
359
|
+
...(causes.length > 0 ? { causes } : {}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
335
362
|
if (RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_RE.test(rawCause)) {
|
|
336
363
|
return {
|
|
337
364
|
code: 'RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE',
|
|
@@ -7,10 +7,19 @@ import {
|
|
|
7
7
|
normalizePlayRunLedgerTerminalSource,
|
|
8
8
|
} from './run-terminal-source';
|
|
9
9
|
import { MAX_LEDGER_LOG_LINES_PER_EVENT } from './ledger-safe-payload';
|
|
10
|
+
import {
|
|
11
|
+
normalizePlayDocflowNodeIoState,
|
|
12
|
+
type PlayDocflowNodeIoState,
|
|
13
|
+
} from './docflow-node-io';
|
|
10
14
|
import {
|
|
11
15
|
isPlayActivityObservation,
|
|
12
16
|
type PlayActivityObservation,
|
|
13
17
|
} from './activity-observation';
|
|
18
|
+
import {
|
|
19
|
+
mergeDatasetBornFrom,
|
|
20
|
+
normalizeDatasetBornFrom,
|
|
21
|
+
type PlayDatasetBornFrom,
|
|
22
|
+
} from './cell-provenance';
|
|
14
23
|
|
|
15
24
|
export type PlayRunLedgerStatus =
|
|
16
25
|
| 'queued'
|
|
@@ -73,6 +82,7 @@ export type PlayRunLedgerStepProgress = {
|
|
|
73
82
|
startedAt?: number | null;
|
|
74
83
|
completedAt?: number | null;
|
|
75
84
|
updatedAt?: number | null;
|
|
85
|
+
nodeIo?: PlayDocflowNodeIoState;
|
|
76
86
|
};
|
|
77
87
|
|
|
78
88
|
export type PlayRunLedgerStepSnapshot = {
|
|
@@ -97,6 +107,12 @@ export type PlayRunLedgerDatasetSnapshot = {
|
|
|
97
107
|
succeededRows: number;
|
|
98
108
|
failedRows: number;
|
|
99
109
|
complete: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Dataset-grain row birth (ADR 0019). Absent on datasets registered before
|
|
112
|
+
* the contract and on any registration that did not state one — never
|
|
113
|
+
* back-filled from row counts, which would assert a lineage nobody observed.
|
|
114
|
+
*/
|
|
115
|
+
bornFrom?: PlayDatasetBornFrom | null;
|
|
100
116
|
updatedAt: number;
|
|
101
117
|
};
|
|
102
118
|
|
|
@@ -265,6 +281,8 @@ export type PlayRunLedgerEvent =
|
|
|
265
281
|
succeededRows?: number;
|
|
266
282
|
failedRows?: number;
|
|
267
283
|
complete?: boolean;
|
|
284
|
+
/** Dataset-grain row birth, stated once per dataset (ADR 0019). */
|
|
285
|
+
bornFrom?: PlayDatasetBornFrom;
|
|
268
286
|
})
|
|
269
287
|
| (PlayRunLedgerBaseEvent & {
|
|
270
288
|
type: 'activity.observed';
|
|
@@ -515,6 +533,7 @@ export function normalizePlayRunLedgerSnapshot(
|
|
|
515
533
|
rawDataset.phase === 'available' || rawDataset.phase === 'failed'
|
|
516
534
|
? rawDataset.phase
|
|
517
535
|
: 'registered';
|
|
536
|
+
const datasetBornFrom = normalizeDatasetBornFrom(rawDataset.bornFrom);
|
|
518
537
|
datasetsById[datasetId] = {
|
|
519
538
|
datasetId,
|
|
520
539
|
path: optionalString(rawDataset.path) ?? `datasets.${datasetId}`,
|
|
@@ -524,6 +543,10 @@ export function normalizePlayRunLedgerSnapshot(
|
|
|
524
543
|
succeededRows: Math.max(0, finiteNumber(rawDataset.succeededRows) ?? 0),
|
|
525
544
|
failedRows: Math.max(0, finiteNumber(rawDataset.failedRows) ?? 0),
|
|
526
545
|
complete: rawDataset.complete === true,
|
|
546
|
+
// ADR 0019 birth survives the snapshot round trip. A partial record is
|
|
547
|
+
// dropped rather than half-admitted, so a dataset either states where its
|
|
548
|
+
// rows came from or says nothing.
|
|
549
|
+
...(datasetBornFrom ? { bornFrom: datasetBornFrom } : {}),
|
|
527
550
|
updatedAt: finiteNumber(rawDataset.updatedAt) ?? 0,
|
|
528
551
|
};
|
|
529
552
|
}
|
|
@@ -766,6 +789,9 @@ function normalizeStepProgress(
|
|
|
766
789
|
...(finiteNumber(value.updatedAt) !== null
|
|
767
790
|
? { updatedAt: finiteNumber(value.updatedAt) }
|
|
768
791
|
: {}),
|
|
792
|
+
...(normalizePlayDocflowNodeIoState(value.nodeIo)
|
|
793
|
+
? { nodeIo: normalizePlayDocflowNodeIoState(value.nodeIo) }
|
|
794
|
+
: {}),
|
|
769
795
|
};
|
|
770
796
|
}
|
|
771
797
|
|
|
@@ -1443,6 +1469,10 @@ export function reducePlayRunLedgerEvent(
|
|
|
1443
1469
|
}
|
|
1444
1470
|
case 'dataset.lifecycle': {
|
|
1445
1471
|
const current = base.datasetsById[event.datasetId];
|
|
1472
|
+
const mergedBornFrom = mergeDatasetBornFrom(
|
|
1473
|
+
current?.bornFrom,
|
|
1474
|
+
event.bornFrom,
|
|
1475
|
+
);
|
|
1446
1476
|
const next: PlayRunLedgerDatasetSnapshot = {
|
|
1447
1477
|
datasetId: event.datasetId,
|
|
1448
1478
|
path: event.path,
|
|
@@ -1458,6 +1488,10 @@ export function reducePlayRunLedgerEvent(
|
|
|
1458
1488
|
),
|
|
1459
1489
|
failedRows: Math.max(current?.failedRows ?? 0, event.failedRows ?? 0),
|
|
1460
1490
|
complete: current?.complete === true || event.complete === true,
|
|
1491
|
+
// Birth is stated at registration. A later phase event carries no birth
|
|
1492
|
+
// record and must not erase the one that was witnessed; a paged
|
|
1493
|
+
// dataset re-registers with a larger admitted-row count.
|
|
1494
|
+
...(mergedBornFrom ? { bornFrom: mergedBornFrom } : {}),
|
|
1461
1495
|
updatedAt: occurredAt,
|
|
1462
1496
|
};
|
|
1463
1497
|
return withTiming({
|
|
@@ -1516,6 +1550,7 @@ function progressSignature(
|
|
|
1516
1550
|
artifactTableNamespace: progress?.artifactTableNamespace ?? null,
|
|
1517
1551
|
startedAt: progress?.startedAt ?? null,
|
|
1518
1552
|
completedAt: progress?.completedAt ?? null,
|
|
1553
|
+
nodeIo: progress?.nodeIo ?? null,
|
|
1519
1554
|
});
|
|
1520
1555
|
}
|
|
1521
1556
|
|
|
@@ -1777,6 +1812,7 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
|
|
|
1777
1812
|
...(progress.completedAt !== undefined
|
|
1778
1813
|
? { completedAt: progress.completedAt }
|
|
1779
1814
|
: {}),
|
|
1815
|
+
...(progress.nodeIo !== undefined ? { nodeIo: progress.nodeIo } : {}),
|
|
1780
1816
|
updatedAt: progress.updatedAt ?? checkpointAt,
|
|
1781
1817
|
};
|
|
1782
1818
|
if (
|
|
@@ -1790,11 +1826,13 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
|
|
|
1790
1826
|
occurredAt: progress.updatedAt ?? checkpointAt,
|
|
1791
1827
|
stepId,
|
|
1792
1828
|
status:
|
|
1793
|
-
typeof progress.
|
|
1794
|
-
? '
|
|
1795
|
-
:
|
|
1796
|
-
? '
|
|
1797
|
-
: '
|
|
1829
|
+
typeof progress.nodeIo?.error === 'string'
|
|
1830
|
+
? 'failed'
|
|
1831
|
+
: typeof progress.completedAt === 'number'
|
|
1832
|
+
? 'completed'
|
|
1833
|
+
: previousStep?.status === 'failed'
|
|
1834
|
+
? 'failed'
|
|
1835
|
+
: 'running',
|
|
1798
1836
|
progress: normalizedProgress,
|
|
1799
1837
|
});
|
|
1800
1838
|
}
|