@nanobpm/bojtos-kit 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @nanobpm/bojtos-kit
2
2
 
3
3
  Framework-agnostic core of the **Bojtos** in-browser BPMN demo framework
4
- ([ADR 0043](../docs/adr/0043-bojtos-demo-framework.md)).
4
+ ([ADR 0043](../README.md#design)).
5
5
 
6
- It wraps [`@nanobpm/engine-wasm`](../engine-wasm) as a single scenario runner —
6
+ It wraps [`@nanobpm/engine-wasm`](https://www.npmjs.com/package/@nanobpm/engine-wasm) as a single scenario runner —
7
7
  the one runner the whole framework (and the console test-run panel) drives, so
8
8
  there is no second, drift-prone engine harness — and re-exports the engine's
9
9
  snapshot/event contract types.
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 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.2.0",
3
+ "version": "0.4.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/nanobpm/bojtos.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.2.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, ActivateInstruction, 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,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 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
  /**