@nanobpm/urban 0.88.0 → 0.89.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 +105 -1
- package/dist/runtime/core/host.js +3 -0
- package/dist/runtime/core/modules/mcp.js +68 -0
- package/dist/runtime/core/modules/runtime.gen.js +1 -1
- package/dist/runtime/engine/nanosdk.d.ts +30 -1
- package/dist/runtime/engine/nanosdk.js +163 -1
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/runtime/browser/runtime.browser.js +77 -1
- package/src/runtime/browser/runtime.browser.test.ts +126 -1
- package/src/runtime/core/host.ts +111 -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/runtime.gen.ts +1 -1
- 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 +12 -0
- package/src/runtime/engine/nanosdk.ts +194 -1
- package/src/runtime/index.ts +4 -0
|
@@ -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() {},
|