@nanobpm/workflow 0.9.0 → 0.11.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/src/nodes/task.ts CHANGED
@@ -2,27 +2,148 @@
2
2
  // OUTSIDE this program (no locally-hosted handler). Its job type defaults to the
3
3
  // derived `${flowId}:${name}` unless overridden with `{ jobType }`. Contributed
4
4
  // from this module through the extension seam (epic #314, S0/#315).
5
+ //
6
+ // This module also owns the AGENT PROMPT BINDING (epic #314, S4/#319): an
7
+ // optional `{ prompt }` option binds an LLM prompt resource to the worker via a
8
+ // `<zeebe:linkedResource … resourceType="GenericScript" linkName="prompt">`,
9
+ // mirroring the nano-workforce agent service tasks. The extension is contributed
10
+ // entirely from this module — the `task` node's own registered emit handler and
11
+ // its option type — WITHOUT re-opening the shared `FlowNode` union, the central
12
+ // `walkNodes`/`emitNode` dispatch, or the generated barrel.
5
13
 
6
14
  import type { NodeEnvelopes } from "../types.js";
7
- import { assertJobType } from "../xml.js";
15
+ import { assertJobType, escapeXml } from "../xml.js";
8
16
  import { registerNodeKind } from "./registry.js";
9
17
 
18
+ /** Binds an LLM prompt resource to an agent service task. Emits, alongside the
19
+ * task's `zeebe:taskDefinition` capability token, a
20
+ * `<zeebe:linkedResources><zeebe:linkedResource resourceId=… bindingType=…
21
+ * resourceType="GenericScript" linkName="prompt"/></zeebe:linkedResources>`
22
+ * extension — the shape the nano-workforce agent tasks use to attach a prompt
23
+ * script to the worker that services the job. */
24
+ export interface PromptBinding {
25
+ /** The `GenericScript` resource id bound as the prompt (the linkedResource
26
+ * `resourceId`, e.g. `"retro.md"`). Required and non-empty. */
27
+ resourceId: string;
28
+ /** The linkedResource `bindingType` — how the engine resolves the resource
29
+ * version at deploy time. Defaults to `"latest"`. */
30
+ bindingType?: string;
31
+ /** Optional prompt addendum: a FEEL expression (or literal) fed to the worker
32
+ * through a `<zeebe:ioMapping><zeebe:input source=… target="appendPrompt"/>`
33
+ * input, the convention the agent tasks use to append runtime context to the
34
+ * bound prompt. Omitted → no `ioMapping` is emitted. */
35
+ append?: string;
36
+ }
37
+
10
38
  declare module "../types.js" {
11
39
  interface FlowNodeRegistry {
12
- task: { kind: "task"; name: string; envelopes?: NodeEnvelopes; jobType?: string };
40
+ task: {
41
+ kind: "task";
42
+ name: string;
43
+ envelopes?: NodeEnvelopes;
44
+ jobType?: string;
45
+ /** The agent prompt binding, when the task is an LLM-agent service task. */
46
+ prompt?: PromptBinding;
47
+ };
13
48
  }
14
49
  }
15
50
 
