@nanobpm/urban 0.88.1 → 0.90.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/runtime/core/host.d.ts +149 -1
- package/dist/runtime/core/host.js +3 -0
- package/dist/runtime/core/modules/mcp.js +68 -0
- package/dist/runtime/engine/nanosdk.d.ts +32 -1
- package/dist/runtime/engine/nanosdk.js +210 -6
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/runtime/core/host.ts +155 -0
- package/src/runtime/core/modules/actions.test.ts +3 -0
- package/src/runtime/core/modules/agent.test.ts +3 -0
- package/src/runtime/core/modules/api.test.ts +3 -0
- package/src/runtime/core/modules/cancel.test.ts +3 -0
- package/src/runtime/core/modules/connectors.test.ts +3 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +3 -0
- package/src/runtime/core/modules/instance-tracking.test.ts +3 -0
- package/src/runtime/core/modules/mcp.test.ts +97 -0
- package/src/runtime/core/modules/mcp.ts +66 -0
- package/src/runtime/core/modules/pages.test.ts +3 -0
- package/src/runtime/core/modules/surfaces.test.ts +6 -0
- package/src/runtime/core/modules/triggers.test.ts +3 -0
- package/src/runtime/core/modules/workers.test.ts +3 -0
- package/src/runtime/core/runtime.extensions.test.ts +9 -0
- package/src/runtime/core/runtime.test.ts +9 -0
- package/src/runtime/engine/nanosdk.test.ts +136 -0
- package/src/runtime/engine/nanosdk.ts +239 -6
- package/src/runtime/index.ts +4 -0
package/src/runtime/core/host.ts
CHANGED
|
@@ -229,6 +229,22 @@ export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
|
229
229
|
export interface ProcessInstanceSnapshot {
|
|
230
230
|
readonly processInstanceKey: string;
|
|
231
231
|
readonly state: ProcessInstanceState;
|
|
232
|
+
/** The key of the deployed process definition this instance runs — the handle a caller feeds to
|
|
233
|
+
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
234
|
+
* `processDefinitionKey` → deployed XML path, no repo checkout). Absent when the engine omits it. */
|
|
235
|
+
readonly processDefinitionKey?: string;
|
|
236
|
+
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
237
|
+
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
238
|
+
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
239
|
+
* `processDefinitionKey`). Lets a reduced-capability read path map a native child back to the
|
|
240
|
+
* subject that spawned it without a raw-REST channel. */
|
|
241
|
+
readonly parentProcessInstanceKey?: string;
|
|
242
|
+
/** The key of this instance's *root* process instance — the top-level ancestor of the
|
|
243
|
+
* call-activity hierarchy this instance belongs to (equal to `processInstanceKey` for a
|
|
244
|
+
* top-level instance). `undefined`/absent when the engine omits it (only present for hierarchies
|
|
245
|
+
* created on an engine version that reports it). Lets a reduced-capability read path correlate a
|
|
246
|
+
* deep native descendant straight back to its root subject. */
|
|
247
|
+
readonly rootProcessInstanceKey?: string;
|
|
232
248
|
}
|
|
233
249
|
|
|
234
250
|
/**
|
|
@@ -247,6 +263,24 @@ export interface UserTaskSummary {
|
|
|
247
263
|
readonly formKey?: string;
|
|
248
264
|
/** An external form reference (a form linked outside the deployment), if any. */
|
|
249
265
|
readonly externalFormReference?: string;
|
|
266
|
+
/** The key of the process instance this task belongs to. Absent when the engine omits it (same
|
|
267
|
+
* optional/engine-provided convention as `processDefinitionKey`). For a task owned by a native
|
|
268
|
+
* child instance, this is the child's own key — the handle a reduced-capability read path feeds
|
|
269
|
+
* to {@link EngineClient.searchProcessInstances} to resolve the child's parent/root linkage. */
|
|
270
|
+
readonly processInstanceKey?: string;
|
|
271
|
+
/** The key of the *immediate* parent process instance of this task's owning instance — the caller
|
|
272
|
+
* that spawned it as a native child. Absent when the engine's user-task read model omits it (as
|
|
273
|
+
* it does today: the engine surfaces the parent linkage on {@link ProcessInstanceSnapshot}, so a
|
|
274
|
+
* caller resolves it via `processInstanceKey` → `searchProcessInstances`). Declared here for
|
|
275
|
+
* parity with the process-instance snapshot and to flow through automatically if the read model
|
|
276
|
+
* starts carrying it. */
|
|
277
|
+
readonly parentProcessInstanceKey?: string;
|
|
278
|
+
/** The key of the *root* process instance of this task's owning instance — the top-level ancestor
|
|
279
|
+
* of the call-activity hierarchy the task lives in. Absent when the engine omits it. Lets a
|
|
280
|
+
* reduced-capability read path (`pollUserTasks` and other reconcile/affordance surfaces)
|
|
281
|
+
* correlate a native-child (e.g. human-escalation grandchild) task straight back to its root
|
|
282
|
+
* subject through the typed seam, with no raw-REST fallback. */
|
|
283
|
+
readonly rootProcessInstanceKey?: string;
|
|
250
284
|
}
|
|
251
285
|
|
|
252
286
|
/**
|
|
@@ -259,6 +293,14 @@ export interface UserTaskFilter {
|
|
|
259
293
|
processInstanceKey?: string;
|
|
260
294
|
assignee?: string;
|
|
261
295
|
candidateGroup?: string;
|
|
296
|
+
/** Select only tasks whose owning instance has this *immediate* parent process instance. Honored
|
|
297
|
+
* server-side by the live gateway where the read model supports it; the in-process testkit
|
|
298
|
+
* applies it client-side against the row's parent linkage (absent today on the user-task read
|
|
299
|
+
* model, so it matches nothing until the engine surfaces it there). */
|
|
300
|
+
parentProcessInstanceKey?: string;
|
|
301
|
+
/** Select only tasks whose owning instance belongs to this *root* process-instance hierarchy — the
|
|
302
|
+
* selector a reduced-capability path uses to fetch every open task under one root subject. */
|
|
303
|
+
rootProcessInstanceKey?: string;
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
/**
|
|
@@ -487,6 +529,76 @@ export interface FormSchema {
|
|
|
487
529
|
readonly schema: Record<string, unknown>;
|
|
488
530
|
}
|
|
489
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Selectors for a variable search: which process instance and/or scope to match, optionally
|
|
534
|
+
* narrowed to a single variable `name`. Used by {@link EngineClient.searchVariables}. An
|
|
535
|
+
* eventually consistent (zero-wait) read; an unset selector matches every variable in scope.
|
|
536
|
+
*/
|
|
537
|
+
export interface VariableFilter {
|
|
538
|
+
processInstanceKey?: string;
|
|
539
|
+
scopeKey?: string;
|
|
540
|
+
name?: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* A single process variable as {@link EngineClient.searchVariables} reports it — the read
|
|
545
|
+
* counterpart to the write-only {@link EngineClient.setVariables}. Mirrors the engine's v2
|
|
546
|
+
* `/variables/search` row: the variable's own `variableKey`, its `name`, its serialized JSON
|
|
547
|
+
* `value` (a string — an object/array value arrives JSON-encoded, so a caller `JSON.parse`s it),
|
|
548
|
+
* the `scopeKey` it is directly defined in (a process-instance key for a process-level variable,
|
|
549
|
+
* an element-instance key for a local one) and the owning `processInstanceKey`. `isTruncated` is
|
|
550
|
+
* `true` when the engine clipped a large `value` for the read model — the caller then knows the
|
|
551
|
+
* `value` is a preview, not the whole payload. This is the only way to read a parked token's
|
|
552
|
+
* business payload (`prKey`, `round`, an escalation `question`/`answer`, `resolution`,
|
|
553
|
+
* `directive`); the ADR 0065 projections carry lifecycle/position, not the payload.
|
|
554
|
+
*/
|
|
555
|
+
export interface VariableSummary {
|
|
556
|
+
readonly variableKey: string;
|
|
557
|
+
readonly name: string;
|
|
558
|
+
readonly value: string;
|
|
559
|
+
readonly scopeKey: string;
|
|
560
|
+
readonly processInstanceKey: string;
|
|
561
|
+
readonly isTruncated: boolean;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Selectors for a job search: which process instance, lifecycle `state`, job `type`, `elementId`,
|
|
566
|
+
* and/or leasing `worker` to match. Used by {@link EngineClient.searchJobs}. `state` is kept a
|
|
567
|
+
* transport-agnostic bare string (the engine's job-state enum is broad and forward-looking — e.g.
|
|
568
|
+
* `CREATED`/`COMPLETED`/`FAILED`/`TIMED_OUT`), so a new engine state flows through without having
|
|
569
|
+
* to be enumerated here. An eventually consistent (zero-wait) read; an unset selector matches
|
|
570
|
+
* every job.
|
|
571
|
+
*/
|
|
572
|
+
export interface JobFilter {
|
|
573
|
+
processInstanceKey?: string;
|
|
574
|
+
state?: string;
|
|
575
|
+
type?: string;
|
|
576
|
+
elementId?: string;
|
|
577
|
+
worker?: string;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* A single job as {@link EngineClient.searchJobs} reports it — mirrors the engine's v2
|
|
582
|
+
* `/jobs/search` row. Carries the `jobKey` a caller passes to
|
|
583
|
+
* {@link EngineClient.updateJobRetries} (the only source of a `jobKey` for a job NOT already
|
|
584
|
+
* behind a `jobNoRetries` incident, which {@link searchIncidents} surfaces), the job `type` and
|
|
585
|
+
* lifecycle `state` (a bare string — see {@link JobFilter}), the owning `processInstanceKey`, and
|
|
586
|
+
* the best-effort diagnostics `worker`/`retries`/`elementId`/`deadline`. The `worker` field is the
|
|
587
|
+
* "is it actually stuck" signal: a `CREATED` job WITH a `worker` set has been leased by an agent,
|
|
588
|
+
* while one with NONE set is merely queued — so `worker` is omitted (absent) when unset. `retries`,
|
|
589
|
+
* `elementId`, and `deadline` are likewise present when the engine reports them, omitted otherwise.
|
|
590
|
+
*/
|
|
591
|
+
export interface JobSummary {
|
|
592
|
+
readonly jobKey: string;
|
|
593
|
+
readonly type: string;
|
|
594
|
+
readonly state: string;
|
|
595
|
+
readonly processInstanceKey: string;
|
|
596
|
+
readonly worker?: string;
|
|
597
|
+
readonly retries?: number;
|
|
598
|
+
readonly elementId?: string;
|
|
599
|
+
readonly deadline?: string;
|
|
600
|
+
}
|
|
601
|
+
|
|
490
602
|
/**
|
|
491
603
|
* EngineClient is the seam onto a Nano engine. The SDK/REST-backed adapter implements
|
|
492
604
|
* it against a live engine; tests implement it in-memory. Core modules depend only on this.
|
|
@@ -562,6 +674,12 @@ export interface EngineClient {
|
|
|
562
674
|
searchProcessInstances(filter?: {
|
|
563
675
|
processInstanceKeys?: string[];
|
|
564
676
|
state?: ProcessInstanceState;
|
|
677
|
+
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
678
|
+
* query for "the native children spawned by this subject". */
|
|
679
|
+
parentProcessInstanceKey?: string;
|
|
680
|
+
/** Select only instances belonging to this *root* process-instance hierarchy — "every instance
|
|
681
|
+
* under this root subject", the deep-descendant correlation query. */
|
|
682
|
+
rootProcessInstanceKey?: string;
|
|
565
683
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
566
684
|
/**
|
|
567
685
|
* Search element instances ("tokens") by process instance, element, and/or lifecycle state
|
|
@@ -616,6 +734,40 @@ export interface EngineClient {
|
|
|
616
734
|
* they answer "how far has this instance progressed", this answers "why is it stuck".
|
|
617
735
|
*/
|
|
618
736
|
searchIncidents(filter?: IncidentFilter): Promise<IncidentSummary[]>;
|
|
737
|
+
/**
|
|
738
|
+
* Search process variables by owning process instance and/or scope, optionally narrowed to a
|
|
739
|
+
* single `name` (`POST /v2/variables/search`). The read counterpart to the write-only
|
|
740
|
+
* {@link setVariables}: it is the only way to see a parked token's business payload (`prKey`,
|
|
741
|
+
* `round`, an escalation `question`/`answer`, `resolution`, `directive`) — the ADR 0065
|
|
742
|
+
* projections carry lifecycle/position, not the payload. An eventually consistent (zero-wait)
|
|
743
|
+
* read; each {@link VariableSummary} carries the variable's serialized JSON `value` (a string)
|
|
744
|
+
* and an `isTruncated` flag set when the engine clipped a large value for the read model.
|
|
745
|
+
*/
|
|
746
|
+
searchVariables(filter?: VariableFilter): Promise<VariableSummary[]>;
|
|
747
|
+
/**
|
|
748
|
+
* Search jobs by owning process instance, lifecycle `state`, job `type`, `elementId`, and/or
|
|
749
|
+
* leasing `worker` (`POST /v2/jobs/search`). This is the "is it actually stuck" read: a
|
|
750
|
+
* `CREATED` job WITH a `worker` set has been leased by an agent, while one with NONE set is
|
|
751
|
+
* merely queued. It is also the on-tool source of a `jobKey` for {@link updateJobRetries} when
|
|
752
|
+
* the job is NOT behind an incident — {@link searchIncidents} only surfaces a `jobKey` for a
|
|
753
|
+
* job that already faulted (a `jobNoRetries` incident), so retrying an un-incidented job would
|
|
754
|
+
* otherwise be impossible without a direct engine-API call. An eventually consistent (zero-wait)
|
|
755
|
+
* read; each {@link JobSummary} carries the `jobKey` plus best-effort `worker`/`retries`/
|
|
756
|
+
* `elementId`/`deadline` diagnostics.
|
|
757
|
+
*/
|
|
758
|
+
searchJobs(filter?: JobFilter): Promise<JobSummary[]>;
|
|
759
|
+
/**
|
|
760
|
+
* Fetch the deployed BPMN XML of a process definition by its `processDefinitionKey`
|
|
761
|
+
* (`GET /v2/process-definitions/{processDefinitionKey}/xml`), or `null` when the key is blank,
|
|
762
|
+
* unknown (a 404), or the definition carries no XML. This is the *deployed* model — the routing
|
|
763
|
+
* source of truth, including its FEEL gateway conditions — so an agent can reason about WHY an
|
|
764
|
+
* instance routed where it did without checking out `resources/processes/*.bpmn` from the app
|
|
765
|
+
* repo. An instance already carries its `processDefinitionKey` (via {@link searchProcessInstances}),
|
|
766
|
+
* so the path is instance → `processDefinitionKey` → deployed XML. A read is treated as absence
|
|
767
|
+
* (`null`) rather than propagating (mirroring {@link getForm}/{@link getElementInstance}), so a
|
|
768
|
+
* caller distinguishes "have it" from "don't" without a try/catch. A zero-wait read.
|
|
769
|
+
*/
|
|
770
|
+
getProcessDefinitionXml(processDefinitionKey: string): Promise<string | null>;
|
|
619
771
|
/**
|
|
620
772
|
* Resolve an open incident by key (`POST /v2/incidents/{incidentKey}/resolution`), unblocking
|
|
621
773
|
* the parked token: a job incident returns the job to the activatable pool (so it must have
|
|
@@ -680,6 +832,9 @@ export const ENGINE_CLIENT_METHODS = [
|
|
|
680
832
|
"searchElementInstanceWaitStates",
|
|
681
833
|
"getElementInstance",
|
|
682
834
|
"searchIncidents",
|
|
835
|
+
"searchVariables",
|
|
836
|
+
"searchJobs",
|
|
837
|
+
"getProcessDefinitionXml",
|
|
683
838
|
"resolveIncident",
|
|
684
839
|
"updateJobRetries",
|
|
685
840
|
"setVariables",
|
|
@@ -37,6 +37,9 @@ const engine: EngineClient = {
|
|
|
37
37
|
searchElementInstanceWaitStates: async () => [],
|
|
38
38
|
getElementInstance: async () => null,
|
|
39
39
|
searchIncidents: async () => [],
|
|
40
|
+
searchVariables: async () => [],
|
|
41
|
+
searchJobs: async () => [],
|
|
42
|
+
getProcessDefinitionXml: async () => null,
|
|
40
43
|
resolveIncident: async () => {},
|
|
41
44
|
updateJobRetries: async () => {},
|
|
42
45
|
setVariables: async () => {},
|
|
@@ -22,6 +22,9 @@ const fakeEngine: EngineClient = {
|
|
|
22
22
|
searchElementInstanceWaitStates: async () => [],
|
|
23
23
|
getElementInstance: async () => null,
|
|
24
24
|
searchIncidents: async () => [],
|
|
25
|
+
searchVariables: async () => [],
|
|
26
|
+
searchJobs: async () => [],
|
|
27
|
+
getProcessDefinitionXml: async () => null,
|
|
25
28
|
resolveIncident: async () => {},
|
|
26
29
|
updateJobRetries: async () => {},
|
|
27
30
|
setVariables: async () => {},
|
|
@@ -35,6 +35,9 @@ const engine: EngineClient = {
|
|
|
35
35
|
searchElementInstanceWaitStates: async () => [],
|
|
36
36
|
getElementInstance: async () => null,
|
|
37
37
|
searchIncidents: async () => [],
|
|
38
|
+
searchVariables: async () => [],
|
|
39
|
+
searchJobs: async () => [],
|
|
40
|
+
getProcessDefinitionXml: async () => null,
|
|
38
41
|
resolveIncident: async () => {},
|
|
39
42
|
updateJobRetries: async () => {},
|
|
40
43
|
setVariables: async () => {},
|
|
@@ -89,6 +89,9 @@ function cancelEngine(opts: {
|
|
|
89
89
|
searchElementInstanceWaitStates: notUsed("searchElementInstanceWaitStates"),
|
|
90
90
|
getElementInstance: notUsed("getElementInstance"),
|
|
91
91
|
searchIncidents: notUsed("searchIncidents"),
|
|
92
|
+
searchVariables: notUsed("searchVariables"),
|
|
93
|
+
searchJobs: notUsed("searchJobs"),
|
|
94
|
+
getProcessDefinitionXml: notUsed("getProcessDefinitionXml"),
|
|
92
95
|
resolveIncident: notUsed("resolveIncident"),
|
|
93
96
|
updateJobRetries: notUsed("updateJobRetries"),
|
|
94
97
|
setVariables: notUsed("setVariables"),
|
|
@@ -25,6 +25,9 @@ class MiniEngine implements EngineClient {
|
|
|
25
25
|
async searchElementInstanceWaitStates(): Promise<[]> { return []; }
|
|
26
26
|
async getElementInstance(): Promise<null> { return null; }
|
|
27
27
|
async searchIncidents(): Promise<never[]> { return []; }
|
|
28
|
+
async searchVariables(): Promise<never[]> { return []; }
|
|
29
|
+
async searchJobs(): Promise<never[]> { return []; }
|
|
30
|
+
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
28
31
|
async resolveIncident(): Promise<void> {}
|
|
29
32
|
async updateJobRetries(): Promise<void> {}
|
|
30
33
|
async setVariables(): Promise<void> {}
|
|
@@ -24,6 +24,9 @@ const noEngine: EngineClient = {
|
|
|
24
24
|
searchElementInstanceWaitStates: async () => [],
|
|
25
25
|
getElementInstance: async () => null,
|
|
26
26
|
searchIncidents: async () => [],
|
|
27
|
+
searchVariables: async () => [],
|
|
28
|
+
searchJobs: async () => [],
|
|
29
|
+
getProcessDefinitionXml: async () => null,
|
|
27
30
|
resolveIncident: async () => {},
|
|
28
31
|
updateJobRetries: async () => {},
|
|
29
32
|
setVariables: async () => {},
|
|
@@ -94,6 +94,9 @@ function fakeEngine(
|
|
|
94
94
|
searchElementInstanceWaitStates: notUsed("searchElementInstanceWaitStates"),
|
|
95
95
|
getElementInstance: notUsed("getElementInstance"),
|
|
96
96
|
searchIncidents: notUsed("searchIncidents"),
|
|
97
|
+
searchVariables: notUsed("searchVariables"),
|
|
98
|
+
searchJobs: notUsed("searchJobs"),
|
|
99
|
+
getProcessDefinitionXml: notUsed("getProcessDefinitionXml"),
|
|
97
100
|
resolveIncident: notUsed("resolveIncident"),
|
|
98
101
|
updateJobRetries: notUsed("updateJobRetries"),
|
|
99
102
|
setVariables: notUsed("setVariables"),
|
|
@@ -91,6 +91,9 @@ function fakeEngine(overrides: Partial<EngineClient> = {}): EngineClient {
|
|
|
91
91
|
searchElementInstanceWaitStates: async () => [],
|
|
92
92
|
getElementInstance: async () => null,
|
|
93
93
|
searchIncidents: async () => [],
|
|
94
|
+
searchVariables: async () => [],
|
|
95
|
+
searchJobs: async () => [],
|
|
96
|
+
getProcessDefinitionXml: async () => null,
|
|
94
97
|
resolveIncident: async () => {},
|
|
95
98
|
updateJobRetries: async () => {},
|
|
96
99
|
setVariables: async () => {},
|
|
@@ -565,6 +568,9 @@ test("the framework debug tools appear and return engine truth", async () => {
|
|
|
565
568
|
assert.ok(names.includes("urban_debug_search_user_tasks"));
|
|
566
569
|
assert.ok(names.includes("urban_debug_instance_state"));
|
|
567
570
|
assert.ok(names.includes("urban_debug_open_user_tasks"));
|
|
571
|
+
assert.ok(names.includes("urban_debug_search_variables"));
|
|
572
|
+
assert.ok(names.includes("urban_debug_search_jobs"));
|
|
573
|
+
assert.ok(names.includes("urban_debug_get_process_definition_xml"));
|
|
568
574
|
|
|
569
575
|
const call = await rpc(router, session, "tools/call", {
|
|
570
576
|
name: "urban_debug_search_process_instances",
|
|
@@ -574,6 +580,97 @@ test("the framework debug tools appear and return engine truth", async () => {
|
|
|
574
580
|
assert.deepEqual(JSON.parse(text), [snapshot]);
|
|
575
581
|
});
|
|
576
582
|
|
|
583
|
+
test("urban_debug_search_variables / search_jobs / get_process_definition_xml are read tools returning engine truth", async () => {
|
|
584
|
+
const variable = {
|
|
585
|
+
variableKey: "v-1",
|
|
586
|
+
name: "prKey",
|
|
587
|
+
value: '"owner/repo#7"',
|
|
588
|
+
scopeKey: "pi-1",
|
|
589
|
+
processInstanceKey: "pi-1",
|
|
590
|
+
isTruncated: false,
|
|
591
|
+
};
|
|
592
|
+
const job = {
|
|
593
|
+
jobKey: "j-1",
|
|
594
|
+
type: "senior:pr-review",
|
|
595
|
+
state: "CREATED",
|
|
596
|
+
processInstanceKey: "pi-1",
|
|
597
|
+
retries: 3,
|
|
598
|
+
elementId: "review",
|
|
599
|
+
};
|
|
600
|
+
const seenVarFilters: unknown[] = [];
|
|
601
|
+
const seenJobFilters: unknown[] = [];
|
|
602
|
+
const seenXmlKeys: string[] = [];
|
|
603
|
+
const engine = fakeEngine({
|
|
604
|
+
searchVariables: async (filter) => {
|
|
605
|
+
seenVarFilters.push(filter);
|
|
606
|
+
return [variable];
|
|
607
|
+
},
|
|
608
|
+
searchJobs: async (filter) => {
|
|
609
|
+
seenJobFilters.push(filter);
|
|
610
|
+
return [job];
|
|
611
|
+
},
|
|
612
|
+
getProcessDefinitionXml: async (key) => {
|
|
613
|
+
seenXmlKeys.push(key);
|
|
614
|
+
return key === "pd-1" ? "<bpmn:definitions/>" : null;
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
const { router } = buildHarness({ engine });
|
|
618
|
+
const session = await connect(router);
|
|
619
|
+
|
|
620
|
+
// All three tools are read tools — they serve on loopback with no credential.
|
|
621
|
+
const vars = await rpc(router, session, "tools/call", {
|
|
622
|
+
name: "urban_debug_search_variables",
|
|
623
|
+
arguments: { processInstanceKey: "pi-1", name: "prKey" },
|
|
624
|
+
});
|
|
625
|
+
assert.notEqual(vars.result?.isError, true, "search_variables is a read tool (no credential needed)");
|
|
626
|
+
assert.deepEqual(JSON.parse(toolContentText(vars.result)), [variable]);
|
|
627
|
+
assert.deepEqual(seenVarFilters, [{ processInstanceKey: "pi-1", name: "prKey" }]);
|
|
628
|
+
|
|
629
|
+
const jobs = await rpc(router, session, "tools/call", {
|
|
630
|
+
name: "urban_debug_search_jobs",
|
|
631
|
+
arguments: { processInstanceKey: "pi-1", state: "CREATED" },
|
|
632
|
+
});
|
|
633
|
+
assert.notEqual(jobs.result?.isError, true, "search_jobs is a read tool (no credential needed)");
|
|
634
|
+
assert.deepEqual(JSON.parse(toolContentText(jobs.result)), [job]);
|
|
635
|
+
assert.deepEqual(seenJobFilters, [{ processInstanceKey: "pi-1", state: "CREATED" }]);
|
|
636
|
+
|
|
637
|
+
const xml = await rpc(router, session, "tools/call", {
|
|
638
|
+
name: "urban_debug_get_process_definition_xml",
|
|
639
|
+
arguments: { processDefinitionKey: "pd-1" },
|
|
640
|
+
});
|
|
641
|
+
assert.notEqual(xml.result?.isError, true, "get_process_definition_xml is a read tool");
|
|
642
|
+
// The deployed XML is a raw string, surfaced verbatim (not JSON-wrapped).
|
|
643
|
+
assert.equal(toolContentText(xml.result), "<bpmn:definitions/>");
|
|
644
|
+
assert.deepEqual(seenXmlKeys, ["pd-1"]);
|
|
645
|
+
|
|
646
|
+
// An unknown key returns typed-absent (null) rather than erroring.
|
|
647
|
+
const absent = await rpc(router, session, "tools/call", {
|
|
648
|
+
name: "urban_debug_get_process_definition_xml",
|
|
649
|
+
arguments: { processDefinitionKey: "nope" },
|
|
650
|
+
});
|
|
651
|
+
assert.equal(toolContentText(absent.result), "null");
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("the three new debug read tools expose self-contained ($ref-free) input schemas", async () => {
|
|
655
|
+
const { router } = buildHarness({ engine: fakeEngine() });
|
|
656
|
+
const session = await connect(router);
|
|
657
|
+
const list = await rpc(router, session, "tools/list", {});
|
|
658
|
+
const tools = list.result?.tools;
|
|
659
|
+
assert.ok(Array.isArray(tools));
|
|
660
|
+
const names = tools.map((t) => Reflect.get(t, "name"));
|
|
661
|
+
for (const name of [
|
|
662
|
+
"urban_debug_search_variables",
|
|
663
|
+
"urban_debug_search_jobs",
|
|
664
|
+
"urban_debug_get_process_definition_xml",
|
|
665
|
+
]) {
|
|
666
|
+
assert.ok(names.includes(name), `${name} must appear in tools/list`);
|
|
667
|
+
const tool: unknown = tools[names.indexOf(name)];
|
|
668
|
+
const schema: unknown = Reflect.get(tool ?? {}, "inputSchema");
|
|
669
|
+
assert.equal(Reflect.get(schema ?? {}, "type"), "object", `${name} schema is an object`);
|
|
670
|
+
assert.ok(!JSON.stringify(schema).includes("$ref"), `${name} schema must be $ref-free`);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
577
674
|
test("an app operationId in the reserved urban_debug_ namespace can't shadow a framework tool", async () => {
|
|
578
675
|
// A `urban_debug_*` operationId is a legal safe path segment, so an app could declare one. Because
|
|
579
676
|
// `CallTool` resolves framework debug tools before app ops, an unreserved namespace would let such
|
|
@@ -69,10 +69,12 @@ import type {
|
|
|
69
69
|
HttpRequest,
|
|
70
70
|
HttpResponse,
|
|
71
71
|
IncidentFilter,
|
|
72
|
+
JobFilter,
|
|
72
73
|
ProcessInstanceState,
|
|
73
74
|
SqliteDb,
|
|
74
75
|
UserTaskFilter,
|
|
75
76
|
UserTaskState,
|
|
77
|
+
VariableFilter,
|
|
76
78
|
WaitStateType,
|
|
77
79
|
} from "../host.ts";
|
|
78
80
|
import { presentFormIdentifier } from "../host.ts";
|
|
@@ -640,6 +642,70 @@ function buildDebugTools(app: AppApi): DebugTool[] {
|
|
|
640
642
|
return app.engine.searchIncidents(filter);
|
|
641
643
|
},
|
|
642
644
|
},
|
|
645
|
+
{
|
|
646
|
+
name: `${DEBUG_PREFIX}search_variables`,
|
|
647
|
+
description:
|
|
648
|
+
"Engine truth: search process variables by process instance and/or scope, optionally by name — the read counterpart to set_variables. Reads only, eventually consistent. This is the only way to see a parked token's business payload (prKey, round, an escalation question/answer, resolution, directive); the projections carry lifecycle/position, not the payload. Each result carries the variableKey/name/serialized-JSON value/scopeKey/processInstanceKey/isTruncated.",
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
processInstanceKey: OPTIONAL_STRING,
|
|
653
|
+
scopeKey: OPTIONAL_STRING,
|
|
654
|
+
name: OPTIONAL_STRING,
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
run: (args) => {
|
|
658
|
+
const filter: VariableFilter = {};
|
|
659
|
+
const pik = readPresentString(args, "processInstanceKey");
|
|
660
|
+
if (pik) filter.processInstanceKey = pik;
|
|
661
|
+
const scopeKey = readPresentString(args, "scopeKey");
|
|
662
|
+
if (scopeKey) filter.scopeKey = scopeKey;
|
|
663
|
+
const name = readPresentString(args, "name");
|
|
664
|
+
if (name) filter.name = name;
|
|
665
|
+
return app.engine.searchVariables(filter);
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: `${DEBUG_PREFIX}search_jobs`,
|
|
670
|
+
description:
|
|
671
|
+
"Engine truth: search jobs by process instance, state, and/or type/elementId/worker. Reads only, eventually consistent. The 'is it actually stuck?' test: a CREATED job WITH a worker set has been leased by an agent, one with NONE set is merely queued. This is also the on-tool source of a jobKey for retry_job (the only other source is a jobNoRetries incident via search_incidents). Each result carries jobKey/type/state/processInstanceKey and the best-effort worker/retries/elementId/deadline.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
type: "object",
|
|
674
|
+
properties: {
|
|
675
|
+
processInstanceKey: OPTIONAL_STRING,
|
|
676
|
+
state: OPTIONAL_STRING,
|
|
677
|
+
type: OPTIONAL_STRING,
|
|
678
|
+
elementId: OPTIONAL_STRING,
|
|
679
|
+
worker: OPTIONAL_STRING,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
run: (args) => {
|
|
683
|
+
const filter: JobFilter = {};
|
|
684
|
+
const pik = readPresentString(args, "processInstanceKey");
|
|
685
|
+
if (pik) filter.processInstanceKey = pik;
|
|
686
|
+
const state = readPresentString(args, "state");
|
|
687
|
+
if (state) filter.state = state;
|
|
688
|
+
const type = readPresentString(args, "type");
|
|
689
|
+
if (type) filter.type = type;
|
|
690
|
+
const elementId = readPresentString(args, "elementId");
|
|
691
|
+
if (elementId) filter.elementId = elementId;
|
|
692
|
+
const worker = readPresentString(args, "worker");
|
|
693
|
+
if (worker) filter.worker = worker;
|
|
694
|
+
return app.engine.searchJobs(filter);
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: `${DEBUG_PREFIX}get_process_definition_xml`,
|
|
699
|
+
description:
|
|
700
|
+
"Engine truth: return the deployed BPMN XML of a process definition by key (the routing source of truth, with its FEEL gateway conditions) — reason about WHY an instance routed where it did without a repo checkout. Reads only; null when the key is unknown. An instance carries its processDefinitionKey via search_process_instances, so the path is instance → processDefinitionKey → deployed XML; pair with get_element_instance to locate the parked element.",
|
|
701
|
+
inputSchema: {
|
|
702
|
+
type: "object",
|
|
703
|
+
properties: { processDefinitionKey: OPTIONAL_STRING },
|
|
704
|
+
required: ["processDefinitionKey"],
|
|
705
|
+
},
|
|
706
|
+
run: (args) =>
|
|
707
|
+
app.engine.getProcessDefinitionXml(requireString(args, "processDefinitionKey")),
|
|
708
|
+
},
|
|
643
709
|
{
|
|
644
710
|
name: `${DEBUG_PREFIX}cancel_instance`,
|
|
645
711
|
description: "MUTATING: cancel a running process instance. Requires the shared-secret guard.",
|
|
@@ -52,6 +52,9 @@ function fakeEngine(): { engine: EngineClient; calls: FakeEngineCalls } {
|
|
|
52
52
|
async searchElementInstanceWaitStates() { return []; },
|
|
53
53
|
async getElementInstance() { return null; },
|
|
54
54
|
async searchIncidents() { return []; },
|
|
55
|
+
async searchVariables() { return []; },
|
|
56
|
+
async searchJobs() { return []; },
|
|
57
|
+
async getProcessDefinitionXml() { return null; },
|
|
55
58
|
async resolveIncident() {},
|
|
56
59
|
async updateJobRetries() {},
|
|
57
60
|
async setVariables() {},
|
|
@@ -38,6 +38,9 @@ function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurf
|
|
|
38
38
|
searchElementInstanceWaitStates: async () => [],
|
|
39
39
|
getElementInstance: async () => null,
|
|
40
40
|
searchIncidents: async () => [],
|
|
41
|
+
searchVariables: async () => [],
|
|
42
|
+
searchJobs: async () => [],
|
|
43
|
+
getProcessDefinitionXml: async () => null,
|
|
41
44
|
resolveIncident: async () => {},
|
|
42
45
|
updateJobRetries: async () => {},
|
|
43
46
|
setVariables: async () => {},
|
|
@@ -61,6 +64,9 @@ const fakeEngine: EngineClient = {
|
|
|
61
64
|
searchElementInstanceWaitStates: async () => [],
|
|
62
65
|
getElementInstance: async () => null,
|
|
63
66
|
searchIncidents: async () => [],
|
|
67
|
+
searchVariables: async () => [],
|
|
68
|
+
searchJobs: async () => [],
|
|
69
|
+
getProcessDefinitionXml: async () => null,
|
|
64
70
|
resolveIncident: async () => {},
|
|
65
71
|
updateJobRetries: async () => {},
|
|
66
72
|
setVariables: async () => {},
|
|
@@ -39,6 +39,9 @@ class MiniEngine implements EngineClient {
|
|
|
39
39
|
async searchElementInstanceWaitStates(): Promise<[]> { return []; }
|
|
40
40
|
async getElementInstance(): Promise<null> { return null; }
|
|
41
41
|
async searchIncidents(): Promise<never[]> { return []; }
|
|
42
|
+
async searchVariables(): Promise<never[]> { return []; }
|
|
43
|
+
async searchJobs(): Promise<never[]> { return []; }
|
|
44
|
+
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
42
45
|
async resolveIncident(): Promise<void> {}
|
|
43
46
|
async updateJobRetries(): Promise<void> {}
|
|
44
47
|
async setVariables(): Promise<void> {}
|
|
@@ -36,6 +36,9 @@ class MiniEngine implements EngineClient {
|
|
|
36
36
|
async searchElementInstanceWaitStates(): Promise<[]> { return []; }
|
|
37
37
|
async getElementInstance(): Promise<null> { return null; }
|
|
38
38
|
async searchIncidents(): Promise<never[]> { return []; }
|
|
39
|
+
async searchVariables(): Promise<never[]> { return []; }
|
|
40
|
+
async searchJobs(): Promise<never[]> { return []; }
|
|
41
|
+
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
39
42
|
async resolveIncident(): Promise<void> {}
|
|
40
43
|
async updateJobRetries(): Promise<void> {}
|
|
41
44
|
async setVariables(): Promise<void> {}
|
|
@@ -44,6 +44,15 @@ class NoopEngine implements EngineClient {
|
|
|
44
44
|
async searchIncidents() {
|
|
45
45
|
return [];
|
|
46
46
|
}
|
|
47
|
+
async searchVariables() {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
async searchJobs() {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
async getProcessDefinitionXml() {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
47
56
|
async resolveIncident() {}
|
|
48
57
|
async updateJobRetries() {}
|
|
49
58
|
async setVariables() {}
|
|
@@ -65,6 +65,15 @@ class FakeEngine implements EngineClient {
|
|
|
65
65
|
async searchIncidents() {
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
68
|
+
async searchVariables() {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
async searchJobs() {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
async getProcessDefinitionXml() {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
68
77
|
async resolveIncident() {}
|
|
69
78
|
async updateJobRetries() {}
|
|
70
79
|
async setVariables() {}
|