@nanobpm/bojtos-kit 0.1.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 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, 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,8 +46,75 @@ 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;
61
+ /**
62
+ * Throw a BPMN business error from a waiting job: interrupts the activity via a
63
+ * matching error boundary/event-subprocess catch, or raises an incident if
64
+ * uncaught. The job is consumed either way.
65
+ */
66
+ throwError(jobKey: string, errorCode: string, errorMessage: string): Snapshot;
67
+ /**
68
+ * Set a job's remaining retries. Used to recover a job parked on a no-retries
69
+ * incident before resolving that incident; does not itself unblock the job.
70
+ */
71
+ updateRetries(jobKey: string, retries: number): Snapshot;
72
+ /**
73
+ * Resolve an open incident by key, retrying the work that failed (returns a
74
+ * parked job to the activatable pool / re-evaluates a gateway / re-creates a
75
+ * service-task job).
76
+ */
77
+ resolveIncident(incidentKey: string): Snapshot;
78
+ /**
79
+ * Merge variables into a scope (a process-instance or element-instance key).
80
+ * When `local` is true they are written strictly into the target scope,
81
+ * otherwise they propagate up to the nearest ancestor scope defining each name.
82
+ */
83
+ setVariables(scopeKey: string, variablesJson: string, local: boolean): Snapshot;
84
+ /**
85
+ * Broadcast a signal by name to every open subscription that matches, across
86
+ * all instances, merging `variablesJson` into each correlated instance.
87
+ */
88
+ broadcastSignal(signalName: string, variablesJson: string): Snapshot;
89
+ /** Cancel (terminate) a running process instance: every token is discarded. */
90
+ cancelInstance(instanceKey: string): Snapshot;
91
+ /**
92
+ * Modify a running process instance (Zeebe "modify process instance"): move
93
+ * tokens by terminating existing element instances and/or activating new
94
+ * ones. Each activate instruction places a token at `elementId` (in the
95
+ * process root scope), first merging its optional `variables` into the root
96
+ * scope; `terminateElementInstanceKeys` are the keys of active element
97
+ * instances (from `instances[].activeElements[].key`) to terminate. If the
98
+ * terminations drain the last token and nothing is activated, the instance is
99
+ * terminated.
100
+ */
101
+ modify(instanceKey: string, activateInstructions: ActivateInstruction[], terminateElementInstanceKeys: string[]): Snapshot;
102
+ /** Complete a waiting user task, merging `variablesJson` into the instance. */
103
+ completeUserTask(userTaskKey: string, variablesJson: string): Snapshot;
104
+ /**
105
+ * Assign a user task to `assignee`. With `allowOverride` false the command is
106
+ * rejected if the task already has an assignee (unassign it first).
107
+ */
108
+ assignUserTask(userTaskKey: string, assignee: string, allowOverride: boolean): Snapshot;
109
+ /** Clear a user task's assignee. */
110
+ unassignUserTask(userTaskKey: string): Snapshot;
111
+ /**
112
+ * Update a user task's attributes from a JSON changeset. Recognised keys (all
113
+ * optional): `candidateGroups` / `candidateUsers` (string arrays),
114
+ * `dueDate` / `followUpDate` (ISO-8601 string, or `null`/`""` to clear),
115
+ * `priority` (0..=100). Only present keys are changed.
116
+ */
117
+ updateUserTask(userTaskKey: string, changesetJson: string): Snapshot;
51
118
  /**
52
119
  * Correlate a message to any instance waiting on it: publishes `messageName`
53
120
  * with `correlationKey` (the value the waiting subscription's `correlationKey`
package/dist/session.js CHANGED
@@ -45,9 +45,46 @@ 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
  }
55
+ throwError(jobKey, errorCode, errorMessage) {
56
+ return parseSnapshot(this.engine.throwError(jobKey, errorCode, errorMessage));
57
+ }
58
+ updateRetries(jobKey, retries) {
59
+ return parseSnapshot(this.engine.updateRetries(jobKey, retries));
60
+ }
61
+ resolveIncident(incidentKey) {
62
+ return parseSnapshot(this.engine.resolveIncident(incidentKey));
63
+ }
64
+ setVariables(scopeKey, variablesJson, local) {
65
+ return parseSnapshot(this.engine.setVariables(scopeKey, variablesJson || "{}", local));
66
+ }
67
+ broadcastSignal(signalName, variablesJson) {
68
+ return parseSnapshot(this.engine.broadcastSignal(signalName, variablesJson || "{}"));
69
+ }
70
+ cancelInstance(instanceKey) {
71
+ return parseSnapshot(this.engine.cancelInstance(instanceKey));
72
+ }
73
+ modify(instanceKey, activateInstructions, terminateElementInstanceKeys) {
74
+ return parseSnapshot(this.engine.modify(instanceKey, JSON.stringify(activateInstructions ?? []), JSON.stringify(terminateElementInstanceKeys ?? [])));
75
+ }
76
+ completeUserTask(userTaskKey, variablesJson) {
77
+ return parseSnapshot(this.engine.completeUserTask(userTaskKey, variablesJson || "{}"));
78
+ }
79
+ assignUserTask(userTaskKey, assignee, allowOverride) {
80
+ return parseSnapshot(this.engine.assignUserTask(userTaskKey, assignee, allowOverride));
81
+ }
82
+ unassignUserTask(userTaskKey) {
83
+ return parseSnapshot(this.engine.unassignUserTask(userTaskKey));
84
+ }
85
+ updateUserTask(userTaskKey, changesetJson) {
86
+ return parseSnapshot(this.engine.updateUserTask(userTaskKey, changesetJson || "{}"));
87
+ }
51
88
  correlateMessage(messageName, correlationKey, variablesJson) {
52
89
  return parseSnapshot(this.engine.correlateMessage(messageName, correlationKey, variablesJson || "{}"));
53
90
  }
package/dist/types.d.ts CHANGED
@@ -51,10 +51,107 @@ export interface TimerDto {
51
51
  dueAt: number;
52
52
  dueInMs: number;
53
53
  }
54
+ /** A user task parked on a `userTask` element, awaiting a human. */
55
+ export interface UserTaskDto {
56
+ key: string;
57
+ instanceKey: string;
58
+ elementInstanceKey: string;
59
+ elementId: string;
60
+ /** `Created` (waiting), `Completed`, or `Canceled`. */
61
+ state: string;
62
+ assignee?: string;
63
+ candidateGroups: string[];
64
+ candidateUsers: string[];
65
+ dueDate?: string;
66
+ followUpDate?: string;
67
+ priority: number;
68
+ }
69
+ /** An open message subscription (a waiting message catch/boundary event). */
70
+ export interface MessageSubscriptionDto {
71
+ key: string;
72
+ instanceKey: string;
73
+ elementId: string;
74
+ messageName: string;
75
+ correlationKey: string;
76
+ /** What the subscription guards (intermediate/boundary, interrupting or not). */
77
+ kind: string;
78
+ }
79
+ /** An open signal subscription (a waiting signal catch/boundary event). */
80
+ export interface SignalSubscriptionDto {
81
+ key: string;
82
+ instanceKey: string;
83
+ elementId: string;
84
+ signalName: string;
85
+ kind: string;
86
+ }
87
+ /**
88
+ * Per-element token statistics for diagram overlays: `active` live tokens,
89
+ * cumulative `completed` element instances, and current `incidents`.
90
+ */
91
+ export interface ElementStatDto {
92
+ elementId: string;
93
+ active: number;
94
+ completed: number;
95
+ incidents: number;
96
+ }
97
+ /** A traversed connection, as source/target element ids. */
98
+ export interface SequenceFlowDto {
99
+ from: string;
100
+ to: string;
101
+ }
102
+ /** An evaluated decision instance (from a `businessRuleTask` / DMN). */
103
+ export interface DecisionInstanceDto {
104
+ instanceKey: string;
105
+ elementId: string;
106
+ decisionKey: string;
107
+ decisionId: string;
108
+ output: unknown;
109
+ evaluatedAt: number;
110
+ }
111
+ /**
112
+ * One activation instruction for {@link BojtosSession.modify}: place a new token
113
+ * at `elementId`, first merging `variables` into the instance's root scope.
114
+ * Mirrors Zeebe's process-instance-modification activate instruction (the token
115
+ * is activated in the process root scope).
116
+ */
117
+ export interface ActivateInstruction {
118
+ elementId: string;
119
+ variables?: Record<string, unknown>;
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
+ }
54
148
  /**
55
149
  * The full simulation state returned by every engine command. `activeElementIds`
56
150
  * / `incidentElementIds` drive the token/incident highlight (the visual
57
151
  * contract, ADR 0043 §4); `instances[].variables` is the live payload.
152
+ * `userTasks`, `messageSubscriptions`, `signalSubscriptions`, `elementStats`,
153
+ * `takenSequenceFlows` and `decisionInstances` back a Web-Modeler-Play-style UI
154
+ * (task panels, correlation/broadcast, overlays, DMN results).
58
155
  */