51
+ declare module "../declarative.js" {
52
+ interface FlowBuilder<C extends object> {
53
+ /**
54
+ * An AGENT external service task: as `task`, but additionally binds an LLM
55
+ * `prompt` resource to the worker via a `zeebe:linkedResource`
56
+ * (`resourceType="GenericScript" linkName="prompt"`). Pair it with a
57
+ * `{ jobType }` capability token (e.g. `senior:retro`) so an agent pool
58
+ * services it. `prompt.append` (optional) feeds a FEEL expression to the
59
+ * worker through a `zeebe:ioMapping` `appendPrompt` input.
60
+ */
61
+ task<K extends string>(name: K, opts: { jobType?: string; prompt: PromptBinding }): FlowBuilder<C>;
62
+ }
63
+ }
64
+
65
+ /** Normalize + validate a prompt binding at authoring time, applying the
66
+ * `bindingType` default. Emitted string fields are trimmed before storing (in
67
+ * parity with how `timer` normalizes `{ after }`/`{ at }`), so a value like
68
+ * `resourceId: " retro.md "` passes validation AND is emitted without stray
69
+ * surrounding whitespace. Returns a fully-resolved binding stored on the node. */
70
+ function resolvePrompt(name: string, prompt: PromptBinding): PromptBinding {
71
+ if (typeof prompt !== "object" || prompt === null) {
72
+ throw new Error(`task("${name}") prompt must be an object with a non-empty resourceId`);
73
+ }
74
+ const resourceId = prompt.resourceId;
75
+ if (typeof resourceId !== "string" || resourceId.trim() === "") {
76
+ throw new Error(`task("${name}") prompt.resourceId must be a non-empty string`);
77
+ }
78
+ // Only an ABSENT bindingType defaults to "latest"; `??` would also swallow an
79
+ // explicit `null`, masking an authoring mistake. Distinguish `undefined` (use
80
+ // the default) from any other non-string (reject), in parity with how `append`
81
+ // below treats `!== undefined` as "provided, so validate strictly".
82
+ const bindingType = prompt.bindingType === undefined ? "latest" : prompt.bindingType;
83
+ if (typeof bindingType !== "string" || bindingType.trim() === "") {
84
+ throw new Error(`task("${name}") prompt.bindingType must be a non-empty string`);
85
+ }
86
+ const resolved: PromptBinding = { resourceId: resourceId.trim(), bindingType: bindingType.trim() };
87
+ if (prompt.append !== undefined) {
88
+ if (typeof prompt.append !== "string" || prompt.append.trim() === "") {
89
+ throw new Error(`task("${name}") prompt.append must be a non-empty string when provided`);
90
+ }
91
+ resolved.append = prompt.append.trim();
92
+ }
93
+ return resolved;
94
+ }
95
+
96
+ /** Build the prompt-specific `<bpmn:extensionElements>` delta appended to the
97
+ * shared service-task emission: the `zeebe:linkedResources` prompt binding and
98
+ * (optionally) a `zeebe:ioMapping` `appendPrompt` input. The task shell
99
+ * (taskDefinition + envelope properties) is rendered by the built-in
100
+ * `EmitApi.addServiceTask`, so this variant only contributes its delta and the
101
+ * envelope serialization can never drift from the plain service task. */
102
+ function promptExtensions(prompt: PromptBinding): string {
103
+ const { resourceId, bindingType, append } = prompt;
104
+ const linked =
105
+ ` <zeebe:linkedResources>\n` +
106
+ ` <zeebe:linkedResource resourceId="${escapeXml(resourceId)}" ` +
107
+ `bindingType="${escapeXml(bindingType ?? "latest")}" resourceType="GenericScript" linkName="prompt" />\n` +
108
+ ` </zeebe:linkedResources>\n`;
109
+ const io =
110
+ append !== undefined
111
+ ? ` <zeebe:ioMapping>\n` +
112
+ ` <zeebe:input source="${escapeXml(append)}" target="appendPrompt" />\n` +
113
+ ` </zeebe:ioMapping>\n`
114
+ : "";
115
+ return linked + io;
116
+ }
117
+
16
118
  registerNodeKind("task", {
17
- build: (api) => (name: string, opts?: { jobType?: string }) => {
119
+ build: (api) => (name: string, opts?: { jobType?: string; prompt?: PromptBinding }) => {
18
120
  api.claim(name);
19
121
  const override = opts?.jobType;
20
122
  if (override !== undefined) assertJobType("task jobType", override);
21
- api.out.push({ kind: "task", name, envelopes: api.contractEnvelopes(name), jobType: override });
123
+ const prompt = opts?.prompt !== undefined ? resolvePrompt(name, opts.prompt) : undefined;
124
+ api.out.push({
125
+ kind: "task",
126
+ name,
127
+ envelopes: api.contractEnvelopes(name),
128
+ jobType: override,
129
+ prompt,
130
+ });
22
131
  return api.self();
23
132
  },
24
133
  emit: (node, incoming, _loop, api) => {
25
- api.addServiceTask(node);
134
+ // No prompt → the standard service-task emission is preserved unchanged (no
135
+ // `linkedResources` are emitted).
136
+ if (node.prompt === undefined) {
137
+ api.addServiceTask(node);
138
+ api.connect(incoming, node.name);
139
+ return [api.newEdge(node.name)];
140
+ }
141
+ // Prompt binding present → the shared `addServiceTask` renders the task
142
+ // shell (taskDefinition + envelope properties, recording envelopes); we only
143
+ // supply the `zeebe:linkedResource` (and optional `appendPrompt` ioMapping)
144
+ // delta, so the envelope/taskDefinition serialization has a single source.
145
+ const prompt = node.prompt;
146
+ api.addServiceTask(node, { extraExt: promptExtensions(prompt) });
26
147
  api.connect(incoming, node.name);
27
148
  return [api.newEdge(node.name)];
28
149
  },