@nanobpm/workflow 0.15.0 → 0.15.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.
@@ -239,6 +239,13 @@ export declare function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => voi
239
239
  * { jobType })`. Deduplicated (preserving first-seen order) since several steps
240
240
  * may intentionally share one override token. */
241
241
  export declare function externalJobTypes(flow: DeclarativeFlow): string[];
242
+ /** The job types of a flow's inline-handled `run` steps (anywhere in the tree) —
243
+ * exactly the set the in-process `Worker` hosts (same walk, same derived
244
+ * `<flowId>:<stepName>`; a `run` step has no `jobType` override). Codegen
245
+ * consumes this to treat those steps as already-served: a worker-stub
246
+ * scaffolder must never plan a stub for them (nano-ide#595). Deduplicated,
247
+ * preserving first-seen order. */
248
+ export declare function inlineJobTypes(flow: DeclarativeFlow): string[];
242
249
  /** A renderable BPMN element the emitter has placed. A kind's `emit` handler
243
250
  * adds one (or more) via `api.addNode(...)` (or a typed helper like
244
251
  * `api.addServiceTask`). */
@@ -319,6 +319,26 @@ export function externalJobTypes(flow) {
319
319
  });
320
320
  return types;
321
321
  }
322
+ /** The job types of a flow's inline-handled `run` steps (anywhere in the tree) —
323
+ * exactly the set the in-process `Worker` hosts (same walk, same derived
324
+ * `<flowId>:<stepName>`; a `run` step has no `jobType` override). Codegen
325
+ * consumes this to treat those steps as already-served: a worker-stub
326
+ * scaffolder must never plan a stub for them (nano-ide#595). Deduplicated,
327
+ * preserving first-seen order. */
328
+ export function inlineJobTypes(flow) {
329
+ const seen = new Set();
330
+ const types = [];
331
+ walkNodes(flow.steps, (n) => {
332
+ if (n.kind !== "run")
333
+ return;
334
+ const type = jobType(flow.id, n.name);
335
+ if (seen.has(type))
336
+ return;
337
+ seen.add(type);
338
+ types.push(type);
339
+ });
340
+ return types;
341
+ }
322
342
  function requireTimerAt(node) {
323
343
  if (node.at === undefined)
324
344
  throw new Error(`timer event is missing its { at } value`);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
2
2
  export type { Journal, ReplayStep } from "./imperative.js";
3
- export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
3
+ export { defineFlow, declarativeToBpmn, externalJobTypes, inlineJobTypes, walkNodes } from "./declarative.js";
4
4
  export type { FlowBuilder } from "./declarative.js";
5
5
  export type { BoundaryOptions } from "./nodes/boundary.js";
6
6
  export type { HumanOptions, HumanIoMapping, HumanIoEntry } from "./nodes/human.js";
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@
39
39
  // code-first surface (steps are not engine-visible; requires determinism
40
40
  // discipline). Retained for advanced durable-orchestration use only.
41
41
  export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
42
- export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
42
+ export { defineFlow, declarativeToBpmn, externalJobTypes, inlineJobTypes, walkNodes } from "./declarative.js";
43
43
  export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
44
44
  export { envelope } from "./envelope.js";
45
45
  export { WorkflowClient, WorkflowError, toBpmn, toDeployableBpmn } from "./client.js";
@@ -4,6 +4,7 @@
4
4
  // wrapped in an embedded MI sub-process. Contributed through the extension seam.
5
5
  import { assertIdent, escapeXml } from "../xml.js";
6
6
  import { registerNodeKind } from "./registry.js";
7
+ import { hasTaskExtensions } from "./task.js";
7
8
  /** The `<bpmn:multiInstanceLoopCharacteristics>` block for a `forEach` node — the
8
9
  * same shape whether lifted onto a leaf service task or a sub-process. */
9
10
  function miCharacteristics(node) {
@@ -82,13 +83,14 @@ registerNodeKind("forEach", {
82
83
  const only = node.body.length === 1 ? node.body[0] : undefined;
83
84
  // A single service-task body carries the MI characteristics directly; a
84
85
  // multi-step body is wrapped in an embedded MI sub-process (its own token
85
- // scope, with its own start/end). A prompt-carrying `task` is deliberately
86
+ // scope, with its own start/end). A `task` with an extension delta (agent
87
+ // marker, prompt binding or io mapping — see `hasTaskExtensions`) is deliberately
86
88
  // EXCLUDED from this fast path: computing its `zeebe:linkedResources`/
87
89
  // `appendPrompt` delta lives in the `task` kind's own emitter, so lifting it
88
90
  // here would silently drop that binding. It falls through to the sub-process
89
91
  // path, where the `task` emitter renders the agent service task (prompt
90
92
  // intact) inside the MI scope.
91
- if (only && (only.kind === "run" || (only.kind === "task" && only.prompt === undefined))) {
93
+ if (only && (only.kind === "run" || (only.kind === "task" && !hasTaskExtensions(only)))) {
92
94
  api.addServiceTask(only, { mi });
93
95
  api.connect(incoming, only.name);
94
96
  return [api.newEdge(only.name)];
@@ -27,6 +27,11 @@ declare module "../types.js" {
27
27
  jobType?: string;
28
28
  /** The agent prompt binding, when the task is an LLM-agent service task. */
29
29
  prompt?: PromptBinding;
30
+ /** Whether the task is an AGENT service task, emitting the engine-native
31
+ * AgentTask marker `<zeebe:agentDefinition agentType="external" />`
32
+ * (issue #597). Resolved at authoring time: an explicit `agent` option
33
+ * wins, otherwise it is `true` exactly when a `prompt` is bound. */
34
+ agent?: boolean;
30
35
  /** An explicit `<zeebe:ioMapping>` on the service task — arbitrary input
31
36
  * (and output) variable mappings, using the shared ioMapping shape. When a
32
37
  * `prompt.append` is ALSO present it is folded into this single mapping as
@@ -49,7 +54,29 @@ declare module "../declarative.js" {
49
54
  task<K extends string>(name: K, opts: {
50
55
  jobType?: string;
51
56
  prompt: PromptBinding;
57
+ agent?: boolean;
58
+ io?: HumanIoMapping;
59
+ }): FlowBuilder<C>;
60
+ /**
61
+ * A PROMPT-LESS agent external service task: as `task`, but `agent: true`
62
+ * emits the AgentTask marker `<zeebe:agentDefinition agentType="external" />`
63
+ * (the nano-workforce agent-task signal, issue #597) without binding a
64
+ * prompt resource — the worker is steered by its inputs alone.
65
+ */
66
+ task<K extends string>(name: K, opts: {
67
+ jobType?: string;
68
+ agent: boolean;
52
69
  io?: HumanIoMapping;
53
70
  }): FlowBuilder<C>;
54
71
  }
55
72
  }
73
+ /** Whether a `task` node carries an extension delta (agent marker, prompt
74
+ * binding, or io mapping) that only this module's emitter renders. The SINGLE
75
+ * predicate any other emitter (e.g. `forEach`'s single-task fast path) must
76
+ * consult before rendering the task shell itself, so a new delta can never be
77
+ * silently dropped by a bypass. */
78
+ export declare function hasTaskExtensions(node: {
79
+ agent?: boolean;
80
+ prompt?: PromptBinding;
81
+ io?: HumanIoMapping;
82
+ }): boolean;
@@ -57,7 +57,10 @@ function resolvePrompt(name, prompt) {
57
57
  * `<zeebe:ioMapping>` (never two). Element order matches the golden:
58
58
  * `linkedResources` (when any) then `ioMapping` (when any), the latter after
59
59
  * `taskDefinition`/`properties`. */
60
- function taskExtensions(prompt, io) {
60
+ function taskExtensions(agent, prompt, io) {
61
+ // The AgentTask marker leads the delta so it sits directly after the
62
+ // taskDefinition, matching the nano-workforce reference shape.
63
+ const marker = agent === true ? ` <zeebe:agentDefinition agentType="external" />\n` : "";
61
64
  const linked = prompt !== undefined
62
65
  ? ` <zeebe:linkedResources>\n` +
63
66
  ` <zeebe:linkedResource resourceId="${escapeXml(prompt.resourceId)}" ` +
@@ -73,7 +76,15 @@ function taskExtensions(prompt, io) {
73
76
  if (prompt?.append !== undefined)
74
77
  inputs.push({ source: prompt.append, target: "appendPrompt" });
75
78
  const merged = { input: inputs, output: io?.output ?? [] };
76
- return linked + renderIoMapping(merged);
79
+ return marker + linked + renderIoMapping(merged);
80
+ }
81
+ /** Whether a `task` node carries an extension delta (agent marker, prompt
82
+ * binding, or io mapping) that only this module's emitter renders. The SINGLE
83
+ * predicate any other emitter (e.g. `forEach`'s single-task fast path) must
84
+ * consult before rendering the task shell itself, so a new delta can never be
85
+ * silently dropped by a bypass. */
86
+ export function hasTaskExtensions(node) {
87
+ return node.agent === true || node.prompt !== undefined || node.io !== undefined;
77
88
  }
78
89
  registerNodeKind("task", {
79
90
  build: (api) => (name, opts) => {
@@ -83,12 +94,17 @@ registerNodeKind("task", {
83
94
  assertJobType("task jobType", override);
84
95
  const prompt = opts?.prompt !== undefined ? resolvePrompt(name, opts.prompt) : undefined;
85
96
  assertIoMapping(`task("${name}")`, opts?.io);
97
+ if (opts?.agent !== undefined && typeof opts.agent !== "boolean") {
98
+ throw new Error(`task("${name}") agent must be a boolean when provided`);
99
+ }
100
+ const agent = opts?.agent ?? prompt !== undefined;
86
101
  api.out.push({
87
102
  kind: "task",
88
103
  name,
89
104
  envelopes: api.contractEnvelopes(name),
90
105
  jobType: override,
91
106
  prompt,
107
+ agent,
92
108
  io: opts?.io,
93
109
  });
94
110
  return api.self();
@@ -97,7 +113,7 @@ registerNodeKind("task", {
97
113
  // Neither a prompt binding nor an explicit io mapping → the standard
98
114
  // service-task emission is preserved unchanged (no `linkedResources`, no
99
115
  // `ioMapping`).
100
- if (node.prompt === undefined && node.io === undefined) {
116
+ if (!hasTaskExtensions(node)) {
101
117
  api.addServiceTask(node);
102
118
  api.connect(incoming, node.name);
103
119
  return [api.newEdge(node.name)];
@@ -107,7 +123,7 @@ registerNodeKind("task", {
107
123
  // properties, recording envelopes); we only supply the `linkedResource`
108
124
  // and/or `ioMapping` delta, so the envelope/taskDefinition serialization has
109
125
  // a single source.
110
- api.addServiceTask(node, { extraExt: taskExtensions(node.prompt, node.io) });
126
+ api.addServiceTask(node, { extraExt: taskExtensions(node.agent, node.prompt, node.io) });
111
127
  api.connect(incoming, node.name);
112
128
  return [api.newEdge(node.name)];
113
129
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Code-first durable orchestration for nanobpmn (ADR 0044/0045). Author a durable workflow as a declarative flow — `defineFlow(id, w => { w.run(...); w.signal(...); w.task(...) })` — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. `w.run` is a local step, `w.signal` is a durable human-in-the-loop wait, `w.task` is a step serviced by an external worker. `defineFlow` is the single code-first surface; the imperative `defineWorkflow` replay function is experimental/internal, not a recommended API. No diagram, no task-type wiring, no correlation plumbing authored by hand — and you can optionally auto-generate a diagram (DI) for the derived model with `declarativeToLayoutedBpmn` via the optional `bpmn-auto-layout` peer dependency. Talks to a running nanobpmn gateway through @nanobpm/nano-sdk (the shared engine-transport spine — ADR 0055), which negotiates the Falcon protocol on a Nano server and REST elsewhere.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -562,6 +562,25 @@ export function externalJobTypes(flow: DeclarativeFlow): string[] {
562
562
  return types;
563
563
  }
564
564
 
565
+ /** The job types of a flow's inline-handled `run` steps (anywhere in the tree) —
566
+ * exactly the set the in-process `Worker` hosts (same walk, same derived
567
+ * `<flowId>:<stepName>`; a `run` step has no `jobType` override). Codegen
568
+ * consumes this to treat those steps as already-served: a worker-stub
569
+ * scaffolder must never plan a stub for them (nano-ide#595). Deduplicated,
570
+ * preserving first-seen order. */
571
+ export function inlineJobTypes(flow: DeclarativeFlow): string[] {
572
+ const seen = new Set<string>();
573
+ const types: string[] = [];
574
+ walkNodes(flow.steps, (n) => {
575
+ if (n.kind !== "run") return;
576
+ const type = jobType(flow.id, n.name);
577
+ if (seen.has(type)) return;
578
+ seen.add(type);
579
+ types.push(type);
580
+ });
581
+ return types;
582
+ }
583
+
565
584
  function requireTimerAt(node: { at?: string }): string {
566
585
  if (node.at === undefined) throw new Error(`timer event is missing its { at } value`);
567
586
  return node.at;
package/src/index.ts CHANGED
@@ -41,7 +41,7 @@
41
41
  // discipline). Retained for advanced durable-orchestration use only.
42
42
  export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
43
43
  export type { Journal, ReplayStep } from "./imperative.js";
44
- export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
44
+ export { defineFlow, declarativeToBpmn, externalJobTypes, inlineJobTypes, walkNodes } from "./declarative.js";
45
45
  export type { FlowBuilder } from "./declarative.js";
46
46
  // The `.boundary(...)` option type (its builder method augments FlowBuilder from
47
47
  // the boundary kind module — S2/#317).
@@ -7,6 +7,7 @@ import type { FlowNode } from "../types.js";
7
7
  import type { Block } from "../declarative.js";
8
8
  import { assertIdent, escapeXml } from "../xml.js";
9
9
  import { registerNodeKind } from "./registry.js";
10
+ import { hasTaskExtensions } from "./task.js";
10
11
 
11
12
  /** The `forEach` node shape (its `FlowNode` variant). */
12
13
  interface ForEachNode {
@@ -115,13 +116,14 @@ registerNodeKind("forEach", {
115
116
  const only = node.body.length === 1 ? node.body[0] : undefined;
116
117
  // A single service-task body carries the MI characteristics directly; a
117
118
  // multi-step body is wrapped in an embedded MI sub-process (its own token
118
- // scope, with its own start/end). A prompt-carrying `task` is deliberately
119
+ // scope, with its own start/end). A `task` with an extension delta (agent
120
+ // marker, prompt binding or io mapping — see `hasTaskExtensions`) is deliberately
119
121
  // EXCLUDED from this fast path: computing its `zeebe:linkedResources`/
120
122
  // `appendPrompt` delta lives in the `task` kind's own emitter, so lifting it
121
123
  // here would silently drop that binding. It falls through to the sub-process
122
124
  // path, where the `task` emitter renders the agent service task (prompt
123
125
  // intact) inside the MI scope.
124
- if (only && (only.kind === "run" || (only.kind === "task" && only.prompt === undefined))) {
126
+ if (only && (only.kind === "run" || (only.kind === "task" && !hasTaskExtensions(only)))) {
125
127
  api.addServiceTask(only, { mi });
126
128
  api.connect(incoming, only.name);
127
129
  return [api.newEdge(only.name)];
package/src/nodes/task.ts CHANGED
@@ -46,6 +46,11 @@ declare module "../types.js" {
46
46
  jobType?: string;
47
47
  /** The agent prompt binding, when the task is an LLM-agent service task. */
48
48
  prompt?: PromptBinding;
49
+ /** Whether the task is an AGENT service task, emitting the engine-native
50
+ * AgentTask marker `<zeebe:agentDefinition agentType="external" />`
51
+ * (issue #597). Resolved at authoring time: an explicit `agent` option
52
+ * wins, otherwise it is `true` exactly when a `prompt` is bound. */
53
+ agent?: boolean;
49
54
  /** An explicit `<zeebe:ioMapping>` on the service task — arbitrary input
50
55
  * (and output) variable mappings, using the shared ioMapping shape. When a
51
56
  * `prompt.append` is ALSO present it is folded into this single mapping as
@@ -68,7 +73,17 @@ declare module "../declarative.js" {
68
73
  */
69
74
  task<K extends string>(
70
75
  name: K,
71
- opts: { jobType?: string; prompt: PromptBinding; io?: HumanIoMapping },
76
+ opts: { jobType?: string; prompt: PromptBinding; agent?: boolean; io?: HumanIoMapping },
77
+ ): FlowBuilder<C>;
78
+ /**
79
+ * A PROMPT-LESS agent external service task: as `task`, but `agent: true`
80
+ * emits the AgentTask marker `<zeebe:agentDefinition agentType="external" />`
81
+ * (the nano-workforce agent-task signal, issue #597) without binding a
82
+ * prompt resource — the worker is steered by its inputs alone.
83
+ */
84
+ task<K extends string>(
85
+ name: K,
86
+ opts: { jobType?: string; agent: boolean; io?: HumanIoMapping },
72
87
  ): FlowBuilder<C>;
73
88
  }
74
89
  }
@@ -118,7 +133,14 @@ function resolvePrompt(name: string, prompt: PromptBinding): PromptBinding {
118
133
  * `<zeebe:ioMapping>` (never two). Element order matches the golden:
119
134
  * `linkedResources` (when any) then `ioMapping` (when any), the latter after
120
135
  * `taskDefinition`/`properties`. */
121
- function taskExtensions(prompt: PromptBinding | undefined, io: HumanIoMapping | undefined): string {
136
+ function taskExtensions(
137
+ agent: boolean | undefined,
138
+ prompt: PromptBinding | undefined,
139
+ io: HumanIoMapping | undefined,
140
+ ): string {
141
+ // The AgentTask marker leads the delta so it sits directly after the
142
+ // taskDefinition, matching the nano-workforce reference shape.
143
+ const marker = agent === true ? ` <zeebe:agentDefinition agentType="external" />\n` : "";
122
144
  const linked =
123
145
  prompt !== undefined
124
146
  ? ` <zeebe:linkedResources>\n` +
@@ -134,22 +156,36 @@ function taskExtensions(prompt: PromptBinding | undefined, io: HumanIoMapping |
134
156
  const inputs: HumanIoEntry[] = [...(io?.input ?? [])];
135
157
  if (prompt?.append !== undefined) inputs.push({ source: prompt.append, target: "appendPrompt" });
136
158
  const merged: HumanIoMapping = { input: inputs, output: io?.output ?? [] };
137
- return linked + renderIoMapping(merged);
159
+ return marker + linked + renderIoMapping(merged);
160
+ }
161
+
162
+ /** Whether a `task` node carries an extension delta (agent marker, prompt
163
+ * binding, or io mapping) that only this module's emitter renders. The SINGLE
164
+ * predicate any other emitter (e.g. `forEach`'s single-task fast path) must
165
+ * consult before rendering the task shell itself, so a new delta can never be
166
+ * silently dropped by a bypass. */
167
+ export function hasTaskExtensions(node: { agent?: boolean; prompt?: PromptBinding; io?: HumanIoMapping }): boolean {
168
+ return node.agent === true || node.prompt !== undefined || node.io !== undefined;
138
169
  }
139
170
 
140
171
  registerNodeKind("task", {
141
- build: (api) => (name: string, opts?: { jobType?: string; prompt?: PromptBinding; io?: HumanIoMapping }) => {
172
+ build: (api) => (name: string, opts?: { jobType?: string; prompt?: PromptBinding; agent?: boolean; io?: HumanIoMapping }) => {
142
173
  api.claim(name);
143
174
  const override = opts?.jobType;
144
175
  if (override !== undefined) assertJobType("task jobType", override);
145
176
  const prompt = opts?.prompt !== undefined ? resolvePrompt(name, opts.prompt) : undefined;
146
177
  assertIoMapping(`task("${name}")`, opts?.io);
178
+ if (opts?.agent !== undefined && typeof opts.agent !== "boolean") {
179
+ throw new Error(`task("${name}") agent must be a boolean when provided`);
180
+ }
181
+ const agent = opts?.agent ?? prompt !== undefined;
147
182
  api.out.push({
148
183
  kind: "task",
149
184
  name,
150
185
  envelopes: api.contractEnvelopes(name),
151
186
  jobType: override,
152
187
  prompt,
188
+ agent,
153
189
  io: opts?.io,
154
190
  });
155
191
  return api.self();
@@ -158,7 +194,7 @@ registerNodeKind("task", {
158
194
  // Neither a prompt binding nor an explicit io mapping → the standard
159
195
  // service-task emission is preserved unchanged (no `linkedResources`, no
160
196
  // `ioMapping`).
161
- if (node.prompt === undefined && node.io === undefined) {
197
+ if (!hasTaskExtensions(node)) {
162
198
  api.addServiceTask(node);
163
199
  api.connect(incoming, node.name);
164
200
  return [api.newEdge(node.name)];
@@ -168,7 +204,7 @@ registerNodeKind("task", {
168
204
  // properties, recording envelopes); we only supply the `linkedResource`
169
205
  // and/or `ioMapping` delta, so the envelope/taskDefinition serialization has
170
206
  // a single source.
171
- api.addServiceTask(node, { extraExt: taskExtensions(node.prompt, node.io) });
207
+ api.addServiceTask(node, { extraExt: taskExtensions(node.agent, node.prompt, node.io) });
172
208
  api.connect(incoming, node.name);
173
209
  return [api.newEdge(node.name)];
174
210
  },