@nanobpm/nano-workforce 0.140.0 → 0.142.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/CHANGELOG.md +12 -0
- package/app/agentic/channel.ts +15 -1
- package/app/agentic/correlation-store.test.ts +105 -12
- package/app/agentic/correlation-store.ts +64 -10
- package/app/agentic/correlation.test.ts +46 -0
- package/app/agentic/correlation.ts +41 -4
- package/app/agentic/element-instance.test.ts +111 -0
- package/app/agentic/element-instance.ts +97 -0
- package/app/agentic/families/relay.family.test.ts +90 -0
- package/app/agentic/families/relay.family.ts +84 -0
- package/app/agentic/registry.ts +17 -3
- package/app/agentic/transcript-read.test.ts +27 -0
- package/app/agentic/transcript-read.ts +11 -0
- package/app/convergeTargets.ts +30 -0
- package/app/deliveryConnector.ts +31 -20
- package/app/deliveryGraph.test.ts +111 -0
- package/app/deliveryGraph.ts +159 -5
- package/app/deliveryGraphCompiler.test.ts +51 -1
- package/app/deliveryGraphCompiler.ts +21 -3
- package/db/migrations/086_agentic_correlation_element_instance.sql +21 -0
- package/docs/adr/0005-agent-authored-delivery-graphs.md +5 -3
- package/docs/agent-guide.md +23 -10
- package/e2e/delivery-graph.e2e.ts +30 -0
- package/main.ts +6 -0
- package/openapi.yaml +21 -2
- package/operations/listAgenticTranscripts.ts +1 -0
- package/package.json +2 -2
- package/workers/delivery-connector/worker.test.ts +26 -8
- package/workers/delivery-connector/worker.ts +15 -7
package/openapi.yaml
CHANGED
|
@@ -643,6 +643,11 @@ components:
|
|
|
643
643
|
elementId:
|
|
644
644
|
type: string
|
|
645
645
|
description: The BPMN element id (activity/task) the job was for, when still known (advisory).
|
|
646
|
+
elementInstanceKey:
|
|
647
|
+
type: string
|
|
648
|
+
description: The engine element-instance key the job's token occupied (#544) — the per-occupancy
|
|
649
|
+
handle, unambiguous across a looping / retried activity where many instances share one elementId.
|
|
650
|
+
Resolved from the job's element-instance wait-state; advisory, present when resolution landed.
|
|
646
651
|
planKey:
|
|
647
652
|
type: string
|
|
648
653
|
description: The plan / epic key this job was part of (e.g. owner/repo#142), when still known (advisory).
|
|
@@ -749,6 +754,10 @@ components:
|
|
|
749
754
|
elementId:
|
|
750
755
|
type: string
|
|
751
756
|
description: The BPMN element id, when still known (advisory).
|
|
757
|
+
elementInstanceKey:
|
|
758
|
+
type: string
|
|
759
|
+
description: The engine element-instance key the job's token occupied (#544) — per-occupancy,
|
|
760
|
+
unambiguous across a looping / retried activity; advisory, present when resolution landed.
|
|
752
761
|
planKey:
|
|
753
762
|
type: string
|
|
754
763
|
description: The plan / epic key, when still known (advisory).
|
|
@@ -1352,10 +1361,13 @@ components:
|
|
|
1352
1361
|
description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
|
|
1353
1362
|
type:
|
|
1354
1363
|
type: string
|
|
1355
|
-
enum: [string, number, boolean, artifact, version, url]
|
|
1364
|
+
enum: [string, number, boolean, artifact, version, url, pr]
|
|
1356
1365
|
description: >-
|
|
1357
1366
|
The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version
|
|
1358
|
-
string, `url` a location — mirrors the values `capability`/`pr` probes late-bind.
|
|
1367
|
+
string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr`
|
|
1368
|
+
(issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened,
|
|
1369
|
+
so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the
|
|
1370
|
+
fact instead of a hardcoded literal.
|
|
1359
1371
|
description:
|
|
1360
1372
|
type: string
|
|
1361
1373
|
maxLength: 512
|
|
@@ -2802,6 +2814,13 @@ paths:
|
|
|
2802
2814
|
schema:
|
|
2803
2815
|
type: string
|
|
2804
2816
|
description: Return only transcripts whose (still-known) correlation names this process instance.
|
|
2817
|
+
- name: elementInstanceKey
|
|
2818
|
+
in: query
|
|
2819
|
+
required: false
|
|
2820
|
+
schema:
|
|
2821
|
+
type: string
|
|
2822
|
+
description: Return only transcripts whose correlation names this engine element-instance key (#544) —
|
|
2823
|
+
resolves a session to one occupancy of a looping / retried activity, unlike the static elementId.
|
|
2805
2824
|
- name: planKey
|
|
2806
2825
|
in: query
|
|
2807
2826
|
required: false
|
|
@@ -46,6 +46,7 @@ export default defineOperation("listAgenticTranscripts", async ({ query, req },
|
|
|
46
46
|
const filter: TranscriptFilter = {
|
|
47
47
|
...(query.jobKey !== undefined ? { jobKey: query.jobKey } : {}),
|
|
48
48
|
...(query.processInstanceKey !== undefined ? { processInstanceKey: query.processInstanceKey } : {}),
|
|
49
|
+
...(query.elementInstanceKey !== undefined ? { elementInstanceKey: query.elementInstanceKey } : {}),
|
|
49
50
|
...(query.planKey !== undefined ? { planKey: query.planKey } : {}),
|
|
50
51
|
...(query.instance !== undefined ? { instance: query.instance } : {}),
|
|
51
52
|
...(query.since !== undefined ? { since: query.since } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.142.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@nanobpm/agentic": "^0.4.0",
|
|
62
|
-
"@nanobpm/urban": "^0.
|
|
62
|
+
"@nanobpm/urban": "^0.83.0",
|
|
63
63
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -123,34 +123,52 @@ test("readConnectorInput: an array payload is rejected (arrays are not plain obj
|
|
|
123
123
|
|
|
124
124
|
test("readConvergeInput: parses pr; convergeOnly defaults from the target; dependsOn is optional", () => {
|
|
125
125
|
// `converge-merge` drives the merge loop → convergeOnly defaults false.
|
|
126
|
-
const merge = readConvergeInput("converge-merge", { pr: "owner/repo#7" });
|
|
126
|
+
const merge = readConvergeInput("converge-merge", { pr: "owner/repo#7" }, null);
|
|
127
127
|
assertEquals(merge.parsed.prKey, "owner/repo#7");
|
|
128
128
|
assertEquals(merge.convergeOnly, false);
|
|
129
129
|
assertEquals(merge.dependsOn, []);
|
|
130
130
|
// `converge` is review-only → convergeOnly defaults true.
|
|
131
|
-
const conv = readConvergeInput("converge", { pr: "owner/repo#7" });
|
|
131
|
+
const conv = readConvergeInput("converge", { pr: "owner/repo#7" }, null);
|
|
132
132
|
assertEquals(conv.convergeOnly, true);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
test("readConvergeInput: an explicit payload.convergeOnly overrides the target default; dependsOn threads through", () => {
|
|
136
|
-
const r = readConvergeInput("converge-merge", { pr: "owner/repo#7", convergeOnly: true, dependsOn: ["owner/repo#5", 42] as unknown as string[] });
|
|
136
|
+
const r = readConvergeInput("converge-merge", { pr: "owner/repo#7", convergeOnly: true, dependsOn: ["owner/repo#5", 42] as unknown as string[] }, null);
|
|
137
137
|
assertEquals(r.convergeOnly, true, "the explicit boolean wins over the target default");
|
|
138
138
|
assertEquals(r.dependsOn, ["owner/repo#5"], "non-string dependsOn entries are dropped");
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test("readConvergeInput: a missing / unparseable pr fails CLOSED (a converge connector with no target PR is meaningless)", () => {
|
|
142
|
-
assertThrows(() => readConvergeInput("converge-merge", null), Error, "payload.pr");
|
|
143
|
-
assertThrows(() => readConvergeInput("converge-merge", {}), Error, "payload.pr");
|
|
144
|
-
assertThrows(() => readConvergeInput("converge", { pr: "not-a-pr" }), Error, "payload.pr");
|
|
142
|
+
assertThrows(() => readConvergeInput("converge-merge", null, null), Error, "payload.pr");
|
|
143
|
+
assertThrows(() => readConvergeInput("converge-merge", {}, null), Error, "payload.pr");
|
|
144
|
+
assertThrows(() => readConvergeInput("converge", { pr: "not-a-pr" }, null), Error, "payload.pr");
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
test("readConvergeInput: an unparseable pr whose value is not JSON-serializable still fails CLOSED with the intended error (not a serializer TypeError)", () => {
|
|
148
148
|
// `p.pr` is user-controlled payload data; a BigInt (or a circular object) makes JSON.stringify
|
|
149
149
|
// throw, which must NOT mask the intended "requires payload.pr" error.
|
|
150
|
-
assertThrows(() => readConvergeInput("converge", { pr: 10n as unknown as string }), Error, "payload.pr");
|
|
150
|
+
assertThrows(() => readConvergeInput("converge", { pr: 10n as unknown as string }, null), Error, "payload.pr");
|
|
151
151
|
const circular: Record<string, unknown> = {};
|
|
152
152
|
circular.self = circular;
|
|
153
|
-
assertThrows(() => readConvergeInput("converge", { pr: circular as unknown as string }), Error, "payload.pr");
|
|
153
|
+
assertThrows(() => readConvergeInput("converge", { pr: circular as unknown as string }, null), Error, "payload.pr");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("readConvergeInput: #548 late-binds the PR from an upstream agent's emitted `pr` fact (no literal needed)", () => {
|
|
157
|
+
const bound = [{ from: "open", name: "pr", value: "owner/repo#42" }];
|
|
158
|
+
// OMITTED payload.pr → binds the single incoming `pr` fact.
|
|
159
|
+
assertEquals(readConvergeInput("converge-merge", null, bound).parsed.prKey, "owner/repo#42");
|
|
160
|
+
assertEquals(readConvergeInput("converge-merge", {}, bound).parsed.prKey, "owner/repo#42");
|
|
161
|
+
// EXPLICIT reference form → resolves that specific threaded fact.
|
|
162
|
+
assertEquals(readConvergeInput("converge", { pr: "open.pr" }, bound).parsed.prKey, "owner/repo#42");
|
|
163
|
+
// A LITERAL still wins as-is even when a bound fact is present (never `<node>.<fact>`-shaped).
|
|
164
|
+
assertEquals(readConvergeInput("converge", { pr: "owner/repo#9" }, bound).parsed.prKey, "owner/repo#9");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("readConvergeInput: #548 a dangling reference (no matching bound fact) fails CLOSED", () => {
|
|
168
|
+
// "open.pr" is reference-shaped but nothing threaded it → falls through to a literal parse, which
|
|
169
|
+
// rejects (it is not an `owner/repo#N`), so a mis-wired graph never silently enrolls a junk PR.
|
|
170
|
+
assertThrows(() => readConvergeInput("converge-merge", { pr: "open.pr" }, []), Error, "payload.pr");
|
|
171
|
+
assertThrows(() => readConvergeInput("converge-merge", { pr: "open.pr" }, [{ from: "other", name: "pr", value: "o/r#1" }]), Error, "payload.pr");
|
|
154
172
|
});
|
|
155
173
|
|
|
156
174
|
test("safeStringify: always returns a string, even for values JSON.stringify serializes to undefined (Symbol/undefined/function)", () => {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
convergeOnlyForTarget,
|
|
16
16
|
dispatchConnector,
|
|
17
17
|
isConvergeTarget,
|
|
18
|
+
resolveConvergePr,
|
|
18
19
|
} from "../../app/deliveryConnector.ts";
|
|
19
20
|
import { isPrSettled, MAX_ROUNDS, type ParsedPr, parsePr, submitPr } from "../../app/service.ts";
|
|
20
21
|
|
|
@@ -81,19 +82,26 @@ export function safeStringify(value: unknown): string {
|
|
|
81
82
|
* `convergeOnly` DEFAULTS from the target (`converge` → review-only `true`; `converge-merge` → drive
|
|
82
83
|
* the merge loop `false`) and may be overridden per-dispatch by an explicit boolean. `dependsOn` is an
|
|
83
84
|
* optional list of PR refs unioned into the enrolled PR's merge-stage dependency set (only non-string
|
|
84
|
-
* entries are dropped; `submitPr` itself ignores unparseable refs). Exported for unit coverage
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* entries are dropped; `submitPr` itself ignores unparseable refs). Exported for unit coverage.
|
|
86
|
+
*
|
|
87
|
+
* `pr` may be sourced three ways (issue #548), resolved by {@link resolveConvergePr} against the
|
|
88
|
+
* threaded `boundFacts` BEFORE parsing: a LITERAL `owner/repo#N`, an explicit fact REFERENCE
|
|
89
|
+
* (`payload.pr: "<upstreamNode>.pr"`), or OMITTED — late-bound from the single incoming `pr` fact an
|
|
90
|
+
* upstream `agent` node emitted for the PR it opened. This is what lets the canonical
|
|
91
|
+
* `agent → connector[converge-merge] → wait` shape carry no hardcoded PR number. */
|
|
87
92
|
export function readConvergeInput(
|
|
88
93
|
target: string,
|
|
89
94
|
payload: Record<string, unknown> | null,
|
|
95
|
+
boundFacts: readonly BoundFact[] | null,
|
|
90
96
|
): { parsed: ParsedPr; convergeOnly: boolean; dependsOn: string[] } {
|
|
91
97
|
const p = payload ?? {};
|
|
92
|
-
const
|
|
98
|
+
const prValue = resolveConvergePr(p.pr, boundFacts ?? []);
|
|
99
|
+
const parsed = parsePr(prValue);
|
|
93
100
|
if (!parsed) {
|
|
94
101
|
throw new Error(
|
|
95
|
-
`delivery-connector: '${target}' target requires
|
|
96
|
-
`
|
|
102
|
+
`delivery-connector: '${target}' target requires a target PR — a literal "owner/repo#N" in ` +
|
|
103
|
+
`payload.pr, an upstream-fact reference (payload.pr: "<node>.pr"), or a threaded \`pr\` fact ` +
|
|
104
|
+
`bound from an upstream \`agent\` node (got ${safeStringify(prValue ?? null)})`,
|
|
97
105
|
);
|
|
98
106
|
}
|
|
99
107
|
const convergeOnly = typeof p.convergeOnly === "boolean" ? p.convergeOnly : convergeOnlyForTarget(target);
|
|
@@ -106,7 +114,7 @@ const handler: AppJobHandler<In, ConnectorDispatchResult> = async (job, app) =>
|
|
|
106
114
|
// A `converge`/`converge-merge` target enrolls a PR into the shared convergence (+ merge) loop.
|
|
107
115
|
// Parse + validate its payload BEFORE claiming a ledger row, so a misconfigured converge node (no
|
|
108
116
|
// parseable `pr`) fails CLOSED without writing a junk dispatch row it could never act on.
|
|
109
|
-
const converge = isConvergeTarget(target) ? readConvergeInput(target, payload) : null;
|
|
117
|
+
const converge = isConvergeTarget(target) ? readConvergeInput(target, payload, boundFacts) : null;
|
|
110
118
|
const dedupeKey = connectorDedupeKey({
|
|
111
119
|
dedupeKey: job.variables.dedupeKey ?? null,
|
|
112
120
|
processInstanceKey: job.processInstanceKey ?? null,
|