@danypops/papyrus 0.41.0 → 0.42.0
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/README.md +8 -11
- package/package.json +1 -1
- package/src/artifact-relationship-view.ts +1 -1
- package/src/cli.ts +12 -178
- package/src/constants.ts +19 -52
- package/src/db.ts +34 -7
- package/src/domain/artifact-event.ts +1 -1
- package/src/domain/blueprint-definition.ts +273 -0
- package/src/domain-services.ts +147 -204
- package/src/modules/logs.ts +1 -1
- package/src/modules/playbooks.ts +9 -7
- package/src/ops.ts +3 -1
- package/src/playbook-definition.ts +75 -29
- package/src/playbook-execution.ts +7 -24
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/service.ts +5 -21
- package/src/task-service.ts +1 -1
- package/src/vehicle/artifact-trash-vehicle.ts +1 -1
- package/src/vehicle/artifact-vehicle-shared.ts +4 -6
- package/src/vehicle/docs-vehicle.ts +2 -2
- package/src/vehicle/notes-vehicle.ts +2 -2
- package/src/vehicle/papyrus-vehicle.ts +2 -5
- package/src/vehicle/playbooks-vehicle.ts +1 -1
- package/src/workflow-execution.ts +139 -69
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
|
@@ -3,13 +3,14 @@ import { SKILL_MAX_RENDERED_BYTES, SKILL_RUN_ID_MAX_LENGTH, SKILL_WORKFLOW_MAX_N
|
|
|
3
3
|
import type { Artifact } from "./domain/artifact.ts";
|
|
4
4
|
import { validateChecklist } from "./domain/checklist.ts";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
} from "./domain/
|
|
6
|
+
resolveBlueprintArguments,
|
|
7
|
+
validateBlueprintDefinition,
|
|
8
|
+
type BlueprintArgumentValue,
|
|
9
|
+
type CallBlueprint,
|
|
10
|
+
type BlueprintDefinition,
|
|
11
|
+
} from "./domain/blueprint-definition.ts";
|
|
12
12
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
13
|
+
import { compilePlaybookDefinition, type PlaybookExternalLink } from "./playbook-definition.ts";
|
|
13
14
|
import type { TaskEventContext } from "./domain/task-event.ts";
|
|
14
15
|
import type { TaskEventStore } from "./ports/task-event-store.ts";
|
|
15
16
|
import type { TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
@@ -34,7 +35,7 @@ export interface InstantiateSkillWorkflowInput {
|
|
|
34
35
|
* Identifies who owns a materialized run for tagging purposes: which artifact gets the
|
|
35
36
|
* `triggers` edges to its root tasks, which extra-bag key records run lineage on each created
|
|
36
37
|
* artifact, and which label prefix scopes them. Defaults used by instantiateSkillWorkflow
|
|
37
|
-
* (ownerId: the
|
|
38
|
+
* (ownerId: the target's own id, extraKey: "skillRun", labelPrefix: "skill-run") are unchanged
|
|
38
39
|
* from before this was made pluggable -- a Playbook-compiled run supplies its own (playbook id,
|
|
39
40
|
* "playbookRun", "playbook-run") instead, the only thing that actually differs between the two.
|
|
40
41
|
*/
|
|
@@ -47,15 +48,15 @@ export interface WorkflowLineage {
|
|
|
47
48
|
export interface WorkflowRunResult {
|
|
48
49
|
skillId: string;
|
|
49
50
|
runId: string;
|
|
50
|
-
arguments: Record<string,
|
|
51
|
+
arguments: Record<string, BlueprintArgumentValue>;
|
|
51
52
|
created: {
|
|
52
53
|
docs: string[];
|
|
53
54
|
rules: string[];
|
|
54
55
|
tasks: string[];
|
|
55
|
-
/** Nested workflow
|
|
56
|
+
/** Nested workflow-definition runs this pipeline triggered as pipeline steps, in execution order. */
|
|
56
57
|
skillRuns: string[];
|
|
57
58
|
};
|
|
58
|
-
/** Real starting points: for a nested
|
|
59
|
+
/** Real starting points: for a nested call root step, that nested run's own root tasks (recursively), not just "all its tasks". */
|
|
59
60
|
rootTaskIds: string[];
|
|
60
61
|
/** Resolved from input.focusRef when supplied -- the one real task id a caller (e.g. Playbook invocation) should focus, undefined when focusRef was not requested or names an unknown ref. */
|
|
61
62
|
entryTaskId?: string;
|
|
@@ -63,34 +64,39 @@ export interface WorkflowRunResult {
|
|
|
63
64
|
execution: TaskExecutionPlan;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
/**
|
|
68
|
+
* A definition-shaped target: kind=playbook (Skill-the-kind is retired; every remaining
|
|
69
|
+
* definition-holding row -- migrated legacy or freshly constructed -- lives under kind=playbook
|
|
70
|
+
* now) with subtype=workflow, distinguishing it from an ordinary steps/trigger-shaped Playbook.
|
|
71
|
+
*/
|
|
72
|
+
function requireWorkflowSkill(artifacts: ArtifactStore, skillId: string): { skill: Artifact; definition: BlueprintDefinition } {
|
|
67
73
|
const skill = artifacts.get(skillId);
|
|
68
|
-
if (!skill) throw new Error(`
|
|
69
|
-
if (skill.kind !== "
|
|
70
|
-
throw new Error(`artifact "${skillId}" is not a workflow
|
|
74
|
+
if (!skill) throw new Error(`playbook artifact "${skillId}" not found`);
|
|
75
|
+
if (skill.kind !== "playbook" || skill.subtype !== "workflow") {
|
|
76
|
+
throw new Error(`artifact "${skillId}" is not a workflow-definition playbook`);
|
|
71
77
|
}
|
|
72
|
-
if (skill.status !== "active") throw new Error(`cannot run workflow
|
|
73
|
-
return { skill, definition:
|
|
78
|
+
if (skill.status !== "active") throw new Error(`cannot run workflow playbook from ${skill.status}`);
|
|
79
|
+
return { skill, definition: validateBlueprintDefinition(skill.extra["definition"]) };
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
function normalizeRunId(skillId: string, requested: string | undefined): string {
|
|
77
83
|
const runId = requested ?? `${skillId.slice(0, 40)}-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
78
84
|
if (runId.length > SKILL_RUN_ID_MAX_LENGTH || !RUN_ID_PATTERN.test(runId)) {
|
|
79
|
-
throw new Error(`
|
|
85
|
+
throw new Error(`run id must match ${RUN_ID_PATTERN} and contain at most ${SKILL_RUN_ID_MAX_LENGTH} characters`);
|
|
80
86
|
}
|
|
81
87
|
return runId;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
function renderValue(value: unknown, arguments_: Record<string,
|
|
90
|
+
function renderValue(value: unknown, arguments_: Record<string, BlueprintArgumentValue>): unknown {
|
|
85
91
|
if (typeof value === "string") {
|
|
86
92
|
const exact = value.match(EXACT_PLACEHOLDER_PATTERN);
|
|
87
93
|
if (exact) {
|
|
88
94
|
const name = exact[1]!;
|
|
89
|
-
if (!(name in arguments_)) throw new Error(`
|
|
95
|
+
if (!(name in arguments_)) throw new Error(`input placeholder "${name}" has no argument value`);
|
|
90
96
|
return arguments_[name]!;
|
|
91
97
|
}
|
|
92
98
|
return value.replace(PLACEHOLDER_PATTERN, (_placeholder, name: string) => {
|
|
93
|
-
if (!(name in arguments_)) throw new Error(`
|
|
99
|
+
if (!(name in arguments_)) throw new Error(`input placeholder "${name}" has no argument value`);
|
|
94
100
|
return String(arguments_[name]!);
|
|
95
101
|
});
|
|
96
102
|
}
|
|
@@ -98,29 +104,29 @@ function renderValue(value: unknown, arguments_: Record<string, SkillArgumentVal
|
|
|
98
104
|
if (typeof value !== "object" || value === null) return value;
|
|
99
105
|
const rendered: Record<string, unknown> = {};
|
|
100
106
|
for (const [key, entry] of Object.entries(value)) {
|
|
101
|
-
if (UNSAFE_KEYS.has(key)) throw new Error(`unsafe
|
|
107
|
+
if (UNSAFE_KEYS.has(key)) throw new Error(`unsafe blueprint key "${key}"`);
|
|
102
108
|
rendered[key] = renderValue(entry, arguments_);
|
|
103
109
|
}
|
|
104
110
|
return rendered;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
function renderDefinition(definition:
|
|
108
|
-
const rendered = renderValue(definition, arguments_) as
|
|
113
|
+
function renderDefinition(definition: BlueprintDefinition, arguments_: Record<string, BlueprintArgumentValue>): BlueprintDefinition {
|
|
114
|
+
const rendered = renderValue(definition, arguments_) as BlueprintDefinition;
|
|
109
115
|
const bytes = new TextEncoder().encode(JSON.stringify(rendered)).byteLength;
|
|
110
|
-
if (bytes > SKILL_MAX_RENDERED_BYTES) throw new Error(`rendered
|
|
116
|
+
if (bytes > SKILL_MAX_RENDERED_BYTES) throw new Error(`rendered workflow exceeds ${SKILL_MAX_RENDERED_BYTES} bytes`);
|
|
111
117
|
for (const task of rendered.blueprints.tasks) {
|
|
112
118
|
if (task.extra?.["checklist"] !== undefined) {
|
|
113
119
|
task.extra["checklist"] = validateChecklist(task.extra["checklist"]);
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
|
-
return
|
|
122
|
+
return validateBlueprintDefinition(rendered);
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
function withRunLabel(labels: string[] | undefined, labelPrefix: string, runId: string): string[] {
|
|
120
126
|
return [...new Set([...(labels ?? []), `${labelPrefix}:${runId}`])];
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
function executionGraph(tasks: Artifact[], definition:
|
|
129
|
+
function executionGraph(tasks: Artifact[], definition: BlueprintDefinition, ids: Map<string, string>, extraKey: string): TaskGraph {
|
|
124
130
|
const byRef = new Map(definition.blueprints.tasks.map((task) => [task.ref, task]));
|
|
125
131
|
const nodes: TaskNode[] = tasks.map((task) => {
|
|
126
132
|
const ref = task.extra[extraKey] && typeof task.extra[extraKey] === "object"
|
|
@@ -138,15 +144,15 @@ function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map
|
|
|
138
144
|
return { nodes, rootIds: nodes.filter((node) => node.parentIds.length === 0).map((node) => node.task.id) };
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
/** projectRoot is optional -- skills.run always supplies one (workflow
|
|
147
|
+
/** projectRoot is optional -- skills.run always supplies one (workflow-definition runs are always project-scoped today), while a Playbook invocation may legitimately be ad hoc/cross-project (e.g. a lab-deploy playbook not tied to any one repo), landing its tasks in the same "unscoped" bucket Tasks.create already supports for a caller that omits projectRoot entirely. */
|
|
142
148
|
export type WorkflowRunHistory = { events: TaskEventStore; scopes: TaskScopeStore; projectRoot?: string; context?: TaskEventContext };
|
|
143
149
|
|
|
144
150
|
/**
|
|
145
151
|
* Public entry point: wraps one complete pipeline run (including every nested sub-pipeline
|
|
146
152
|
* it triggers) in exactly one atomic transaction. The recursive core (runWorkflowSteps) never
|
|
147
153
|
* opens its own atomic wrapper -- SQLite savepoint nesting (inTransaction in db.ts) would
|
|
148
|
-
* tolerate it, but wrapping once here keeps the atomicity story unambiguous: one
|
|
149
|
-
*
|
|
154
|
+
* tolerate it, but wrapping once here keeps the atomicity story unambiguous: one run call is
|
|
155
|
+
* one all-or-nothing graph mutation, however many nested targets it triggers.
|
|
150
156
|
*/
|
|
151
157
|
export function instantiateSkillWorkflow(
|
|
152
158
|
artifacts: ArtifactStore,
|
|
@@ -159,31 +165,80 @@ export function instantiateSkillWorkflow(
|
|
|
159
165
|
return requireAtomicArtifactStore(artifacts).atomic(run);
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
/** Reads each created task's own lineage.ref tag back off the store -- resolves a compiled Playbook's blueprint refs back to real task ids after materialization, without threading an extra ref-to-id map out of materializeWorkflowDefinition's own return shape. Shared by top-level Playbook invocation (playbook-execution.ts) and a nested Playbook pipeline-call step alike. */
|
|
169
|
+
export function resolveRefToTaskId(artifacts: ArtifactStore, taskIds: string[], extraKey: string): Map<string, string> {
|
|
170
|
+
const map = new Map<string, string>();
|
|
171
|
+
for (const taskId of taskIds) {
|
|
172
|
+
const lineage = artifacts.get(taskId)?.extra[extraKey];
|
|
173
|
+
if (typeof lineage !== "object" || lineage === null || Array.isArray(lineage)) continue;
|
|
174
|
+
const ref = (lineage as Record<string, unknown>)["ref"];
|
|
175
|
+
if (typeof ref === "string") map.set(ref, taskId);
|
|
176
|
+
}
|
|
177
|
+
return map;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Applies a compiled Playbook's external links (a Rule that `gates` it, a Doc it `references`, etc.) once its blueprint refs have resolved to real task ids -- shared by top-level Playbook invocation and a nested Playbook pipeline-call step alike. */
|
|
181
|
+
export function applyPlaybookExternalLinks(artifacts: ArtifactStore, externalLinks: PlaybookExternalLink[], refToTaskId: Map<string, string>): void {
|
|
182
|
+
for (const link of externalLinks) {
|
|
183
|
+
const taskId = refToTaskId.get(link.rootRef);
|
|
184
|
+
if (!taskId) continue; // defensive -- every rootRef the compiler emits is always materialized
|
|
185
|
+
if (link.ownerIsFrom) artifacts.link({ from: taskId, relation: link.relation, to: link.otherArtifactId });
|
|
186
|
+
else artifacts.link({ from: link.otherArtifactId, relation: link.relation, to: taskId });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
162
190
|
/**
|
|
163
|
-
* The recursive pipeline core. A
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
191
|
+
* The recursive pipeline core. A `skills` blueprint entry's target can be either a
|
|
192
|
+
* workflow-definition Playbook (an already-persisted, versioned JSON blueprint) or an ordinary
|
|
193
|
+
* steps/trigger-shaped Playbook (its steps/composition tree compiled fresh, right here, the
|
|
194
|
+
* same way a top-level Playbook invocation does) -- the Jenkins "downstream job" / Ansible
|
|
195
|
+
* "include_tasks" primitive either way, dispatched purely by the target artifact's own subtype.
|
|
196
|
+
* Nested runs execute BEFORE this level's dependsOn/parent edges are wired, since a step
|
|
197
|
+
* depending on a call ref needs to know every task id that nested run actually produced (not
|
|
198
|
+
* knowable ahead of time -- it depends on the nested target's own definition). `ancestorIds`
|
|
199
|
+
* tracks the current call CHAIN (not a global ever-visited set): sibling calls under the same
|
|
200
|
+
* parent are independent and may legitimately share a called target; only a real cycle back to
|
|
201
|
+
* an ancestor is rejected. This is a SEPARATE cycle/depth check from a Playbook's own
|
|
202
|
+
* contains/depends_on composition tree (playbook-definition.ts's compileNode) -- a cycle
|
|
203
|
+
* threading through BOTH graphs at once (Playbook composition -> a call step -> back into that
|
|
204
|
+
* same Playbook's composition) is not cross-checked between the two, but each graph's own
|
|
205
|
+
* independent depth cap still bounds it; it fails with a nesting-depth error rather than a
|
|
206
|
+
* precise cycle message, not an infinite loop.
|
|
171
207
|
*/
|
|
172
208
|
function runWorkflowSteps(
|
|
173
209
|
artifacts: ArtifactStore,
|
|
174
|
-
|
|
210
|
+
targetId: string,
|
|
175
211
|
input: InstantiateSkillWorkflowInput,
|
|
176
212
|
history: WorkflowRunHistory | undefined,
|
|
177
|
-
|
|
213
|
+
ancestorIds: ReadonlySet<string>,
|
|
178
214
|
depth: number,
|
|
179
215
|
): WorkflowRunResult {
|
|
180
|
-
if (
|
|
181
|
-
if (depth > SKILL_WORKFLOW_MAX_NESTING_DEPTH) throw new Error(`
|
|
182
|
-
const nextAncestors = new Set([...
|
|
183
|
-
const
|
|
216
|
+
if (ancestorIds.has(targetId)) throw new Error(`workflow nesting cycle includes "${targetId}"`);
|
|
217
|
+
if (depth > SKILL_WORKFLOW_MAX_NESTING_DEPTH) throw new Error(`workflow nesting exceeds ${SKILL_WORKFLOW_MAX_NESTING_DEPTH} levels`);
|
|
218
|
+
const nextAncestors = new Set([...ancestorIds, targetId]);
|
|
219
|
+
const target = artifacts.get(targetId);
|
|
220
|
+
// subtype=workflow is the legacy definition-shaped case (raw extra.definition, no
|
|
221
|
+
// extra.steps) -- compilePlaybookDefinition assumes an ordinary steps/trigger-shaped
|
|
222
|
+
// Playbook and must not see it; it falls through to requireWorkflowSkill below instead.
|
|
223
|
+
if (target?.kind === "playbook" && target.subtype !== "workflow") {
|
|
224
|
+
const compiled = compilePlaybookDefinition(artifacts, targetId);
|
|
225
|
+
const result = materializeWorkflowDefinition(
|
|
226
|
+
artifacts,
|
|
227
|
+
{ ownerId: targetId, extraKey: "playbookRun", labelPrefix: "playbook-run" },
|
|
228
|
+
compiled.definition,
|
|
229
|
+
{ ...input, focusRef: compiled.entryRef },
|
|
230
|
+
history,
|
|
231
|
+
nextAncestors,
|
|
232
|
+
depth,
|
|
233
|
+
);
|
|
234
|
+
const refToTaskId = resolveRefToTaskId(artifacts, result.created.tasks, "playbookRun");
|
|
235
|
+
applyPlaybookExternalLinks(artifacts, compiled.externalLinks, refToTaskId);
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
const { definition } = requireWorkflowSkill(artifacts, targetId);
|
|
184
239
|
return materializeWorkflowDefinition(
|
|
185
240
|
artifacts,
|
|
186
|
-
{ ownerId:
|
|
241
|
+
{ ownerId: targetId, extraKey: "skillRun", labelPrefix: "skill-run" },
|
|
187
242
|
definition,
|
|
188
243
|
input,
|
|
189
244
|
history,
|
|
@@ -193,23 +248,23 @@ function runWorkflowSteps(
|
|
|
193
248
|
}
|
|
194
249
|
|
|
195
250
|
/**
|
|
196
|
-
* The definition-materialization core, shared by workflow
|
|
197
|
-
* via runWorkflowSteps above) and Playbook invocation
|
|
198
|
-
* ALREADY-RESOLVED
|
|
199
|
-
* path, compiled in-memory from a Playbook's
|
|
200
|
-
* depends_on composition tree for the Playbook path --
|
|
201
|
-
* wires dependsOn/parent/links, recurses into nested
|
|
202
|
-
* Playbook-compiled definition, which never populates
|
|
203
|
-
* created artifact and the run's containing labels via
|
|
204
|
-
* "skillRun"/"skill-run" shape -- the only thing that differs
|
|
205
|
-
* `ancestorSkillIds`/`depth` are the same cycle/nesting-depth tracking
|
|
206
|
-
* enforced before this was extracted; a Playbook caller with no nested
|
|
207
|
-
* into passes an empty set and depth 0 and never revisits this function itself.
|
|
251
|
+
* The definition-materialization core, shared by workflow-definition targets
|
|
252
|
+
* (instantiateSkillWorkflow, via runWorkflowSteps above) and Playbook invocation
|
|
253
|
+
* (playbook-execution.ts): given an ALREADY-RESOLVED BlueprintDefinition -- fetched from a
|
|
254
|
+
* persisted workflow-definition artifact for that path, compiled in-memory from a Playbook's
|
|
255
|
+
* steps/trigger/arguments and its contains/depends_on composition tree for the Playbook path --
|
|
256
|
+
* creates every blueprint artifact, wires dependsOn/parent/links, recurses into nested call
|
|
257
|
+
* pipeline steps (a no-op for a Playbook-compiled definition, which never populates
|
|
258
|
+
* blueprints.skills), and tags every created artifact and the run's containing labels via
|
|
259
|
+
* `lineage` rather than a hardcoded "skillRun"/"skill-run" shape -- the only thing that differs
|
|
260
|
+
* between the two callers. `ancestorSkillIds`/`depth` are the same cycle/nesting-depth tracking
|
|
261
|
+
* runWorkflowSteps already enforced before this was extracted; a Playbook caller with no nested
|
|
262
|
+
* calls to recurse into passes an empty set and depth 0 and never revisits this function itself.
|
|
208
263
|
*/
|
|
209
264
|
export function materializeWorkflowDefinition(
|
|
210
265
|
artifacts: ArtifactStore,
|
|
211
266
|
lineage: WorkflowLineage,
|
|
212
|
-
definition:
|
|
267
|
+
definition: BlueprintDefinition,
|
|
213
268
|
input: InstantiateSkillWorkflowInput,
|
|
214
269
|
history: WorkflowRunHistory | undefined,
|
|
215
270
|
ancestorSkillIds: ReadonlySet<string>,
|
|
@@ -217,7 +272,7 @@ export function materializeWorkflowDefinition(
|
|
|
217
272
|
): WorkflowRunResult {
|
|
218
273
|
const { ownerId, extraKey, labelPrefix } = lineage;
|
|
219
274
|
const projectRoot = history?.projectRoot !== undefined ? normalizeProjectRoot(history.projectRoot) : undefined;
|
|
220
|
-
const arguments_ =
|
|
275
|
+
const arguments_ = resolveBlueprintArguments(definition, input.arguments);
|
|
221
276
|
const rendered = renderDefinition(definition, arguments_);
|
|
222
277
|
const runId = normalizeRunId(ownerId, input.runId);
|
|
223
278
|
const refs = [
|
|
@@ -231,14 +286,14 @@ export function materializeWorkflowDefinition(
|
|
|
231
286
|
// A bound at THIS level's own blueprint size; nested runs are independently bounded the same
|
|
232
287
|
// way at their own level, and nesting depth is separately capped -- so total blast radius
|
|
233
288
|
// across a whole pipeline stays bounded on both dimensions even though a step's dependency
|
|
234
|
-
// on a
|
|
289
|
+
// on a call ref can fan out to more edges than this per-level count captures exactly.
|
|
235
290
|
const relationshipCount = rendered.links.length
|
|
236
291
|
+ rendered.blueprints.tasks.reduce((count, task) => count + (task.dependsOn?.length ?? 0) + (task.parent ? 2 : 0), 0)
|
|
237
292
|
+ rendered.blueprints.skills.reduce((count, call) => count + (call.dependsOn?.length ?? 0) + (call.parent ? 2 : 0), 0)
|
|
238
293
|
+ rendered.blueprints.tasks.filter((task) => (task.dependsOn?.length ?? 0) === 0).length
|
|
239
294
|
+ rendered.blueprints.skills.filter((call) => (call.dependsOn?.length ?? 0) === 0).length;
|
|
240
295
|
if (relationshipCount > TASK_EXECUTION_MAX_EDGES) {
|
|
241
|
-
throw new Error(`
|
|
296
|
+
throw new Error(`workflow run exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
|
|
242
297
|
}
|
|
243
298
|
|
|
244
299
|
const docs = rendered.blueprints.docs.map((blueprint) => artifacts.create({
|
|
@@ -291,17 +346,21 @@ export function materializeWorkflowDefinition(
|
|
|
291
346
|
|
|
292
347
|
// Nested pipeline steps run before edge-wiring: dependents need to know what tasks each
|
|
293
348
|
// nested run actually produced. stepTaskIds/stepRootTaskIds map EVERY step ref (task or
|
|
294
|
-
//
|
|
295
|
-
//
|
|
349
|
+
// call) to the task id(s) it resolves to, so dependsOn/parent wiring below treats both
|
|
350
|
+
// kinds of step uniformly.
|
|
296
351
|
const nestedRuns: WorkflowRunResult[] = [];
|
|
297
352
|
const stepTaskIds = new Map<string, string[]>(tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, [task.id]]));
|
|
298
353
|
const stepRootTaskIds = new Map<string, string[]>(
|
|
299
354
|
tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, (rendered.blueprints.tasks[index]!.dependsOn?.length ?? 0) === 0 ? [task.id] : []]),
|
|
300
355
|
);
|
|
301
|
-
|
|
356
|
+
// A call ref never gets its own real task -- it resolves to whatever the nested run's entry
|
|
357
|
+
// (or, absent a resolvable one, its first root task) actually is. Lets a focusRef chain
|
|
358
|
+
// straight through an arbitrary number of nested calls down to a real task, recursively.
|
|
359
|
+
const stepEntryTaskIds = new Map<string, string>();
|
|
360
|
+
for (const call of rendered.blueprints.skills as CallBlueprint[]) {
|
|
302
361
|
const nested = runWorkflowSteps(
|
|
303
362
|
artifacts,
|
|
304
|
-
call.
|
|
363
|
+
call.targetId,
|
|
305
364
|
{ runId: `${runId}-${call.ref}`, arguments: call.arguments },
|
|
306
365
|
history,
|
|
307
366
|
ancestorSkillIds,
|
|
@@ -310,6 +369,8 @@ export function materializeWorkflowDefinition(
|
|
|
310
369
|
nestedRuns.push(nested);
|
|
311
370
|
stepTaskIds.set(call.ref, nested.created.tasks);
|
|
312
371
|
stepRootTaskIds.set(call.ref, nested.rootTaskIds);
|
|
372
|
+
const entry = nested.entryTaskId ?? nested.rootTaskIds[0];
|
|
373
|
+
if (entry !== undefined) stepEntryTaskIds.set(call.ref, entry);
|
|
313
374
|
}
|
|
314
375
|
|
|
315
376
|
for (const blueprint of rendered.blueprints.tasks) {
|
|
@@ -323,7 +384,7 @@ export function materializeWorkflowDefinition(
|
|
|
323
384
|
artifacts.link({ from: id, relation: "part_of", to: parentId });
|
|
324
385
|
}
|
|
325
386
|
}
|
|
326
|
-
for (const call of rendered.blueprints.skills as
|
|
387
|
+
for (const call of rendered.blueprints.skills as CallBlueprint[]) {
|
|
327
388
|
const stepTaskIdsForCall = stepTaskIds.get(call.ref) ?? [];
|
|
328
389
|
for (const dependency of call.dependsOn ?? []) {
|
|
329
390
|
for (const dependencyId of stepTaskIds.get(dependency) ?? []) {
|
|
@@ -346,17 +407,26 @@ export function materializeWorkflowDefinition(
|
|
|
346
407
|
|
|
347
408
|
const rootTaskIds = [
|
|
348
409
|
...rendered.blueprints.tasks.filter((task) => (task.dependsOn?.length ?? 0) === 0).map((task) => ids.get(task.ref)!),
|
|
349
|
-
...(rendered.blueprints.skills as
|
|
410
|
+
...(rendered.blueprints.skills as CallBlueprint[])
|
|
350
411
|
.filter((call) => (call.dependsOn?.length ?? 0) === 0)
|
|
351
412
|
.flatMap((call) => stepRootTaskIds.get(call.ref) ?? []),
|
|
352
413
|
];
|
|
353
414
|
for (const task of rendered.blueprints.tasks) {
|
|
354
415
|
if ((task.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: ids.get(task.ref)! });
|
|
355
416
|
}
|
|
356
|
-
for (const call of rendered.blueprints.skills as
|
|
357
|
-
if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: call.
|
|
417
|
+
for (const call of rendered.blueprints.skills as CallBlueprint[]) {
|
|
418
|
+
if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: call.targetId });
|
|
358
419
|
}
|
|
359
420
|
|
|
421
|
+
// A focusRef naming an ordinary task ref resolves directly through ids; one naming a call
|
|
422
|
+
// ref resolves through the nested run it triggered instead (never through ids, which only
|
|
423
|
+
// maps a call ref to a synthetic placeholder that was never actually created).
|
|
424
|
+
const focusTaskId = input.focusRef === undefined
|
|
425
|
+
? undefined
|
|
426
|
+
: rendered.blueprints.tasks.some((task) => task.ref === input.focusRef)
|
|
427
|
+
? ids.get(input.focusRef)
|
|
428
|
+
: stepEntryTaskIds.get(input.focusRef);
|
|
429
|
+
|
|
360
430
|
return {
|
|
361
431
|
skillId: ownerId,
|
|
362
432
|
runId,
|
|
@@ -368,7 +438,7 @@ export function materializeWorkflowDefinition(
|
|
|
368
438
|
skillRuns: [...nestedRuns.map((run) => run.runId), ...nestedRuns.flatMap((run) => run.created.skillRuns)],
|
|
369
439
|
},
|
|
370
440
|
rootTaskIds,
|
|
371
|
-
...(
|
|
441
|
+
...(focusTaskId !== undefined ? { entryTaskId: focusTaskId } : {}),
|
|
372
442
|
execution: projectTaskExecution(executionGraph(tasks, rendered, ids, extraKey)),
|
|
373
443
|
};
|
|
374
444
|
}
|