@inflexa-ai/harness 0.44.0 → 0.45.1
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/agents/conversation-agent.d.ts +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +51 -0
- package/dist/app/approve-plan.js +84 -0
- package/dist/app/chat-turn.js +6 -3
- package/dist/app/data-profile-view.d.ts +1 -1
- package/dist/app/run-activity.js +4 -6
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +14 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.d.ts +7 -0
- package/dist/contracts/chat-frame.js +24 -6
- package/dist/contracts/chat-parts.d.ts +73 -2
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/part-registry.js +3 -0
- package/dist/contracts/schemas/chat-events.d.ts +38 -0
- package/dist/contracts/schemas/chat-events.js +8 -0
- package/dist/contracts/schemas/chat-parts.d.ts +239 -15
- package/dist/contracts/schemas/chat-parts.js +80 -12
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/contracts/subagent-activity.d.ts +12 -0
- package/dist/contracts/subagent-activity.js +76 -0
- package/dist/execution/artifact-metadata.js +1 -3
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +130 -28
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/execution/run-event-parts.d.ts +6 -5
- package/dist/execution/run-event-parts.js +6 -5
- package/dist/execution/run-event-stream.d.ts +18 -6
- package/dist/execution/run-event-stream.js +67 -17
- package/dist/execution/run-synthesis.d.ts +21 -0
- package/dist/execution/run-synthesis.js +77 -3
- package/dist/index.d.ts +28 -10
- package/dist/index.js +26 -2
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/metrics.d.ts +2 -2
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +16 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +8 -0
- package/dist/memory/card-builders.d.ts +11 -0
- package/dist/memory/card-builders.js +35 -21
- package/dist/memory/conversation-display-recorder.d.ts +2 -2
- package/dist/memory/conversation-display-recorder.js +38 -4
- package/dist/memory/conversation-display-storage.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +18 -3
- package/dist/memory/thread-history.d.ts +42 -8
- package/dist/memory/thread-history.js +164 -45
- package/dist/memory/thread-store.d.ts +13 -0
- package/dist/memory/thread-store.js +15 -1
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +3 -1
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +39 -20
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +93 -11
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/report-model/pin-snapshot.d.ts +1 -1
- package/dist/report-model/pin-snapshot.js +2 -50
- package/dist/runtime/assemble.d.ts +8 -1
- package/dist/runtime/assemble.js +11 -0
- package/dist/runtime/boot.d.ts +3 -0
- package/dist/runtime/boot.js +10 -1
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
- package/dist/sandbox/sandbox-step-translate.js +24 -0
- package/dist/schemas/artifact-manifest.d.ts +1 -1
- package/dist/schemas/artifact-manifest.js +11 -9
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +75 -21
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/analyses-metrics.d.ts +79 -7
- package/dist/state/analyses-metrics.js +141 -6
- package/dist/state/artifacts.d.ts +66 -3
- package/dist/state/artifacts.js +85 -7
- package/dist/state/data-profile.d.ts +4 -2
- package/dist/state/data-profile.js +7 -5
- package/dist/state/index.d.ts +9 -8
- package/dist/state/index.js +5 -5
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
- package/dist/state/migrations/index.js +8 -0
- package/dist/state/plans.d.ts +21 -0
- package/dist/state/plans.js +25 -0
- package/dist/state/runs.d.ts +79 -14
- package/dist/state/runs.js +150 -33
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tasks/data-profile-activity.d.ts +7 -1
- package/dist/tasks/data-profile-activity.js +18 -9
- package/dist/tasks/data-profile.d.ts +15 -1
- package/dist/tasks/data-profile.js +16 -11
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/approval/gateway.js +28 -5
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +80 -1
- package/dist/tools/execute-analysis.js +352 -110
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/inspect-data-profile.d.ts +1 -1
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/inspect-run.js +7 -7
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +62 -21
- package/dist/workflows/execute-analysis.js +307 -79
- package/dist/workflows/plan/graph.d.ts +16 -0
- package/dist/workflows/plan/graph.js +36 -0
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/dist/workflows/sandbox-step.js +14 -12
- package/package.json +5 -1
|
@@ -50,20 +50,25 @@
|
|
|
50
50
|
* the run before any step starts
|
|
51
51
|
* - a `suspend` error of a synthesis model request suspends the run in
|
|
52
52
|
* place of the synthesis failure
|
|
53
|
-
* - the analysis
|
|
54
|
-
* `collectAndComplete`. The
|
|
53
|
+
* - the run row and the analysis flip to `suspended_insufficient_funds` in
|
|
54
|
+
* `collectAndComplete`. The run status is terminal and does not block a
|
|
55
|
+
* new run of the plan. The paused parent lands in `CANCELLED`, a
|
|
55
56
|
* durably reschedulable state; re-driving it is a future enhancement,
|
|
56
57
|
* not yet wired here. The reason of the host rides to the run row, the
|
|
57
58
|
* charge close, the metric, and the terminal part, unread.
|
|
58
59
|
* 5. `synthesizeFindings` (single sequential block, no sandbox)
|
|
59
60
|
* 6. `collectAndComplete` (terminal — runs on ALL paths)
|
|
60
61
|
* - determine final status from completed/cancelled/failed counts
|
|
61
|
-
* - update `cortex_runs.status` + `error
|
|
62
|
-
* the
|
|
63
|
-
*
|
|
64
|
-
* died before
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* - update `cortex_runs.status` + `error`. When the write changes the row,
|
|
63
|
+
* the same step emits `run_completed` before the commit, records the run
|
|
64
|
+
* outcome metric plus the outcome of every settled step whose child never
|
|
65
|
+
* wrote its own terminal row (a cancelled child, or one that died before
|
|
66
|
+
* `failStep`), closes the running charge with the matching outcome, and
|
|
67
|
+
* revokes the run authorization through the injected `runAuthorizer`
|
|
68
|
+
* - the write retries with backoff on a failed transaction. When the write is
|
|
69
|
+
* refused, or fails on each attempt, close and revoke in their own steps, then
|
|
70
|
+
* emit `run_completed`. After a failed write the row stays `running`, so the
|
|
71
|
+
* body records its run end as a workflow event for the run canceler
|
|
67
72
|
* - emit terminal stream part
|
|
68
73
|
* - WRITES NOTHING to the conversation thread (results are pull-only
|
|
69
74
|
* via `inspectRun`)
|
|
@@ -73,18 +78,20 @@ import { err, ok } from "neverthrow";
|
|
|
73
78
|
import { mkdir } from "node:fs/promises";
|
|
74
79
|
import { join } from "node:path";
|
|
75
80
|
import { forStep } from "../auth/types.js";
|
|
81
|
+
import { RUN_END_EVENT_KEY } from "../execution/run-end-record.js";
|
|
76
82
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
77
83
|
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
78
84
|
import { ATTR_INFLEXA_STEP_ID, stableSpan } from "../lib/otel-spans.js";
|
|
79
85
|
import { recordRunCompleted, recordStepCompleted, stepOutcomeOf } from "../lib/metrics.js";
|
|
80
|
-
import { describeDbError } from "../lib/db-result.js";
|
|
86
|
+
import { describeDbError, withTransaction } from "../lib/db-result.js";
|
|
81
87
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
82
|
-
import {
|
|
88
|
+
import { countArtifactsByRuns, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, queryStepsByRun, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
|
|
83
89
|
import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
|
|
90
|
+
import { flattenDataPart } from "../contracts/chat-frame.js";
|
|
84
91
|
import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
|
|
85
|
-
import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing, emptyStepMemory } from "../prompts/briefing.js";
|
|
92
|
+
import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing, emptyStepMemory, } from "../prompts/briefing.js";
|
|
86
93
|
import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
|
|
87
|
-
import { isChatDataPart } from "../sandbox/sandbox-step-translate.js";
|
|
94
|
+
import { isChatDataPart, synthesisEmission } from "../sandbox/sandbox-step-translate.js";
|
|
88
95
|
import { synthesizeRun } from "../app/synthesize-run.js";
|
|
89
96
|
import { computeTopologicalLevels, scheduleReady, validatePlanDag } from "./execute-analysis-scheduler.js";
|
|
90
97
|
import { recordCancelledChild } from "./metrics.js";
|
|
@@ -181,7 +188,7 @@ function observeRunGuarded(deps, observation) {
|
|
|
181
188
|
* projection/timeout-propagation tests.
|
|
182
189
|
*/
|
|
183
190
|
export function buildChildInput(args) {
|
|
184
|
-
const { input, stepId, level, runId, workflowId, prompt } = args;
|
|
191
|
+
const { input, stepId, level, runId, workflowId, prompt, templateBinding } = args;
|
|
185
192
|
const resources = input.resourcesByStepId[stepId];
|
|
186
193
|
if (!resources) {
|
|
187
194
|
throw new Error(`executeAnalysis: step "${stepId}" missing from resourcesByStepId — every step must declare resources`);
|
|
@@ -203,6 +210,9 @@ export function buildChildInput(args) {
|
|
|
203
210
|
// genuinely declared nothing, and it silently deletes every same-run edge
|
|
204
211
|
// the step was entitled to.
|
|
205
212
|
dependsOn: planStep.depends_on,
|
|
213
|
+
// Composed beside the seed, thus the same checkpoint carries both and a
|
|
214
|
+
// replay hands the child the binding its briefing describes.
|
|
215
|
+
...(templateBinding ? { templateBinding } : {}),
|
|
206
216
|
level,
|
|
207
217
|
prompt,
|
|
208
218
|
parentWorkflowId: workflowId,
|
|
@@ -220,13 +230,18 @@ export function buildChildInput(args) {
|
|
|
220
230
|
* rendered any earlier (at plan submission, say) cannot mention them, and the
|
|
221
231
|
* agent is left to rediscover its own upstream.
|
|
222
232
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
233
|
+
* The template contract of a grounded step is read with the package, and the
|
|
234
|
+
* plan settings are bound to it: the seed describes the binding, and the child
|
|
235
|
+
* input carries it, from one checkpoint. The child fetches nothing.
|
|
236
|
+
*
|
|
237
|
+
* REPLAY: the caller wraps this in a `DBOS.runStep`, so the composed seed is
|
|
238
|
+
* checkpointed and a replay returns it verbatim — the DB, the disk, and the
|
|
239
|
+
* knowledge service are not read again and cannot drift the seed under a
|
|
240
|
+
* recovered run. Never call it unwrapped from the workflow body.
|
|
227
241
|
*/
|
|
228
242
|
export async function composeStepSeed(args) {
|
|
229
|
-
|
|
243
|
+
const { briefing, templateBinding } = await gatherStepPackage(args);
|
|
244
|
+
return { prompt: composeStepBriefing(briefing), ...(templateBinding ? { templateBinding } : {}) };
|
|
230
245
|
}
|
|
231
246
|
/**
|
|
232
247
|
* Do every read of one step's package and give the data its seed is rendered
|
|
@@ -240,6 +255,9 @@ export async function composeStepSeed(args) {
|
|
|
240
255
|
* so an edit during a run reaches each later step, and two steps of one run
|
|
241
256
|
* can carry different copies.
|
|
242
257
|
*
|
|
258
|
+
* The contract of the template a grounded step renders is read here too, from
|
|
259
|
+
* the knowledge client, and the plan settings are bound to it.
|
|
260
|
+
*
|
|
243
261
|
* A read failure throws (house rules): a swallowed error would be frozen into
|
|
244
262
|
* the checkpointed seed and served to every replay of this step.
|
|
245
263
|
*/
|
|
@@ -280,12 +298,151 @@ export async function gatherStepPackage(args) {
|
|
|
280
298
|
agentByStepId: input.agentByStepId,
|
|
281
299
|
pool: deps.pool,
|
|
282
300
|
});
|
|
301
|
+
const contract = await resolveTemplateContract(step, deps.knowledge);
|
|
302
|
+
return {
|
|
303
|
+
briefing: {
|
|
304
|
+
step,
|
|
305
|
+
workspace,
|
|
306
|
+
profile: profile?.result ?? null,
|
|
307
|
+
upstream,
|
|
308
|
+
memory: { goal: memory.goal, constraints: memory.constraints },
|
|
309
|
+
...contract.briefing,
|
|
310
|
+
},
|
|
311
|
+
...(contract.binding ? { templateBinding: contract.binding } : {}),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** A served contract carries no `match`; the two refusals do. */
|
|
315
|
+
function isTemplateContract(answer) {
|
|
316
|
+
return !("match" in answer);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Fetch the contract of the template a step grounds on, and bind the plan
|
|
320
|
+
* settings to it. A step without a template has no template part. A step
|
|
321
|
+
* with one and no way to read it (no client, no answer, or an unknown
|
|
322
|
+
* template) gets the reason, thus the seed reports the absence rather than
|
|
323
|
+
* hiding it, and no binding.
|
|
324
|
+
*/
|
|
325
|
+
async function resolveTemplateContract(step, client) {
|
|
326
|
+
const ref = step.grounding?.template;
|
|
327
|
+
if (!ref)
|
|
328
|
+
return { briefing: {} };
|
|
329
|
+
if (!client)
|
|
330
|
+
return { briefing: { templateNotRetrieved: "no knowledge client is bound" } };
|
|
331
|
+
const answer = await client.contract(ref);
|
|
332
|
+
if (!isTemplateContract(answer)) {
|
|
333
|
+
const reason = answer.match === "unavailable"
|
|
334
|
+
? `the knowledge service did not answer: ${answer.reason}`
|
|
335
|
+
: `the knowledge service refused the lookup: ${answer.message}`;
|
|
336
|
+
return { briefing: { templateNotRetrieved: reason } };
|
|
337
|
+
}
|
|
338
|
+
return bindTemplateContract(ref, answer, step.grounding?.settings ?? [], {
|
|
339
|
+
stepId: step.id,
|
|
340
|
+
claims: step.grounding?.claims ?? [],
|
|
341
|
+
...(step.grounding?.snapshot && step.grounding.snapshot !== "none" ? { snapshot: step.grounding.snapshot } : {}),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/** The version of a template reference (`tpl-x@1.0.0` gives `1.0.0`), or `undefined` when the reference has none. */
|
|
345
|
+
function versionOfRef(ref) {
|
|
346
|
+
const at = ref.indexOf("@");
|
|
347
|
+
return at < 0 ? undefined : ref.slice(at + 1);
|
|
348
|
+
}
|
|
349
|
+
/** Two setting values agree when their JSON agrees: the values are scalars or lists of strings. */
|
|
350
|
+
function sameSettingValue(a, b) {
|
|
351
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Whether a plan value is a value of the slot, and not a policy word. A
|
|
355
|
+
* procedure parameter and a template slot share a name, but not always a
|
|
356
|
+
* value domain: `min_samples = smallest_group_size` names a rule the agent
|
|
357
|
+
* applies, and the slot takes the resulting integer. Only a member of the
|
|
358
|
+
* enum of the slot, a number for a number slot, or a boolean for a boolean
|
|
359
|
+
* slot binds. A free-text slot never binds, because the host cannot tell a
|
|
360
|
+
* policy word from a value there.
|
|
361
|
+
*/
|
|
362
|
+
function fitsSlot(slot, value) {
|
|
363
|
+
if (slot.enum)
|
|
364
|
+
return typeof value === "string" && slot.enum.includes(value);
|
|
365
|
+
if (slot.type === "integer" || slot.type === "number")
|
|
366
|
+
return typeof value === "number";
|
|
367
|
+
if (slot.type === "boolean")
|
|
368
|
+
return typeof value === "boolean";
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
function bindTemplateContract(ref, contract, settings, grounding) {
|
|
372
|
+
const refVersion = versionOfRef(ref);
|
|
373
|
+
const versionMatches = refVersion === undefined || refVersion === contract.version;
|
|
374
|
+
const adaptable = contract.parameters.filter((slot) => slot.adaptable);
|
|
375
|
+
const adaptableNames = new Set(adaptable.map((slot) => slot.name));
|
|
376
|
+
const pinnedByName = new Map(contract.parameters.filter((slot) => !slot.adaptable).map((slot) => [slot.name, slot]));
|
|
377
|
+
const bound = [];
|
|
378
|
+
const unbound = [];
|
|
379
|
+
const slots = {};
|
|
380
|
+
const sources = {};
|
|
381
|
+
for (const setting of settings) {
|
|
382
|
+
const shown = `\`${setting.name}\` = ${JSON.stringify(setting.value)} (${setting.step})`;
|
|
383
|
+
if (!versionMatches) {
|
|
384
|
+
unbound.push(`${shown}: not bound, because the served version differs from the plan`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (adaptableNames.has(setting.name)) {
|
|
388
|
+
const slot = adaptable.find((candidate) => candidate.name === setting.name);
|
|
389
|
+
if (slot && !fitsSlot(slot, setting.value)) {
|
|
390
|
+
unbound.push(`${shown}: a policy of the plan, not a value of the \`${setting.name}\` slot (${slot.type}); state the value it gives`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const prior = slots[setting.name];
|
|
394
|
+
if (prior !== undefined) {
|
|
395
|
+
if (!sameSettingValue(prior, setting.value))
|
|
396
|
+
unbound.push(`${shown}: differs from the bound value ${JSON.stringify(prior)} of the same slot`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
slots[setting.name] = setting.value;
|
|
400
|
+
if (setting.source)
|
|
401
|
+
sources[setting.name] = setting.source;
|
|
402
|
+
bound.push({ name: setting.name, value: setting.value, source: setting.source ?? "plan" });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const pinned = pinnedByName.get(setting.name);
|
|
406
|
+
if (pinned) {
|
|
407
|
+
const pinnedValue = pinned.default === undefined ? "a fixed value" : JSON.stringify(pinned.default);
|
|
408
|
+
unbound.push(sameSettingValue(pinned.default, setting.value)
|
|
409
|
+
? `${shown}: pinned by the template at the same value`
|
|
410
|
+
: `${shown}: the template pins \`${setting.name}\` at ${pinnedValue}, and the plan value cannot be sent`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
unbound.push(`${shown}: no slot of the template carries it`);
|
|
414
|
+
}
|
|
283
415
|
return {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
416
|
+
briefing: {
|
|
417
|
+
template: {
|
|
418
|
+
ref,
|
|
419
|
+
version_served: contract.version,
|
|
420
|
+
slots: adaptable,
|
|
421
|
+
inputs: (contract.inputs ?? []).map((input) => ({
|
|
422
|
+
name: input.name,
|
|
423
|
+
path: input.path,
|
|
424
|
+
...(input.description ? { description: input.description } : {}),
|
|
425
|
+
})),
|
|
426
|
+
bound,
|
|
427
|
+
unbound_settings: unbound,
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
...(versionMatches
|
|
431
|
+
? {
|
|
432
|
+
binding: {
|
|
433
|
+
template: ref,
|
|
434
|
+
slots,
|
|
435
|
+
sources,
|
|
436
|
+
step: grounding.stepId,
|
|
437
|
+
claims: grounding.claims,
|
|
438
|
+
...(grounding.snapshot ? { snapshot: grounding.snapshot } : {}),
|
|
439
|
+
// The local slots of the contract, thus the tool binds a path or a column name on the machine
|
|
440
|
+
// without a second read of the contract.
|
|
441
|
+
local: contract.parameters.filter((slot) => slot.local === true),
|
|
442
|
+
adaptable: adaptable.map((slot) => slot.name),
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
289
446
|
};
|
|
290
447
|
}
|
|
291
448
|
/**
|
|
@@ -359,9 +516,20 @@ export function synthesisRowUpdate(outcome, durationMs) {
|
|
|
359
516
|
*/
|
|
360
517
|
export function synthesizeFindings(args) {
|
|
361
518
|
const { analysisId, runId, completedSteps, session, deps } = args;
|
|
519
|
+
const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis.synthesis");
|
|
520
|
+
// Observation is a diagnostic channel: a failed progress write is logged and never fails the synthesis.
|
|
521
|
+
let progressSeq = 0;
|
|
522
|
+
const writeProgressAlone = async (part, phase) => {
|
|
523
|
+
try {
|
|
524
|
+
await DBOS.writeStream("events", part);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
logger.warn("synthesis progress write failed (non-fatal)", { phase, ...logger.errorFields(err) });
|
|
528
|
+
}
|
|
529
|
+
};
|
|
362
530
|
const emit = (event) => {
|
|
363
531
|
const part = isChatDataPart(event)
|
|
364
|
-
? event
|
|
532
|
+
? flattenDataPart(event)
|
|
365
533
|
: {
|
|
366
534
|
type: "data-loop-event",
|
|
367
535
|
data: { stepId: "synthesis", event },
|
|
@@ -395,14 +563,15 @@ export function synthesizeFindings(args) {
|
|
|
395
563
|
// call path.
|
|
396
564
|
session: forStep(session, SYNTHESIS_STEP_ID),
|
|
397
565
|
emit,
|
|
398
|
-
onProgress: (phase, activity, extra = {}) =>
|
|
399
|
-
|
|
400
|
-
id
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
activity,
|
|
404
|
-
|
|
405
|
-
|
|
566
|
+
onProgress: async (phase, activity, extra = {}) => {
|
|
567
|
+
// Synthesis runs inside one `DBOS.runStep`, thus `DBOS.now()` reads the wall clock here, and a rerun of
|
|
568
|
+
// the step after a crash writes its entries again under the same ids. A reader dedups an entry by id.
|
|
569
|
+
const seq = progressSeq++;
|
|
570
|
+
const at = new Date(await DBOS.now()).toISOString();
|
|
571
|
+
const parts = synthesisEmission(runId, SYNTHESIS_STEP_ID, seq, phase, activity, at, extra);
|
|
572
|
+
await writeProgressAlone(parts.progress, phase);
|
|
573
|
+
await writeProgressAlone(parts.entry, phase);
|
|
574
|
+
},
|
|
406
575
|
});
|
|
407
576
|
}
|
|
408
577
|
// ── Workflow registration ────────────────────────────────────────────
|
|
@@ -847,17 +1016,21 @@ async function runSchedulerLoop(args) {
|
|
|
847
1016
|
// can be named. Checkpointed so a replay re-dispatches the child
|
|
848
1017
|
// with a byte-identical prompt instead of re-reading the DB/disk.
|
|
849
1018
|
const seedStepName = `compose-step-seed:${stepId}`;
|
|
850
|
-
const
|
|
1019
|
+
const checkpointed = await DBOS.runStep(() => {
|
|
851
1020
|
stableSpan(seedStepName, "compose-step-seed", { [ATTR_INFLEXA_STEP_ID]: stepId });
|
|
852
1021
|
return composeStepSeed({ input, stepId, runId, deps });
|
|
853
1022
|
}, { name: seedStepName });
|
|
1023
|
+
// A run recovered from a checkpoint written when this step returned the
|
|
1024
|
+
// bare seed string replays that string: it carries no binding.
|
|
1025
|
+
const seed = typeof checkpointed === "string" ? { prompt: checkpointed } : checkpointed;
|
|
854
1026
|
const baseChildInput = buildChildInput({
|
|
855
1027
|
input,
|
|
856
1028
|
stepId,
|
|
857
1029
|
level: levels.get(stepId) ?? 0,
|
|
858
1030
|
runId,
|
|
859
1031
|
workflowId,
|
|
860
|
-
prompt,
|
|
1032
|
+
prompt: seed.prompt,
|
|
1033
|
+
...(seed.templateBinding ? { templateBinding: seed.templateBinding } : {}),
|
|
861
1034
|
});
|
|
862
1035
|
const childInput = {
|
|
863
1036
|
...baseChildInput,
|
|
@@ -1038,6 +1211,9 @@ async function cancelInFlight(inFlight, cause, canceledByParent) {
|
|
|
1038
1211
|
recordCancelledChild({ cause });
|
|
1039
1212
|
}
|
|
1040
1213
|
}
|
|
1214
|
+
// ── (6) collectAndComplete ────────────────────────────────────────────
|
|
1215
|
+
/** The waits between the attempts are 1, 2, 4, and 8 seconds. */
|
|
1216
|
+
const FINAL_STATUS_WRITE_RETRY = { retriesAllowed: true, maxAttempts: 5, intervalSeconds: 1, backoffRate: 2 };
|
|
1041
1217
|
async function collectAndComplete(args) {
|
|
1042
1218
|
const { input, runId, workflowId, startedAtMs, completed, failed, canceled, suspension, failureReason, forceFailed, deps } = args;
|
|
1043
1219
|
// A forced failure is terminal and never a resumable suspension.
|
|
@@ -1066,18 +1242,9 @@ async function collectAndComplete(args) {
|
|
|
1066
1242
|
: suspended !== null
|
|
1067
1243
|
? "workflow-suspended"
|
|
1068
1244
|
: "workflow-canceled";
|
|
1069
|
-
|
|
1070
|
-
// status write below — and thus before any of the terminal cleanup steps. The CLI
|
|
1071
|
-
// watches the run by POLLING that row (`run.ts` `waitForRunTerminal`) and shuts down
|
|
1072
|
-
// the instant it leaves `running`, flushing provenance and then `process.exit()`ing;
|
|
1073
|
-
// emitting the record only after the status write races that shutdown and can drop
|
|
1074
|
-
// the run's terminal provenance entirely. Emitting first makes the record dirty
|
|
1075
|
-
// (synchronously, via the bus) before the row can be observed terminal, so the CLI's
|
|
1076
|
-
// flush always captures it. Not step-wrapped — a DBOS recovery replay must re-fire it;
|
|
1077
|
-
// `DBOS.now()` is checkpointed, so the span stays replay-stable (see `RunProvenanceEvent`).
|
|
1078
|
-
// The duration measures from the `run_started` read threaded in as `startedAtMs`.
|
|
1245
|
+
const finalError = failureReason ?? (status === "canceled" ? "external_cancel" : null);
|
|
1079
1246
|
const terminalAtMs = await DBOS.now();
|
|
1080
|
-
emitProvenanceGuarded(deps, {
|
|
1247
|
+
const emitRunEnd = () => emitProvenanceGuarded(deps, {
|
|
1081
1248
|
type: "run_completed",
|
|
1082
1249
|
analysisId: input.analysisId,
|
|
1083
1250
|
runId,
|
|
@@ -1085,12 +1252,58 @@ async function collectAndComplete(args) {
|
|
|
1085
1252
|
atMs: terminalAtMs,
|
|
1086
1253
|
durationMs: terminalAtMs - startedAtMs,
|
|
1087
1254
|
}, input.runSession);
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1255
|
+
// Ownership defaults to true for inputs persisted before the field existed (a
|
|
1256
|
+
// workflow recovered across this deploy) — those were always Cortex-owned.
|
|
1257
|
+
const authorization = {
|
|
1258
|
+
runSession: input.runSession,
|
|
1259
|
+
ownsMandate: input.ownsMandate ?? true, // oss-core-managed-ok
|
|
1260
|
+
};
|
|
1261
|
+
const closeCharge = () => deliverNotice(logger.with({ chargeOutcome: chargeOutcome.kind }), "RunCharge.close", deps.runCharge.close({
|
|
1262
|
+
analysisId: input.analysisId,
|
|
1263
|
+
runId,
|
|
1264
|
+
outcome: chargeOutcome,
|
|
1265
|
+
session: input.runSession,
|
|
1266
|
+
}));
|
|
1267
|
+
const revokeAuthorization = () => deliverNotice(logger.with({ revokeReason }), "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, revokeReason));
|
|
1268
|
+
// The writer that wins the row owns the run end: the `run_completed` emit, the charge close,
|
|
1269
|
+
// and the revoke. The step does all three when its write changes the row. An engine cancel
|
|
1270
|
+
// cannot stop a step that started, so a won write is never cut before these acts.
|
|
1271
|
+
//
|
|
1272
|
+
// The emit rides inside the transaction of the write. The update holds the row lock until the
|
|
1273
|
+
// commit, so the run canceler waits and then loses, and no reader sees the terminal row before
|
|
1274
|
+
// the emit. The cli polls the row and flushes the provenance when it leaves `running`; an emit
|
|
1275
|
+
// after the commit can miss that flush. `emitRunEvent` returns `void`, so the emit completes before the commit.
|
|
1276
|
+
//
|
|
1277
|
+
// DBOS retries only a thrown error, so an attempt before the last throws on a failed transaction.
|
|
1278
|
+
// The last attempt returns "failed" instead: the body takes the failed path with no `try`, and a
|
|
1279
|
+
// failure of DBOS to record a won step cannot be mistaken for a failed write. A commit can fail
|
|
1280
|
+
// after the emit, so `emittedInStep` keeps the later attempts from emitting again.
|
|
1281
|
+
let emittedInStep = false;
|
|
1282
|
+
const finalWrite = await DBOS.runStep(async () => {
|
|
1283
|
+
const stepLog = logger.with({ status }).named("persist-final-status");
|
|
1284
|
+
const written = await withTransaction(deps.pool, "runs.persistFinalStatus", (client) => updateRunStatus(client, runId, status, finalError).map((changed) => {
|
|
1285
|
+
if (changed && !emittedInStep) {
|
|
1286
|
+
emitRunEnd();
|
|
1287
|
+
emittedInStep = true;
|
|
1288
|
+
}
|
|
1289
|
+
return changed;
|
|
1290
|
+
}));
|
|
1291
|
+
if (written.isErr()) {
|
|
1292
|
+
const attempt = DBOS.stepStatus;
|
|
1293
|
+
const isLastAttempt = attempt?.currentAttempt === undefined || attempt.maxAttempts === undefined || attempt.currentAttempt >= attempt.maxAttempts;
|
|
1294
|
+
const failure = { error: describeDbError(written.error), ...stepLog.errorFields(written.error.cause) };
|
|
1295
|
+
if (isLastAttempt) {
|
|
1296
|
+
stepLog.error("ledger write failed", failure);
|
|
1297
|
+
return "failed";
|
|
1298
|
+
}
|
|
1299
|
+
stepLog.warn("ledger write failed, the step retries", { ...failure, attempt: attempt.currentAttempt });
|
|
1300
|
+
throw new Error(`ledger write failed: ${describeDbError(written.error)}`, { cause: written.error.cause });
|
|
1301
|
+
}
|
|
1302
|
+
// A refused write means an earlier write already set a terminal status: the run
|
|
1303
|
+
// canceler's, or an earlier run of this step before a crash. That writer owns the run end.
|
|
1304
|
+
if (!written.value)
|
|
1305
|
+
return "refused";
|
|
1306
|
+
// DBOS caches the step, so a recovered body records nothing again. A step's own terminal
|
|
1094
1307
|
// row already carried its metric; the residue counted here is every settled step
|
|
1095
1308
|
// without one — a child the cancel cut before `mark-canceled` (the cancel check runs
|
|
1096
1309
|
// before a step body), or one that died before `failStep`. The ledger read is kept
|
|
@@ -1107,8 +1320,41 @@ async function collectAndComplete(args) {
|
|
|
1107
1320
|
recordStepCompleted({ agentId: row.agentId, status: "failed" });
|
|
1108
1321
|
}
|
|
1109
1322
|
recordRunCompleted({ workflow: "analysis", status, durationMs: terminalAtMs - startedAtMs });
|
|
1110
|
-
|
|
1111
|
-
|
|
1323
|
+
await closeCharge();
|
|
1324
|
+
await revokeAuthorization();
|
|
1325
|
+
return "won";
|
|
1326
|
+
}, { name: "persist-final-status", ...FINAL_STATUS_WRITE_RETRY });
|
|
1327
|
+
// A refused write leaves two owners. When the canceler won, its engine cancel came before its
|
|
1328
|
+
// write, so `close-running-charge` throws the cancel and the body does no act of the run end.
|
|
1329
|
+
// When a recovery runs the status step again after its own write landed, no cancel occurred and
|
|
1330
|
+
// the body completes the run end here. A failed write leaves the row `running` with no owner,
|
|
1331
|
+
// and the body still closes and revokes.
|
|
1332
|
+
let closed = false;
|
|
1333
|
+
let revoked = false;
|
|
1334
|
+
if (finalWrite !== "won") {
|
|
1335
|
+
closed = await DBOS.runStep(closeCharge, { name: "close-running-charge" });
|
|
1336
|
+
revoked = await DBOS.runStep(revokeAuthorization, { name: "revoke-run-auth" });
|
|
1337
|
+
}
|
|
1338
|
+
// The step emits only in the execution that ran it. A won step that DBOS gives from the cache
|
|
1339
|
+
// did not run, and a refused or failed write did not emit; the body fires the run end here
|
|
1340
|
+
// with the same checkpointed values. It is not step-wrapped, so a recovery replay fires it again.
|
|
1341
|
+
if (!emittedInStep)
|
|
1342
|
+
emitRunEnd();
|
|
1343
|
+
// After a failed write the row is not terminal, so the body records its run end for the run
|
|
1344
|
+
// canceler. The record comes after the acts: a cancel before it stops the body and the canceler
|
|
1345
|
+
// then owns the run end. A cancel is the only error that goes back to DBOS; a failed record must
|
|
1346
|
+
// not undo an otherwise-complete run, like the sibling terminal steps.
|
|
1347
|
+
if (finalWrite === "failed") {
|
|
1348
|
+
const recorded = { status, error: finalError, closed, revoked };
|
|
1349
|
+
try {
|
|
1350
|
+
await DBOS.setEvent(RUN_END_EVENT_KEY, recorded);
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
if (error instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
1354
|
+
throw error;
|
|
1355
|
+
logger.error("run end record failed, the run canceler acts as for a run with no record", { status, ...logger.errorFields(error) });
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1112
1358
|
// Record the run's synthesis outcome AFTER the status write — a reader that
|
|
1113
1359
|
// sees a terminal status then also sees whether (and why) synthesis ran. A
|
|
1114
1360
|
// null outcome means synthesis never ran; leave the columns NULL ("unknown").
|
|
@@ -1119,39 +1365,21 @@ async function collectAndComplete(args) {
|
|
|
1119
1365
|
await ledgerStep(logger.with({ synthesisStatus: outcome.status }), "persist-synthesis-outcome", () => setRunSynthesisOutcome(deps.pool, runId, outcome.status, outcome.reason));
|
|
1120
1366
|
}
|
|
1121
1367
|
// Sweep never-started steps to `skipped` — but ONLY on genuinely-terminal
|
|
1122
|
-
// paths. The gate is the BRANCH, not the written run status: the
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
// finalisation step here.
|
|
1368
|
+
// paths. The gate is the BRANCH, not the written run status: the pending rows
|
|
1369
|
+
// of a suspended run must survive for a later resume. Log-don't-rollback, like
|
|
1370
|
+
// every other finalisation step here.
|
|
1126
1371
|
if (suspended === null) {
|
|
1127
1372
|
await ledgerStep(logger, "sweep-pending-steps", () => sweepPendingStepExecutions(deps.pool, runId));
|
|
1128
1373
|
}
|
|
1129
1374
|
if (suspended !== null) {
|
|
1130
1375
|
await ledgerStep(logger, "suspend-analysis", () => suspendAnalysisQuery(deps.pool, input.analysisId));
|
|
1131
1376
|
}
|
|
1132
|
-
|
|
1133
|
-
analysisId: input.analysisId,
|
|
1134
|
-
runId,
|
|
1135
|
-
outcome: chargeOutcome,
|
|
1136
|
-
session: input.runSession,
|
|
1137
|
-
})), { name: "close-running-charge" });
|
|
1138
|
-
// Revoke the run authorization for the terminal run state. Ownership
|
|
1139
|
-
// defaults to true for inputs persisted before the field existed (a
|
|
1140
|
-
// workflow recovered across this deploy) — those were always Cortex-owned.
|
|
1141
|
-
const authorization = {
|
|
1142
|
-
runSession: input.runSession,
|
|
1143
|
-
ownsMandate: input.ownsMandate ?? true, // oss-core-managed-ok
|
|
1144
|
-
};
|
|
1145
|
-
await DBOS.runStep(() => deliverNotice(logger.with({ revokeReason }), "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, revokeReason)), {
|
|
1146
|
-
name: "revoke-run-auth",
|
|
1147
|
-
});
|
|
1148
|
-
// The `run_completed` provenance was already emitted above (before the status write, to
|
|
1149
|
-
// beat the CLI's poll-and-shutdown). These branches only fan out the UI stream part, whose
|
|
1377
|
+
// The run end was emitted above. These branches only fan out the UI stream part, whose
|
|
1150
1378
|
// completed/failed shapes genuinely differ — the provenance record does not.
|
|
1151
1379
|
if (status === "completed" || status === "partial") {
|
|
1152
|
-
const artifactCount = await DBOS.runStep(async () => await
|
|
1153
|
-
|
|
1154
|
-
|
|
1380
|
+
const artifactCount = await DBOS.runStep(async () => await countArtifactsByRuns(deps.pool, input.analysisId, [runId])
|
|
1381
|
+
.map((counts) => counts[0]?.count ?? 0)
|
|
1382
|
+
.unwrapOr(0), { name: "count-run-artifacts" });
|
|
1155
1383
|
await emitStreamPart({
|
|
1156
1384
|
type: "data-run-completed",
|
|
1157
1385
|
runId,
|
|
@@ -1197,7 +1425,7 @@ async function collectAndComplete(args) {
|
|
|
1197
1425
|
function deriveFinalStatus(args) {
|
|
1198
1426
|
const { totalSteps, completed, failed, canceled, suspended } = args;
|
|
1199
1427
|
if (suspended)
|
|
1200
|
-
return "
|
|
1428
|
+
return "suspended_insufficient_funds";
|
|
1201
1429
|
if (failed.size > 0) {
|
|
1202
1430
|
return completed.size > 0 ? "partial" : "failed";
|
|
1203
1431
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graph of a stored plan: each step with its agent, its dependencies, and its
|
|
3
|
+
* level. The levels and the agent fallback are those of `data-dag-state`, so a
|
|
4
|
+
* host draws the same graph before, during, and after a run.
|
|
5
|
+
*/
|
|
6
|
+
import type { PlanGraphStep } from "../../contracts/chat-parts.js";
|
|
7
|
+
/**
|
|
8
|
+
* Give the graph of a stored plan document, in the order of the plan. An empty
|
|
9
|
+
* step list gives an empty graph. A document that fails the stored plan schema, or
|
|
10
|
+
* whose steps have a duplicate id, a missing dependency, or a cycle, gives `null`
|
|
11
|
+
* (a plan that no run can execute has no levels).
|
|
12
|
+
*
|
|
13
|
+
* The throw of `validatePlanDag` is caught here, at the boundary: the function is a
|
|
14
|
+
* read for a caller that has no use for the typed error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function planGraphOf(plan: unknown): PlanGraphStep[] | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graph of a stored plan: each step with its agent, its dependencies, and its
|
|
3
|
+
* level. The levels and the agent fallback are those of `data-dag-state`, so a
|
|
4
|
+
* host draws the same graph before, during, and after a run.
|
|
5
|
+
*/
|
|
6
|
+
import { AnalysisPlanSchema } from "../../schemas/workflow-state.js";
|
|
7
|
+
import { computeTopologicalLevels, validatePlanDag } from "../execute-analysis-scheduler.js";
|
|
8
|
+
/**
|
|
9
|
+
* Give the graph of a stored plan document, in the order of the plan. An empty
|
|
10
|
+
* step list gives an empty graph. A document that fails the stored plan schema, or
|
|
11
|
+
* whose steps have a duplicate id, a missing dependency, or a cycle, gives `null`
|
|
12
|
+
* (a plan that no run can execute has no levels).
|
|
13
|
+
*
|
|
14
|
+
* The throw of `validatePlanDag` is caught here, at the boundary: the function is a
|
|
15
|
+
* read for a caller that has no use for the typed error.
|
|
16
|
+
*/
|
|
17
|
+
export function planGraphOf(plan) {
|
|
18
|
+
const parsed = AnalysisPlanSchema.safeParse(plan);
|
|
19
|
+
if (!parsed.success)
|
|
20
|
+
return null;
|
|
21
|
+
const steps = parsed.data.steps;
|
|
22
|
+
try {
|
|
23
|
+
validatePlanDag(steps);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const levels = computeTopologicalLevels(steps);
|
|
29
|
+
return steps.map((step) => ({
|
|
30
|
+
id: step.id,
|
|
31
|
+
name: step.name,
|
|
32
|
+
agent: step.agent ?? "unknown",
|
|
33
|
+
dependsOn: [...step.depends_on],
|
|
34
|
+
level: levels.get(step.id) ?? 0,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
@@ -49,6 +49,7 @@ import type { SandboxRef } from "../sandbox/types.js";
|
|
|
49
49
|
import { ProvenanceCollector } from "../provenance/collector.js";
|
|
50
50
|
import { type BlockerHolder } from "../tools/sandbox/report-blocker.js";
|
|
51
51
|
import { type FileMetadataCell } from "../tools/sandbox/submit-file-metadata.js";
|
|
52
|
+
import type { TemplateBinding } from "../tools/knowledge/template.js";
|
|
52
53
|
/**
|
|
53
54
|
* Input the parent hands the child. Must be JSON-serialisable — DBOS
|
|
54
55
|
* persists this as the workflow's input row.
|
|
@@ -70,6 +71,17 @@ export interface SandboxStepInput {
|
|
|
70
71
|
* rather than admitting unstable ones.
|
|
71
72
|
*/
|
|
72
73
|
readonly dependsOn?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* The plan settings bound to the template of this step, composed by the
|
|
76
|
+
* parent at dispatch beside the seed. The agent factory hands it to
|
|
77
|
+
* `knowledge_template`, which merges the bound slots under the model
|
|
78
|
+
* values and refuses a silent change of one.
|
|
79
|
+
*
|
|
80
|
+
* Optional for the same reason as `dependsOn`: a durable input that
|
|
81
|
+
* predates the field recovers without it, and a step without a grounded
|
|
82
|
+
* template has nothing to bind. Absence means the model values ride alone.
|
|
83
|
+
*/
|
|
84
|
+
readonly templateBinding?: TemplateBinding;
|
|
73
85
|
/** Topological level — persisted on `cortex_step_executions.wave`. */
|
|
74
86
|
readonly level: number;
|
|
75
87
|
/** User-content prompt the agent receives as its initial message. */
|