59
156
  export interface Snapshot {
60
157
  now: number;
@@ -67,6 +164,12 @@ export interface Snapshot {
67
164
  jobs: JobDto[];
68
165
  incidents: IncidentDto[];
69
166
  timers: TimerDto[];
167
+ userTasks: UserTaskDto[];
168
+ messageSubscriptions: MessageSubscriptionDto[];
169
+ signalSubscriptions: SignalSubscriptionDto[];
170
+ elementStats: ElementStatDto[];
171
+ takenSequenceFlows: SequenceFlowDto[];
172
+ decisionInstances: DecisionInstanceDto[];
70
173
  activeElementIds: string[];
71
174
  incidentElementIds: string[];
72
175
  }
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 batch = [];
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
- batch.push({ handler, job });
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 batch) {
109
+ for (const { handler, job } of jobBatch) {
63
110
  await runOne(session, handler, job);
64
111
  }
65
- return { snapshot: session.snapshot(), handled: batch.length };
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.1.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.1.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 { ActivatedJob, Snapshot, WasmEvent } from "./types.js";
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,8 +74,87 @@ 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;
89
+ /**
90
+ * Throw a BPMN business error from a waiting job: interrupts the activity via a
91
+ * matching error boundary/event-subprocess catch, or raises an incident if
92
+ * uncaught. The job is consumed either way.
93
+ */
94
+ throwError(jobKey: string, errorCode: string, errorMessage: string): Snapshot;
95
+ /**
96
+ * Set a job's remaining retries. Used to recover a job parked on a no-retries
97
+ * incident before resolving that incident; does not itself unblock the job.
98
+ */
99
+ updateRetries(jobKey: string, retries: number): Snapshot;
100
+ /**
101
+ * Resolve an open incident by key, retrying the work that failed (returns a
102
+ * parked job to the activatable pool / re-evaluates a gateway / re-creates a
103
+ * service-task job).
104
+ */
105
+ resolveIncident(incidentKey: string): Snapshot;
106
+ /**
107
+ * Merge variables into a scope (a process-instance or element-instance key).
108
+ * When `local` is true they are written strictly into the target scope,
109
+ * otherwise they propagate up to the nearest ancestor scope defining each name.
110
+ */
111
+ setVariables(
112
+ scopeKey: string,
113
+ variablesJson: string,
114
+ local: boolean,
115
+ ): Snapshot;
116
+ /**
117
+ * Broadcast a signal by name to every open subscription that matches, across
118
+ * all instances, merging `variablesJson` into each correlated instance.
119
+ */
120
+ broadcastSignal(signalName: string, variablesJson: string): Snapshot;
121
+ /** Cancel (terminate) a running process instance: every token is discarded. */
122
+ cancelInstance(instanceKey: string): Snapshot;
123
+ /**
124
+ * Modify a running process instance (Zeebe "modify process instance"): move
125
+ * tokens by terminating existing element instances and/or activating new
126
+ * ones. Each activate instruction places a token at `elementId` (in the
127
+ * process root scope), first merging its optional `variables` into the root
128
+ * scope; `terminateElementInstanceKeys` are the keys of active element
129
+ * instances (from `instances[].activeElements[].key`) to terminate. If the
130
+ * terminations drain the last token and nothing is activated, the instance is
131
+ * terminated.
132
+ */
133
+ modify(
134
+ instanceKey: string,
135
+ activateInstructions: ActivateInstruction[],
136
+ terminateElementInstanceKeys: string[],
137
+ ): Snapshot;
138
+ /** Complete a waiting user task, merging `variablesJson` into the instance. */
139
+ completeUserTask(userTaskKey: string, variablesJson: string): Snapshot;
140
+ /**
141
+ * Assign a user task to `assignee`. With `allowOverride` false the command is
142
+ * rejected if the task already has an assignee (unassign it first).
143
+ */
144
+ assignUserTask(
145
+ userTaskKey: string,
146
+ assignee: string,
147
+ allowOverride: boolean,
148
+ ): Snapshot;
149
+ /** Clear a user task's assignee. */
150
+ unassignUserTask(userTaskKey: string): Snapshot;
151
+ /**
152
+ * Update a user task's attributes from a JSON changeset. Recognised keys (all
153
+ * optional): `candidateGroups` / `candidateUsers` (string arrays),
154
+ * `dueDate` / `followUpDate` (ISO-8601 string, or `null`/`""` to clear),
155
+ * `priority` (0..=100). Only present keys are changed.
156
+ */
157
+ updateUserTask(userTaskKey: string, changesetJson: string): Snapshot;
73
158
  /**
74
159
  * Correlate a message to any instance waiting on it: publishes `messageName`
75
160
  * with `correlationKey` (the value the waiting subscription's `correlationKey`
@@ -137,10 +222,99 @@ class WasmBojtosSession implements BojtosSession {
137
222
  return parseSnapshot(this.engine.completeJob(jobKey, variablesJson || "{}"));
138
223
  }
139
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
+
140
236
  failJob(jobKey: string, retries: number, message: string): Snapshot {
141
237
  return parseSnapshot(this.engine.failJob(jobKey, retries, message));
142
238
  }
143
239
 
240
+ throwError(
241
+ jobKey: string,
242
+ errorCode: string,
243
+ errorMessage: string,
244
+ ): Snapshot {
245
+ return parseSnapshot(
246
+ this.engine.throwError(jobKey, errorCode, errorMessage),
247
+ );
248
+ }
249
+
250
+ updateRetries(jobKey: string, retries: number): Snapshot {
251
+ return parseSnapshot(this.engine.updateRetries(jobKey, retries));
252
+ }
253
+
254
+ resolveIncident(incidentKey: string): Snapshot {
255
+ return parseSnapshot(this.engine.resolveIncident(incidentKey));
256
+ }
257
+
258
+ setVariables(
259
+ scopeKey: string,
260
+ variablesJson: string,
261
+ local: boolean,
262
+ ): Snapshot {
263
+ return parseSnapshot(
264
+ this.engine.setVariables(scopeKey, variablesJson || "{}", local),
265
+ );
266
+ }
267
+
268
+ broadcastSignal(signalName: string, variablesJson: string): Snapshot {
269
+ return parseSnapshot(
270
+ this.engine.broadcastSignal(signalName, variablesJson || "{}"),
271
+ );
272
+ }
273
+
274
+ cancelInstance(instanceKey: string): Snapshot {
275
+ return parseSnapshot(this.engine.cancelInstance(instanceKey));
276
+ }
277
+
278
+ modify(
279
+ instanceKey: string,
280
+ activateInstructions: ActivateInstruction[],
281
+ terminateElementInstanceKeys: string[],
282
+ ): Snapshot {
283
+ return parseSnapshot(
284
+ this.engine.modify(
285
+ instanceKey,
286
+ JSON.stringify(activateInstructions ?? []),
287
+ JSON.stringify(terminateElementInstanceKeys ?? []),
288
+ ),
289
+ );
290
+ }
291
+
292
+ completeUserTask(userTaskKey: string, variablesJson: string): Snapshot {
293
+ return parseSnapshot(
294
+ this.engine.completeUserTask(userTaskKey, variablesJson || "{}"),
295
+ );
296
+ }
297
+
298
+ assignUserTask(
299
+ userTaskKey: string,
300
+ assignee: string,
301
+ allowOverride: boolean,
302
+ ): Snapshot {
303
+ return parseSnapshot(
304
+ this.engine.assignUserTask(userTaskKey, assignee, allowOverride),
305
+ );
306
+ }
307
+
308
+ unassignUserTask(userTaskKey: string): Snapshot {
309
+ return parseSnapshot(this.engine.unassignUserTask(userTaskKey));
310
+ }
311
+
312
+ updateUserTask(userTaskKey: string, changesetJson: string): Snapshot {
313
+ return parseSnapshot(
314
+ this.engine.updateUserTask(userTaskKey, changesetJson || "{}"),
315
+ );
316
+ }
317
+
144
318
  correlateMessage(
145
319
  messageName: string,
146
320
  correlationKey: string,
package/src/types.ts CHANGED
@@ -63,10 +63,116 @@ export interface TimerDto {
63
63
  dueInMs: number;
64
64
  }
65
65
 
66
+ /** A user task parked on a `userTask` element, awaiting a human. */
67
+ export interface UserTaskDto {
68
+ key: string;
69
+ instanceKey: string;
70
+ elementInstanceKey: string;
71
+ elementId: string;
72
+ /** `Created` (waiting), `Completed`, or `Canceled`. */
73
+ state: string;
74
+ assignee?: string;
75
+ candidateGroups: string[];
76
+ candidateUsers: string[];
77
+ dueDate?: string;
78
+ followUpDate?: string;
79
+ priority: number;
80
+ }
81
+
82
+ /** An open message subscription (a waiting message catch/boundary event). */
83
+ export interface MessageSubscriptionDto {
84
+ key: string;
85
+ instanceKey: string;
86
+ elementId: string;
87
+ messageName: string;
88
+ correlationKey: string;
89
+ /** What the subscription guards (intermediate/boundary, interrupting or not). */
90
+ kind: string;
91
+ }
92
+
93
+ /** An open signal subscription (a waiting signal catch/boundary event). */
94
+ export interface SignalSubscriptionDto {
95
+ key: string;
96
+ instanceKey: string;
97
+ elementId: string;
98
+ signalName: string;
99
+ kind: string;
100
+ }
101
+
102
+ /**
103
+ * Per-element token statistics for diagram overlays: `active` live tokens,
104
+ * cumulative `completed` element instances, and current `incidents`.
105
+ */
106
+ export interface ElementStatDto {
107
+ elementId: string;
108
+ active: number;
109
+ completed: number;
110
+ incidents: number;
111
+ }
112
+
113
+ /** A traversed connection, as source/target element ids. */
114
+ export interface SequenceFlowDto {
115
+ from: string;
116
+ to: string;
117
+ }
118
+
119
+ /** An evaluated decision instance (from a `businessRuleTask` / DMN). */
120
+ export interface DecisionInstanceDto {
121
+ instanceKey: string;
122
+ elementId: string;
123
+ decisionKey: string;
124
+ decisionId: string;
125
+ output: unknown;
126
+ evaluatedAt: number;
127
+ }
128
+
129
+ /**
130
+ * One activation instruction for {@link BojtosSession.modify}: place a new token
131
+ * at `elementId`, first merging `variables` into the instance's root scope.
132
+ * Mirrors Zeebe's process-instance-modification activate instruction (the token
133
+ * is activated in the process root scope).
134
+ */
135
+ export interface ActivateInstruction {
136
+ elementId: string;
137
+ variables?: Record<string, unknown>;
138
+ }
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
+
66
169
  /**
67
170
  * The full simulation state returned by every engine command. `activeElementIds`
68
171
  * / `incidentElementIds` drive the token/incident highlight (the visual
69
172
  * contract, ADR 0043 §4); `instances[].variables` is the live payload.
173
+ * `userTasks`, `messageSubscriptions`, `signalSubscriptions`, `elementStats`,
174
+ * `takenSequenceFlows` and `decisionInstances` back a Web-Modeler-Play-style UI
175
+ * (task panels, correlation/broadcast, overlays, DMN results).
70
176
  */
71
177
  export interface Snapshot {
72
178
  now: number;
@@ -79,6 +185,12 @@ export interface Snapshot {
79
185
  jobs: JobDto[];
80
186
  incidents: IncidentDto[];
81
187
  timers: TimerDto[];
188
+ userTasks: UserTaskDto[];
189
+ messageSubscriptions: MessageSubscriptionDto[];
190
+ signalSubscriptions: SignalSubscriptionDto[];
191
+ elementStats: ElementStatDto[];
192
+ takenSequenceFlows: SequenceFlowDto[];
193
+ decisionInstances: DecisionInstanceDto[];
82
194
  activeElementIds: string[];
83
195
  incidentElementIds: string[];
84
196
  }
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 batch: { handler: JobHandler; job: ActivatedJob }[] = [];
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
- batch.push({ handler, job });
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 batch) {
205
+ for (const { handler, job } of jobBatch) {
128
206
  await runOne(session, handler, job);
129
207
  }
130
- return { snapshot: session.snapshot(), handled: batch.length };
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
  /**