@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.
Files changed (30) hide show
  1. package/dist/runtime/core/host.d.ts +105 -1
  2. package/dist/runtime/core/host.js +3 -0
  3. package/dist/runtime/core/modules/mcp.js +68 -0
  4. package/dist/runtime/core/modules/runtime.gen.js +1 -1
  5. package/dist/runtime/engine/nanosdk.d.ts +30 -1
  6. package/dist/runtime/engine/nanosdk.js +163 -1
  7. package/dist/runtime/index.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/runtime/browser/runtime.browser.js +77 -1
  10. package/src/runtime/browser/runtime.browser.test.ts +126 -1
  11. package/src/runtime/core/host.ts +111 -0
  12. package/src/runtime/core/modules/actions.test.ts +3 -0
  13. package/src/runtime/core/modules/agent.test.ts +3 -0
  14. package/src/runtime/core/modules/api.test.ts +3 -0
  15. package/src/runtime/core/modules/cancel.test.ts +3 -0
  16. package/src/runtime/core/modules/connectors.test.ts +3 -0
  17. package/src/runtime/core/modules/datasource.migrations.test.ts +3 -0
  18. package/src/runtime/core/modules/instance-tracking.test.ts +3 -0
  19. package/src/runtime/core/modules/mcp.test.ts +97 -0
  20. package/src/runtime/core/modules/mcp.ts +66 -0
  21. package/src/runtime/core/modules/pages.test.ts +3 -0
  22. package/src/runtime/core/modules/runtime.gen.ts +1 -1
  23. package/src/runtime/core/modules/surfaces.test.ts +6 -0
  24. package/src/runtime/core/modules/triggers.test.ts +3 -0
  25. package/src/runtime/core/modules/workers.test.ts +3 -0
  26. package/src/runtime/core/runtime.extensions.test.ts +9 -0
  27. package/src/runtime/core/runtime.test.ts +9 -0
  28. package/src/runtime/engine/nanosdk.test.ts +12 -0
  29. package/src/runtime/engine/nanosdk.ts +194 -1
  30. 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() {},