@nanobpm/bojtos-kit 0.2.0 → 0.3.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/dist/index.d.ts +2 -2
- package/dist/session.d.ts +11 -1
- package/dist/session.js +4 -0
- package/dist/types.d.ts +27 -0
- package/dist/worker.d.ts +29 -1
- package/dist/worker.js +57 -4
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/session.ts +28 -1
- package/src/types.ts +29 -0
- package/src/worker.ts +89 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { ensureWasm, createBojtosSession, type BojtosSession, type WasmSource, } from "./session.js";
|
|
2
|
-
export { dispatchWorkers, dispatchRound, JobFailure, type JobHandler, type JobResult, type DispatchOptions, type DispatchResult, type RoundResult, } from "./worker.js";
|
|
3
|
-
export type { Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, ActiveEl, WasmEvent, } from "./types.js";
|
|
2
|
+
export { dispatchWorkers, dispatchRound, JobFailure, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, } from "./worker.js";
|
|
3
|
+
export type { Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, ActiveEl, AgentActivation, AgentResult, WasmEvent, } from "./types.js";
|
package/dist/session.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type InitInput } from "@nanobpm/engine-wasm";
|
|
2
|
-
import type { ActivatedJob, ActivateInstruction, Snapshot, WasmEvent } from "./types.js";
|
|
2
|
+
import type { ActivatedJob, ActivateInstruction, AgentResult, Snapshot, WasmEvent } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* The source of the engine wasm binary. Under a bundler that understands
|
|
5
5
|
* `new URL(..., import.meta.url)` (e.g. Vite) the default loader needs no
|
|
@@ -46,6 +46,16 @@ export interface BojtosSession {
|
|
|
46
46
|
activateJobs(jobType: string, maxJobs: number, timeoutMs: number, worker: string): ActivatedJob[];
|
|
47
47
|
/** Complete a waiting job, merging `variablesJson` into the instance. */
|
|
48
48
|
completeJob(jobKey: string, variablesJson: string): Snapshot;
|
|
49
|
+
/**
|
|
50
|
+
* Complete an ad-hoc sub-process's **agent** job — the container's JOB_WORKER
|
|
51
|
+
* job (Camunda's agentic `aiagent-job-worker`) — carrying the agent's
|
|
52
|
+
* {@link AgentResult}. Its `activateElements` run the chosen inner tools this
|
|
53
|
+
* turn; `completionConditionFulfilled` ends the agent loop; `variables` merge
|
|
54
|
+
* into the instance (e.g. the agent's final decision). This is the ad-hoc seam
|
|
55
|
+
* plain {@link completeJob} deliberately omits. Register agents on the dispatch
|
|
56
|
+
* loop via `DispatchOptions.agents` rather than calling this directly.
|
|
57
|
+
*/
|
|
58
|
+
completeAgentJob(jobKey: string, result: AgentResult): Snapshot;
|
|
49
59
|
/** Fail a waiting job; with no retries left this raises an incident. */
|
|
50
60
|
failJob(jobKey: string, retries: number, message: string): Snapshot;
|
|
51
61
|
/**
|
package/dist/session.js
CHANGED
|
@@ -45,6 +45,10 @@ class WasmBojtosSession {
|
|
|
45
45
|
completeJob(jobKey, variablesJson) {
|
|
46
46
|
return parseSnapshot(this.engine.completeJob(jobKey, variablesJson || "{}"));
|
|
47
47
|
}
|
|
48
|
+
completeAgentJob(jobKey, result) {
|
|
49
|
+
const { variables, ...agentResult } = result ?? {};
|
|
50
|
+
return parseSnapshot(this.engine.completeAgentJob(jobKey, JSON.stringify(variables ?? {}), JSON.stringify(agentResult ?? {})));
|
|
51
|
+
}
|
|
48
52
|
failJob(jobKey, retries, message) {
|
|
49
53
|
return parseSnapshot(this.engine.failJob(jobKey, retries, message));
|
|
50
54
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -118,6 +118,33 @@ export interface ActivateInstruction {
|
|
|
118
118
|
elementId: string;
|
|
119
119
|
variables?: Record<string, unknown>;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* One tool activation an agent asks for inside an ad-hoc sub-process (Camunda
|
|
123
|
+
* agentic `activateElements[]`): activate the inner element `elementId`, seeding
|
|
124
|
+
* `variables` into its local scope. Distinct from {@link ActivateInstruction}
|
|
125
|
+
* (process-instance modification) — this activates a *child* of the ad-hoc
|
|
126
|
+
* container, not a token in the process root.
|
|
127
|
+
*/
|
|
128
|
+
export interface AgentActivation {
|
|
129
|
+
elementId: string;
|
|
130
|
+
variables?: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The result an agent returns when completing an ad-hoc sub-process's agent job
|
|
134
|
+
* (Camunda's agentic `JobResult`): the tools to run this turn
|
|
135
|
+
* (`activateElements`), whether the container's `<completionCondition>` is now
|
|
136
|
+
* satisfied (`completionConditionFulfilled`), and whether to cancel any
|
|
137
|
+
* still-running tools (`cancelRemainingInstances`). `variables` merges into the
|
|
138
|
+
* instance on completion exactly like a plain job result (e.g. the agent's final
|
|
139
|
+
* decision). All fields optional: an empty result completes the container this
|
|
140
|
+
* turn with nothing activated.
|
|
141
|
+
*/
|
|
142
|
+
export interface AgentResult {
|
|
143
|
+
activateElements?: AgentActivation[];
|
|
144
|
+
completionConditionFulfilled?: boolean;
|
|
145
|
+
cancelRemainingInstances?: boolean;
|
|
146
|
+
variables?: Record<string, unknown>;
|
|
147
|
+
}
|
|
121
148
|
/**
|
|
122
149
|
* The full simulation state returned by every engine command. `activeElementIds`
|
|
123
150
|
* / `incidentElementIds` drive the token/incident highlight (the visual
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BojtosSession } from "./session.js";
|
|
2
|
-
import type { ActivatedJob, Snapshot } from "./types.js";
|
|
2
|
+
import type { ActivatedJob, AgentResult, Snapshot } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* The variables a handler merges into its instance on completion. Return an
|
|
5
5
|
* object to merge it, or `void`/`undefined` to complete with no new variables.
|
|
@@ -14,6 +14,17 @@ export type JobResult = Record<string, unknown>;
|
|
|
14
14
|
* completion. May be async. Throw to fail the job.
|
|
15
15
|
*/
|
|
16
16
|
export type JobHandler = (job: ActivatedJob) => JobResult | void | Promise<JobResult | void>;
|
|
17
|
+
/**
|
|
18
|
+
* A handler for an ad-hoc sub-process's **agent** job (the container's
|
|
19
|
+
* JOB_WORKER job). Given the activated container job (carrying the instance's
|
|
20
|
+
* current variables — e.g. accumulated tool outputs), return the
|
|
21
|
+
* {@link AgentResult} for this turn: which inner tools to activate, whether the
|
|
22
|
+
* agent is done, and any variables to merge. Called once per agent turn; the
|
|
23
|
+
* engine re-emits the agent job after the activated tools drain, so a stateful
|
|
24
|
+
* closure can drive a multi-turn agent (activate tools → read results →
|
|
25
|
+
* decide → complete). May be async. Throw to fail the container job.
|
|
26
|
+
*/
|
|
27
|
+
export type AgentHandler = (job: ActivatedJob) => AgentResult | Promise<AgentResult>;
|
|
17
28
|
/**
|
|
18
29
|
* Throw from a {@link JobHandler} to fail a job with an explicit remaining
|
|
19
30
|
* `retries` count (default is `job.retries - 1`). With `retries: 0` the engine
|
|
@@ -40,6 +51,16 @@ export interface DispatchOptions {
|
|
|
40
51
|
* exceeding the cap throws instead.
|
|
41
52
|
*/
|
|
42
53
|
maxRounds?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Handlers for ad-hoc sub-process **agent** job types (Camunda agentic
|
|
56
|
+
* `aiagent-job-worker`), keyed by the container's `zeebe:taskDefinition type`.
|
|
57
|
+
* Dispatched like {@link JobHandler}s but completed via
|
|
58
|
+
* {@link BojtosSession.completeAgentJob}, so their returned
|
|
59
|
+
* {@link AgentResult} drives the tools to activate this turn. The engine
|
|
60
|
+
* re-emits the agent job across turns, so the standard drain loop advances the
|
|
61
|
+
* whole agent conversation to quiescence.
|
|
62
|
+
*/
|
|
63
|
+
agents?: Record<string, AgentHandler>;
|
|
43
64
|
}
|
|
44
65
|
/** What one {@link dispatchRound} pass did. */
|
|
45
66
|
export interface RoundResult {
|
|
@@ -66,6 +87,13 @@ export interface DispatchResult {
|
|
|
66
87
|
* exactly one step. That makes this the animatable unit: drive it on a timer to
|
|
67
88
|
* watch the token(s) hop task-to-task. {@link dispatchWorkers} loops it to
|
|
68
89
|
* quiescence.
|
|
90
|
+
*
|
|
91
|
+
* Ad-hoc **agent** job types registered via `opts.agents` are activated and
|
|
92
|
+
* completed in the same frontier-snapshot pass, but through
|
|
93
|
+
* {@link BojtosSession.completeAgentJob} so their {@link AgentResult} activates
|
|
94
|
+
* the chosen tools. A tool a turn activates joins the *next* frontier, and the
|
|
95
|
+
* engine re-emits the agent job after those tools drain, so the agent's whole
|
|
96
|
+
* multi-turn conversation animates one step per round like any other token.
|
|
69
97
|
*/
|
|
70
98
|
export declare function dispatchRound(session: BojtosSession, workers: Record<string, JobHandler>, opts?: DispatchOptions): Promise<RoundResult>;
|
|
71
99
|
/**
|
package/dist/worker.js
CHANGED
|
@@ -35,6 +35,30 @@ async function runOne(session, handler, job) {
|
|
|
35
35
|
// we let it bubble to the caller.
|
|
36
36
|
session.completeJob(job.key, payload);
|
|
37
37
|
}
|
|
38
|
+
async function runOneAgent(session, handler, job) {
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
// As with a plain job, only the handler is treated as demo logic: a throw
|
|
42
|
+
// (or a result the engine can't serialize) fails the container job rather
|
|
43
|
+
// than bubbling up as an engine/ABI error.
|
|
44
|
+
result = await handler(job);
|
|
45
|
+
// Mirror runOne: a result the engine can't serialize is the demo's own
|
|
46
|
+
// logic failing, not an engine/ABI error. session.completeAgentJob
|
|
47
|
+
// stringifies the result internally (outside this try), so probe-serialize
|
|
48
|
+
// it here to route a serialization failure through failJob instead of
|
|
49
|
+
// letting it bubble out of the dispatch loop.
|
|
50
|
+
JSON.stringify(result);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const retries = e instanceof JobFailure && e.retries !== undefined
|
|
54
|
+
? e.retries
|
|
55
|
+
: Math.max(0, job.retries - 1);
|
|
56
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
57
|
+
session.failJob(job.key, retries, message);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
session.completeAgentJob(job.key, result);
|
|
61
|
+
}
|
|
38
62
|
/**
|
|
39
63
|
* Run one activate-and-handle pass: activate every registered job type's
|
|
40
64
|
* currently-`Created` jobs *first* (a snapshot of the token frontier), then hand
|
|
@@ -44,25 +68,54 @@ async function runOne(session, handler, job) {
|
|
|
44
68
|
* exactly one step. That makes this the animatable unit: drive it on a timer to
|
|
45
69
|
* watch the token(s) hop task-to-task. {@link dispatchWorkers} loops it to
|
|
46
70
|
* quiescence.
|
|
71
|
+
*
|
|
72
|
+
* Ad-hoc **agent** job types registered via `opts.agents` are activated and
|
|
73
|
+
* completed in the same frontier-snapshot pass, but through
|
|
74
|
+
* {@link BojtosSession.completeAgentJob} so their {@link AgentResult} activates
|
|
75
|
+
* the chosen tools. A tool a turn activates joins the *next* frontier, and the
|
|
76
|
+
* engine re-emits the agent job after those tools drain, so the agent's whole
|
|
77
|
+
* multi-turn conversation animates one step per round like any other token.
|
|
47
78
|
*/
|
|
48
79
|
export async function dispatchRound(session, workers, opts = {}) {
|
|
49
80
|
const maxJobs = opts.maxJobsPerActivation ?? 10;
|
|
50
81
|
const timeout = opts.lockTimeoutMs ?? 30_000;
|
|
51
82
|
const worker = opts.worker ?? "bojtos";
|
|
83
|
+
const agents = opts.agents ?? {};
|
|
84
|
+
// A job type registered as both a worker and an agent is ambiguous: the
|
|
85
|
+
// worker pass below would activate and plain-complete it first, so its
|
|
86
|
+
// agentic `activateElements` could never be sent. Reject up front rather than
|
|
87
|
+
// silently no-op the tool activation.
|
|
88
|
+
for (const jobType of Object.keys(agents)) {
|
|
89
|
+
if (jobType in workers) {
|
|
90
|
+
throw new Error(`dispatchRound: job type "${jobType}" is registered as both a worker and an agent — register it as exactly one`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
52
93
|
// Activation pass: lock the whole current frontier before running any handler,
|
|
53
94
|
// so a job a handler unblocks isn't also picked up this round (which would
|
|
54
95
|
// cascade the entire chain in a single "step").
|
|
55
|
-
const
|
|
96
|
+
const jobBatch = [];
|
|
56
97
|
for (const [jobType, handler] of Object.entries(workers)) {
|
|
57
98
|
for (const job of session.activateJobs(jobType, maxJobs, timeout, worker)) {
|
|
58
|
-
|
|
99
|
+
jobBatch.push({ handler, job });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const agentBatch = [];
|
|
103
|
+
for (const [jobType, handler] of Object.entries(agents)) {
|
|
104
|
+
for (const job of session.activateJobs(jobType, maxJobs, timeout, worker)) {
|
|
105
|
+
agentBatch.push({ handler, job });
|
|
59
106
|
}
|
|
60
107
|
}
|
|
61
108
|
// Handle pass.
|
|
62
|
-
for (const { handler, job } of
|
|
109
|
+
for (const { handler, job } of jobBatch) {
|
|
63
110
|
await runOne(session, handler, job);
|
|
64
111
|
}
|
|
65
|
-
|
|
112
|
+
for (const { handler, job } of agentBatch) {
|
|
113
|
+
await runOneAgent(session, handler, job);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
snapshot: session.snapshot(),
|
|
117
|
+
handled: jobBatch.length + agentBatch.length,
|
|
118
|
+
};
|
|
66
119
|
}
|
|
67
120
|
/**
|
|
68
121
|
* Drive an in-browser worker loop over a {@link BojtosSession}: repeatedly
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Framework-agnostic core of the Bojtos in-browser BPMN demo framework (ADR 0043): a single scenario runner over the @nanobpm/engine-wasm engine (deploy, start instances, complete/fail jobs, advance the clock, read snapshots and the event log), plus the engine's snapshot/event contract types. Consumed by @nanobpm/bojtos-react and the console test-run panel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Magikcraft/nano-bpm",
|
|
9
|
+
"url": "git+https://github.com/Magikcraft/nano-bpm.git",
|
|
10
10
|
"directory": "bojtos-kit"
|
|
11
11
|
},
|
|
12
12
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepack": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@nanobpm/engine-wasm": "^0.
|
|
35
|
+
"@nanobpm/engine-wasm": "^0.3.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.6.3"
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
JobFailure,
|
|
15
15
|
type JobHandler,
|
|
16
16
|
type JobResult,
|
|
17
|
+
type AgentHandler,
|
|
17
18
|
type DispatchOptions,
|
|
18
19
|
type DispatchResult,
|
|
19
20
|
type RoundResult,
|
|
@@ -26,5 +27,7 @@ export type {
|
|
|
26
27
|
IncidentDto,
|
|
27
28
|
TimerDto,
|
|
28
29
|
ActiveEl,
|
|
30
|
+
AgentActivation,
|
|
31
|
+
AgentResult,
|
|
29
32
|
WasmEvent,
|
|
30
33
|
} from "./types.js";
|
package/src/session.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import init, { type InitInput, TestEngine } from "@nanobpm/engine-wasm";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ActivatedJob,
|
|
4
|
+
ActivateInstruction,
|
|
5
|
+
AgentResult,
|
|
6
|
+
Snapshot,
|
|
7
|
+
WasmEvent,
|
|
8
|
+
} from "./types.js";
|
|
3
9
|
|
|
4
10
|
// Lazily initialise the wasm module exactly once per page, no matter how many
|
|
5
11
|
// sessions are created. Mirrors the console's original `ensureWasm`.
|
|
@@ -68,6 +74,16 @@ export interface BojtosSession {
|
|
|
68
74
|
): ActivatedJob[];
|
|
69
75
|
/** Complete a waiting job, merging `variablesJson` into the instance. */
|
|
70
76
|
completeJob(jobKey: string, variablesJson: string): Snapshot;
|
|
77
|
+
/**
|
|
78
|
+
* Complete an ad-hoc sub-process's **agent** job — the container's JOB_WORKER
|
|
79
|
+
* job (Camunda's agentic `aiagent-job-worker`) — carrying the agent's
|
|
80
|
+
* {@link AgentResult}. Its `activateElements` run the chosen inner tools this
|
|
81
|
+
* turn; `completionConditionFulfilled` ends the agent loop; `variables` merge
|
|
82
|
+
* into the instance (e.g. the agent's final decision). This is the ad-hoc seam
|
|
83
|
+
* plain {@link completeJob} deliberately omits. Register agents on the dispatch
|
|
84
|
+
* loop via `DispatchOptions.agents` rather than calling this directly.
|
|
85
|
+
*/
|
|
86
|
+
completeAgentJob(jobKey: string, result: AgentResult): Snapshot;
|
|
71
87
|
/** Fail a waiting job; with no retries left this raises an incident. */
|
|
72
88
|
failJob(jobKey: string, retries: number, message: string): Snapshot;
|
|
73
89
|
/**
|
|
@@ -206,6 +222,17 @@ class WasmBojtosSession implements BojtosSession {
|
|
|
206
222
|
return parseSnapshot(this.engine.completeJob(jobKey, variablesJson || "{}"));
|
|
207
223
|
}
|
|
208
224
|
|
|
225
|
+
completeAgentJob(jobKey: string, result: AgentResult): Snapshot {
|
|
226
|
+
const { variables, ...agentResult } = result ?? {};
|
|
227
|
+
return parseSnapshot(
|
|
228
|
+
this.engine.completeAgentJob(
|
|
229
|
+
jobKey,
|
|
230
|
+
JSON.stringify(variables ?? {}),
|
|
231
|
+
JSON.stringify(agentResult ?? {}),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
209
236
|
failJob(jobKey: string, retries: number, message: string): Snapshot {
|
|
210
237
|
return parseSnapshot(this.engine.failJob(jobKey, retries, message));
|
|
211
238
|
}
|
package/src/types.ts
CHANGED
|
@@ -137,6 +137,35 @@ export interface ActivateInstruction {
|
|
|
137
137
|
variables?: Record<string, unknown>;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* One tool activation an agent asks for inside an ad-hoc sub-process (Camunda
|
|
142
|
+
* agentic `activateElements[]`): activate the inner element `elementId`, seeding
|
|
143
|
+
* `variables` into its local scope. Distinct from {@link ActivateInstruction}
|
|
144
|
+
* (process-instance modification) — this activates a *child* of the ad-hoc
|
|
145
|
+
* container, not a token in the process root.
|
|
146
|
+
*/
|
|
147
|
+
export interface AgentActivation {
|
|
148
|
+
elementId: string;
|
|
149
|
+
variables?: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The result an agent returns when completing an ad-hoc sub-process's agent job
|
|
154
|
+
* (Camunda's agentic `JobResult`): the tools to run this turn
|
|
155
|
+
* (`activateElements`), whether the container's `<completionCondition>` is now
|
|
156
|
+
* satisfied (`completionConditionFulfilled`), and whether to cancel any
|
|
157
|
+
* still-running tools (`cancelRemainingInstances`). `variables` merges into the
|
|
158
|
+
* instance on completion exactly like a plain job result (e.g. the agent's final
|
|
159
|
+
* decision). All fields optional: an empty result completes the container this
|
|
160
|
+
* turn with nothing activated.
|
|
161
|
+
*/
|
|
162
|
+
export interface AgentResult {
|
|
163
|
+
activateElements?: AgentActivation[];
|
|
164
|
+
completionConditionFulfilled?: boolean;
|
|
165
|
+
cancelRemainingInstances?: boolean;
|
|
166
|
+
variables?: Record<string, unknown>;
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
/**
|
|
141
170
|
* The full simulation state returned by every engine command. `activeElementIds`
|
|
142
171
|
* / `incidentElementIds` drive the token/incident highlight (the visual
|
package/src/worker.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BojtosSession } from "./session.js";
|
|
2
|
-
import type { ActivatedJob, Snapshot } from "./types.js";
|
|
2
|
+
import type { ActivatedJob, AgentResult, Snapshot } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The variables a handler merges into its instance on completion. Return an
|
|
@@ -19,6 +19,20 @@ export type JobHandler = (
|
|
|
19
19
|
job: ActivatedJob,
|
|
20
20
|
) => JobResult | void | Promise<JobResult | void>;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A handler for an ad-hoc sub-process's **agent** job (the container's
|
|
24
|
+
* JOB_WORKER job). Given the activated container job (carrying the instance's
|
|
25
|
+
* current variables — e.g. accumulated tool outputs), return the
|
|
26
|
+
* {@link AgentResult} for this turn: which inner tools to activate, whether the
|
|
27
|
+
* agent is done, and any variables to merge. Called once per agent turn; the
|
|
28
|
+
* engine re-emits the agent job after the activated tools drain, so a stateful
|
|
29
|
+
* closure can drive a multi-turn agent (activate tools → read results →
|
|
30
|
+
* decide → complete). May be async. Throw to fail the container job.
|
|
31
|
+
*/
|
|
32
|
+
export type AgentHandler = (
|
|
33
|
+
job: ActivatedJob,
|
|
34
|
+
) => AgentResult | Promise<AgentResult>;
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
37
|
* Throw from a {@link JobHandler} to fail a job with an explicit remaining
|
|
24
38
|
* `retries` count (default is `job.retries - 1`). With `retries: 0` the engine
|
|
@@ -48,6 +62,16 @@ export interface DispatchOptions {
|
|
|
48
62
|
* exceeding the cap throws instead.
|
|
49
63
|
*/
|
|
50
64
|
maxRounds?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Handlers for ad-hoc sub-process **agent** job types (Camunda agentic
|
|
67
|
+
* `aiagent-job-worker`), keyed by the container's `zeebe:taskDefinition type`.
|
|
68
|
+
* Dispatched like {@link JobHandler}s but completed via
|
|
69
|
+
* {@link BojtosSession.completeAgentJob}, so their returned
|
|
70
|
+
* {@link AgentResult} drives the tools to activate this turn. The engine
|
|
71
|
+
* re-emits the agent job across turns, so the standard drain loop advances the
|
|
72
|
+
* whole agent conversation to quiescence.
|
|
73
|
+
*/
|
|
74
|
+
agents?: Record<string, AgentHandler>;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
/** What one {@link dispatchRound} pass did. */
|
|
@@ -96,6 +120,35 @@ async function runOne(
|
|
|
96
120
|
session.completeJob(job.key, payload);
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
async function runOneAgent(
|
|
124
|
+
session: BojtosSession,
|
|
125
|
+
handler: AgentHandler,
|
|
126
|
+
job: ActivatedJob,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
let result: AgentResult;
|
|
129
|
+
try {
|
|
130
|
+
// As with a plain job, only the handler is treated as demo logic: a throw
|
|
131
|
+
// (or a result the engine can't serialize) fails the container job rather
|
|
132
|
+
// than bubbling up as an engine/ABI error.
|
|
133
|
+
result = await handler(job);
|
|
134
|
+
// Mirror runOne: a result the engine can't serialize is the demo's own
|
|
135
|
+
// logic failing, not an engine/ABI error. session.completeAgentJob
|
|
136
|
+
// stringifies the result internally (outside this try), so probe-serialize
|
|
137
|
+
// it here to route a serialization failure through failJob instead of
|
|
138
|
+
// letting it bubble out of the dispatch loop.
|
|
139
|
+
JSON.stringify(result);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
const retries =
|
|
142
|
+
e instanceof JobFailure && e.retries !== undefined
|
|
143
|
+
? e.retries
|
|
144
|
+
: Math.max(0, job.retries - 1);
|
|
145
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
146
|
+
session.failJob(job.key, retries, message);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
session.completeAgentJob(job.key, result);
|
|
150
|
+
}
|
|
151
|
+
|
|
99
152
|
/**
|
|
100
153
|
* Run one activate-and-handle pass: activate every registered job type's
|
|
101
154
|
* currently-`Created` jobs *first* (a snapshot of the token frontier), then hand
|
|
@@ -105,6 +158,13 @@ async function runOne(
|
|
|
105
158
|
* exactly one step. That makes this the animatable unit: drive it on a timer to
|
|
106
159
|
* watch the token(s) hop task-to-task. {@link dispatchWorkers} loops it to
|
|
107
160
|
* quiescence.
|
|
161
|
+
*
|
|
162
|
+
* Ad-hoc **agent** job types registered via `opts.agents` are activated and
|
|
163
|
+
* completed in the same frontier-snapshot pass, but through
|
|
164
|
+
* {@link BojtosSession.completeAgentJob} so their {@link AgentResult} activates
|
|
165
|
+
* the chosen tools. A tool a turn activates joins the *next* frontier, and the
|
|
166
|
+
* engine re-emits the agent job after those tools drain, so the agent's whole
|
|
167
|
+
* multi-turn conversation animates one step per round like any other token.
|
|
108
168
|
*/
|
|
109
169
|
export async function dispatchRound(
|
|
110
170
|
session: BojtosSession,
|
|
@@ -114,20 +174,44 @@ export async function dispatchRound(
|
|
|
114
174
|
const maxJobs = opts.maxJobsPerActivation ?? 10;
|
|
115
175
|
const timeout = opts.lockTimeoutMs ?? 30_000;
|
|
116
176
|
const worker = opts.worker ?? "bojtos";
|
|
177
|
+
const agents = opts.agents ?? {};
|
|
178
|
+
// A job type registered as both a worker and an agent is ambiguous: the
|
|
179
|
+
// worker pass below would activate and plain-complete it first, so its
|
|
180
|
+
// agentic `activateElements` could never be sent. Reject up front rather than
|
|
181
|
+
// silently no-op the tool activation.
|
|
182
|
+
for (const jobType of Object.keys(agents)) {
|
|
183
|
+
if (jobType in workers) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`dispatchRound: job type "${jobType}" is registered as both a worker and an agent — register it as exactly one`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
117
189
|
// Activation pass: lock the whole current frontier before running any handler,
|
|
118
190
|
// so a job a handler unblocks isn't also picked up this round (which would
|
|
119
191
|
// cascade the entire chain in a single "step").
|
|
120
|
-
const
|
|
192
|
+
const jobBatch: { handler: JobHandler; job: ActivatedJob }[] = [];
|
|
121
193
|
for (const [jobType, handler] of Object.entries(workers)) {
|
|
122
194
|
for (const job of session.activateJobs(jobType, maxJobs, timeout, worker)) {
|
|
123
|
-
|
|
195
|
+
jobBatch.push({ handler, job });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const agentBatch: { handler: AgentHandler; job: ActivatedJob }[] = [];
|
|
199
|
+
for (const [jobType, handler] of Object.entries(agents)) {
|
|
200
|
+
for (const job of session.activateJobs(jobType, maxJobs, timeout, worker)) {
|
|
201
|
+
agentBatch.push({ handler, job });
|
|
124
202
|
}
|
|
125
203
|
}
|
|
126
204
|
// Handle pass.
|
|
127
|
-
for (const { handler, job } of
|
|
205
|
+
for (const { handler, job } of jobBatch) {
|
|
128
206
|
await runOne(session, handler, job);
|
|
129
207
|
}
|
|
130
|
-
|
|
208
|
+
for (const { handler, job } of agentBatch) {
|
|
209
|
+
await runOneAgent(session, handler, job);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
snapshot: session.snapshot(),
|
|
213
|
+
handled: jobBatch.length + agentBatch.length,
|
|
214
|
+
};
|
|
131
215
|
}
|
|
132
216
|
|
|
133
217
|
/**
|