@nanobpm/urban 0.92.0 → 0.93.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 +124 -1
- package/dist/runtime/core/host.js +3 -0
- package/dist/runtime/core/modules/mcp.js +7 -7
- package/dist/runtime/engine/nanosdk.d.ts +28 -1
- package/dist/runtime/engine/nanosdk.js +262 -0
- package/dist/runtime/index.d.ts +2 -1
- package/package.json +2 -1
- package/src/runtime/core/host.ts +146 -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 +67 -11
- package/src/runtime/core/modules/mcp.ts +7 -6
- 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 +209 -0
- package/src/runtime/engine/nanosdk.ts +305 -0
- package/src/runtime/index.ts +16 -0
package/src/runtime/core/host.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
// written against these interfaces and MUST NOT import `node:*` or reference `Deno`.
|
|
4
4
|
// The adapters/ directory supplies concrete implementations.
|
|
5
5
|
|
|
6
|
+
// The engine-native AgentHistory read shapes ({@link AgentHistoryRecord}) reuse the
|
|
7
|
+
// Camunda `AgentHistoryRecordValue`-parity types the transcript store already models
|
|
8
|
+
// (roles, content blocks, per-turn metrics, tool calls) rather than forking a second
|
|
9
|
+
// projection of the same conversation grammar (No Drift Surfaces). This is a type-only
|
|
10
|
+
// import — fully erased at runtime — so `core/` stays free of any concrete host/engine
|
|
11
|
+
// module load, exactly as the rule above requires.
|
|
12
|
+
import type {
|
|
13
|
+
TranscriptContentBlock,
|
|
14
|
+
TranscriptToolCall,
|
|
15
|
+
TranscriptTurnMetrics,
|
|
16
|
+
TranscriptTurnRole,
|
|
17
|
+
} from "@nanobpm/agentic/transcript";
|
|
18
|
+
|
|
6
19
|
/** A minimal HTTP request as seen by a mounted surface/trigger handler. */
|
|
7
20
|
export interface HttpRequest {
|
|
8
21
|
method: string;
|
|
@@ -603,6 +616,100 @@ export interface JobSummary {
|
|
|
603
616
|
readonly deadline?: string;
|
|
604
617
|
}
|
|
605
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Aggregated metrics for an agent instance across all of its model calls — the
|
|
621
|
+
* `AgentInstanceMetrics` the engine rolls up on the instance itself (distinct from the
|
|
622
|
+
* per-turn {@link TranscriptTurnMetrics} carried on an {@link AgentHistoryRecord}). Total
|
|
623
|
+
* `inputTokens`/`outputTokens` consumed, and the count of `modelCalls`/`toolCalls` made.
|
|
624
|
+
*/
|
|
625
|
+
export interface AgentInstanceMetrics {
|
|
626
|
+
readonly inputTokens: number;
|
|
627
|
+
readonly outputTokens: number;
|
|
628
|
+
readonly modelCalls: number;
|
|
629
|
+
readonly toolCalls: number;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* A single engine-native agent instance as {@link EngineClient.searchAgentInstances} /
|
|
634
|
+
* {@link EngineClient.getAgentInstance} report it — the projection of the engine's
|
|
635
|
+
* `AgentInstanceResult`. Carries the `agentInstanceKey` a caller passes to
|
|
636
|
+
* {@link EngineClient.searchAgentInstanceHistory}, the lifecycle `status` (a bare string —
|
|
637
|
+
* the engine's `AgentInstanceStatusEnum` is broad: `INITIALIZING`/`THINKING`/`TOOL_CALLING`/
|
|
638
|
+
* `IDLE`/`COMPLETED`/…), the owning `processInstanceKey`, and best-effort diagnostics
|
|
639
|
+
* (`elementId`, the owning `elementInstanceKeys`, the process-definition linkage, aggregated
|
|
640
|
+
* `metrics`, and the creation/update/completion timestamps). Optional fields are omitted when
|
|
641
|
+
* the engine does not report them.
|
|
642
|
+
*/
|
|
643
|
+
export interface AgentInstanceSummary {
|
|
644
|
+
readonly agentInstanceKey: string;
|
|
645
|
+
readonly status: string;
|
|
646
|
+
readonly processInstanceKey: string;
|
|
647
|
+
readonly elementId?: string;
|
|
648
|
+
readonly elementInstanceKeys?: readonly string[];
|
|
649
|
+
readonly rootProcessInstanceKey?: string;
|
|
650
|
+
readonly processDefinitionKey?: string;
|
|
651
|
+
readonly processDefinitionId?: string;
|
|
652
|
+
readonly metrics?: AgentInstanceMetrics;
|
|
653
|
+
readonly creationDate?: string;
|
|
654
|
+
readonly lastUpdatedDate?: string;
|
|
655
|
+
readonly completionDate?: string;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* A single agent-instance history item as {@link EngineClient.searchAgentInstanceHistory}
|
|
660
|
+
* reports it — the projection of the engine's `AgentInstanceHistoryItemResult`, i.e. one
|
|
661
|
+
* Camunda `AgentHistoryRecordValue`. Its conversation grammar (`role`, `content` blocks,
|
|
662
|
+
* `toolCalls`, per-turn `metrics`) reuses the `@nanobpm/agentic/transcript` parity types the
|
|
663
|
+
* transcript store already models, so the engine-read seam and the transcript store project
|
|
664
|
+
* the same shape (No Drift Surfaces). `historyItemKey` is the stable, creation-ordered
|
|
665
|
+
* identity; `loopIteration` is the agent-loop counter (one LLM call + its tool dispatches +
|
|
666
|
+
* their results share an iteration); `commitStatus` is the engine's `COMMITTED`/`PENDING`/
|
|
667
|
+
* `DISCARDED` flag (a bare string). `metrics` is present only when the engine recorded per-call
|
|
668
|
+
* metrics for the item; `elementInstanceKey`/`jobKey`/`producedAt` are best-effort diagnostics,
|
|
669
|
+
* omitted when unreported.
|
|
670
|
+
*/
|
|
671
|
+
export interface AgentHistoryRecord {
|
|
672
|
+
readonly historyItemKey: string;
|
|
673
|
+
readonly agentInstanceKey: string;
|
|
674
|
+
readonly loopIteration: number;
|
|
675
|
+
readonly role: TranscriptTurnRole;
|
|
676
|
+
readonly content: readonly TranscriptContentBlock[];
|
|
677
|
+
readonly toolCalls: readonly TranscriptToolCall[];
|
|
678
|
+
readonly metrics?: TranscriptTurnMetrics;
|
|
679
|
+
readonly commitStatus: string;
|
|
680
|
+
readonly elementInstanceKey?: string;
|
|
681
|
+
readonly jobKey?: string;
|
|
682
|
+
readonly producedAt?: string;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Selectors for {@link EngineClient.searchAgentInstances}. Every field is optional; an
|
|
686
|
+
* omitted/blank selector is not applied (No Drift Surfaces — the same presence rule the key
|
|
687
|
+
* selectors elsewhere in this seam use). */
|
|
688
|
+
export interface AgentInstanceFilter {
|
|
689
|
+
/** Only agent instances owned by this process instance. */
|
|
690
|
+
readonly processInstanceKey?: string;
|
|
691
|
+
/** Only agent instances belonging to this *root* process-instance hierarchy. */
|
|
692
|
+
readonly rootProcessInstanceKey?: string;
|
|
693
|
+
/** Only agent instances in this lifecycle status (the engine's `AgentInstanceStatusEnum`). */
|
|
694
|
+
readonly status?: string;
|
|
695
|
+
/** Only agent instances owned by this BPMN element (the AI-agent task / ad-hoc sub-process). */
|
|
696
|
+
readonly elementId?: string;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Selectors for {@link EngineClient.searchAgentInstanceHistory} (the `agentInstanceKey` is a
|
|
700
|
+
* required positional argument, not a filter field). Every field is optional. Note the engine
|
|
701
|
+
* defaults an unfiltered history search to `COMMITTED` items only. */
|
|
702
|
+
export interface AgentHistoryFilter {
|
|
703
|
+
/** Only history items with this conversation role. Note that `TranscriptTurnRole` is the wider
|
|
704
|
+
* transcript union: only the engine-side subset `"USER" | "ASSISTANT" | "TOOL_RESULT"` is
|
|
705
|
+
* applied as a filter — `"CONFIGURATION"`/`"UNSPECIFIED"` are ignored (not forwarded). */
|
|
706
|
+
readonly role?: TranscriptTurnRole;
|
|
707
|
+
/** Only history items produced in this agent-loop iteration. */
|
|
708
|
+
readonly loopIteration?: number;
|
|
709
|
+
/** Only history items produced under this element instance. */
|
|
710
|
+
readonly elementInstanceKey?: string;
|
|
711
|
+
}
|
|
712
|
+
|
|
606
713
|
/**
|
|
607
714
|
* EngineClient is the seam onto a Nano engine. The SDK/REST-backed adapter implements
|
|
608
715
|
* it against a live engine; tests implement it in-memory. Core modules depend only on this.
|
|
@@ -772,6 +879,42 @@ export interface EngineClient {
|
|
|
772
879
|
* caller distinguishes "have it" from "don't" without a try/catch. A zero-wait read.
|
|
773
880
|
*/
|
|
774
881
|
getProcessDefinitionXml(processDefinitionKey: string): Promise<string | null>;
|
|
882
|
+
/**
|
|
883
|
+
* Search engine-native agent instances (`POST /agent-instances/search`), optionally narrowed
|
|
884
|
+
* by owning `processInstanceKey`/`rootProcessInstanceKey`, lifecycle `status`, or `elementId`.
|
|
885
|
+
* The read entry point onto the engine's AgentInstance/AgentHistory model: an
|
|
886
|
+
* {@link AgentInstanceSummary} carries the `agentInstanceKey` a caller passes to
|
|
887
|
+
* {@link searchAgentInstanceHistory} to read that instance's conversation. An eventually
|
|
888
|
+
* consistent (zero-wait) read; each malformed row (no `agentInstanceKey`/`processInstanceKey`)
|
|
889
|
+
* is dropped rather than surfaced.
|
|
890
|
+
*/
|
|
891
|
+
searchAgentInstances(filter?: AgentInstanceFilter): Promise<AgentInstanceSummary[]>;
|
|
892
|
+
/**
|
|
893
|
+
* Search one agent instance's conversation history
|
|
894
|
+
* (`POST /agent-instances/{agentInstanceKey}/history/search`) — the ordered
|
|
895
|
+
* {@link AgentHistoryRecord}s (turns) the engine retains for `agentInstanceKey`, optionally
|
|
896
|
+
* narrowed by `role`/`loopIteration`/`elementInstanceKey`. This is the production read path
|
|
897
|
+
* onto engine AgentHistory the historical-transcript + metrics consumer builds on
|
|
898
|
+
* (nanobpm/nano-workforce#747). Each record's conversation grammar reuses the
|
|
899
|
+
* `@nanobpm/agentic/transcript` parity shapes (roles, content blocks, per-turn metrics, tool
|
|
900
|
+
* calls). Note the engine defaults an unfiltered search to `COMMITTED` items only. A blank
|
|
901
|
+
* `agentInstanceKey` addresses no instance, so it returns the empty list rather than issuing a
|
|
902
|
+
* request with an empty path segment. An eventually consistent (zero-wait) read; malformed rows
|
|
903
|
+
* are dropped.
|
|
904
|
+
*/
|
|
905
|
+
searchAgentInstanceHistory(
|
|
906
|
+
agentInstanceKey: string,
|
|
907
|
+
filter?: AgentHistoryFilter,
|
|
908
|
+
): Promise<AgentHistoryRecord[]>;
|
|
909
|
+
/**
|
|
910
|
+
* Fetch a single agent instance by its key (`GET /agent-instances/{agentInstanceKey}`), or
|
|
911
|
+
* `null` when it cannot be resolved — the key is blank, no such agent instance exists (a 404),
|
|
912
|
+
* or the fetch otherwise fails. A read is treated as absence rather than propagating (mirroring
|
|
913
|
+
* {@link getForm}/{@link getElementInstance}/{@link getProcessDefinitionXml}), so a caller
|
|
914
|
+
* distinguishes "have it" from "don't" without a try/catch. A zero-wait read returning the same
|
|
915
|
+
* {@link AgentInstanceSummary} shape as {@link searchAgentInstances}.
|
|
916
|
+
*/
|
|
917
|
+
getAgentInstance(agentInstanceKey: string): Promise<AgentInstanceSummary | null>;
|
|
775
918
|
/**
|
|
776
919
|
* Resolve an open incident by key (`POST /v2/incidents/{incidentKey}/resolution`), unblocking
|
|
777
920
|
* the parked token: a job incident returns the job to the activatable pool (so it must have
|
|
@@ -839,6 +982,9 @@ export const ENGINE_CLIENT_METHODS = [
|
|
|
839
982
|
"searchVariables",
|
|
840
983
|
"searchJobs",
|
|
841
984
|
"getProcessDefinitionXml",
|
|
985
|
+
"searchAgentInstances",
|
|
986
|
+
"searchAgentInstanceHistory",
|
|
987
|
+
"getAgentInstance",
|
|
842
988
|
"resolveIncident",
|
|
843
989
|
"updateJobRetries",
|
|
844
990
|
"setVariables",
|
|
@@ -40,6 +40,9 @@ const engine: EngineClient = {
|
|
|
40
40
|
searchVariables: async () => [],
|
|
41
41
|
searchJobs: async () => [],
|
|
42
42
|
getProcessDefinitionXml: async () => null,
|
|
43
|
+
searchAgentInstances: async () => [],
|
|
44
|
+
searchAgentInstanceHistory: async () => [],
|
|
45
|
+
getAgentInstance: async () => null,
|
|
43
46
|
resolveIncident: async () => {},
|
|
44
47
|
updateJobRetries: async () => {},
|
|
45
48
|
setVariables: async () => {},
|
|
@@ -25,6 +25,9 @@ const fakeEngine: EngineClient = {
|
|
|
25
25
|
searchVariables: async () => [],
|
|
26
26
|
searchJobs: async () => [],
|
|
27
27
|
getProcessDefinitionXml: async () => null,
|
|
28
|
+
searchAgentInstances: async () => [],
|
|
29
|
+
searchAgentInstanceHistory: async () => [],
|
|
30
|
+
getAgentInstance: async () => null,
|
|
28
31
|
resolveIncident: async () => {},
|
|
29
32
|
updateJobRetries: async () => {},
|
|
30
33
|
setVariables: async () => {},
|
|
@@ -38,6 +38,9 @@ const engine: EngineClient = {
|
|
|
38
38
|
searchVariables: async () => [],
|
|
39
39
|
searchJobs: async () => [],
|
|
40
40
|
getProcessDefinitionXml: async () => null,
|
|
41
|
+
searchAgentInstances: async () => [],
|
|
42
|
+
searchAgentInstanceHistory: async () => [],
|
|
43
|
+
getAgentInstance: async () => null,
|
|
41
44
|
resolveIncident: async () => {},
|
|
42
45
|
updateJobRetries: async () => {},
|
|
43
46
|
setVariables: async () => {},
|
|
@@ -92,6 +92,9 @@ function cancelEngine(opts: {
|
|
|
92
92
|
searchVariables: notUsed("searchVariables"),
|
|
93
93
|
searchJobs: notUsed("searchJobs"),
|
|
94
94
|
getProcessDefinitionXml: notUsed("getProcessDefinitionXml"),
|
|
95
|
+
searchAgentInstances: notUsed("searchAgentInstances"),
|
|
96
|
+
searchAgentInstanceHistory: notUsed("searchAgentInstanceHistory"),
|
|
97
|
+
getAgentInstance: notUsed("getAgentInstance"),
|
|
95
98
|
resolveIncident: notUsed("resolveIncident"),
|
|
96
99
|
updateJobRetries: notUsed("updateJobRetries"),
|
|
97
100
|
setVariables: notUsed("setVariables"),
|
|
@@ -28,6 +28,9 @@ class MiniEngine implements EngineClient {
|
|
|
28
28
|
async searchVariables(): Promise<never[]> { return []; }
|
|
29
29
|
async searchJobs(): Promise<never[]> { return []; }
|
|
30
30
|
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
31
|
+
async searchAgentInstances(): Promise<never[]> { return []; }
|
|
32
|
+
async searchAgentInstanceHistory(): Promise<never[]> { return []; }
|
|
33
|
+
async getAgentInstance(): Promise<null> { return null; }
|
|
31
34
|
async resolveIncident(): Promise<void> {}
|
|
32
35
|
async updateJobRetries(): Promise<void> {}
|
|
33
36
|
async setVariables(): Promise<void> {}
|
|
@@ -27,6 +27,9 @@ const noEngine: EngineClient = {
|
|
|
27
27
|
searchVariables: async () => [],
|
|
28
28
|
searchJobs: async () => [],
|
|
29
29
|
getProcessDefinitionXml: async () => null,
|
|
30
|
+
searchAgentInstances: async () => [],
|
|
31
|
+
searchAgentInstanceHistory: async () => [],
|
|
32
|
+
getAgentInstance: async () => null,
|
|
30
33
|
resolveIncident: async () => {},
|
|
31
34
|
updateJobRetries: async () => {},
|
|
32
35
|
setVariables: async () => {},
|
|
@@ -97,6 +97,9 @@ function fakeEngine(
|
|
|
97
97
|
searchVariables: notUsed("searchVariables"),
|
|
98
98
|
searchJobs: notUsed("searchJobs"),
|
|
99
99
|
getProcessDefinitionXml: notUsed("getProcessDefinitionXml"),
|
|
100
|
+
searchAgentInstances: notUsed("searchAgentInstances"),
|
|
101
|
+
searchAgentInstanceHistory: notUsed("searchAgentInstanceHistory"),
|
|
102
|
+
getAgentInstance: notUsed("getAgentInstance"),
|
|
100
103
|
resolveIncident: notUsed("resolveIncident"),
|
|
101
104
|
updateJobRetries: notUsed("updateJobRetries"),
|
|
102
105
|
setVariables: notUsed("setVariables"),
|
|
@@ -29,7 +29,6 @@ import { makeRouter } from "../router.ts";
|
|
|
29
29
|
import { mountApi } from "./api.ts";
|
|
30
30
|
import { makeGateway } from "./gateway.ts";
|
|
31
31
|
import { DataLayer, type ProvisionedSource } from "./datasource.ts";
|
|
32
|
-
import { InstanceStateStore } from "./instance-state-store.ts";
|
|
33
32
|
import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
|
|
34
33
|
import { evictExcessSessions, isLoopbackRequest, missingRequiredArgs, mountMcp, newSessionId, readHeader, readMcpConfig } from "./mcp.ts";
|
|
35
34
|
import { collectMcpToolProjection, diffMcpToolProjection, parseSpec } from "../../../openapi/spec.ts";
|
|
@@ -94,6 +93,9 @@ function fakeEngine(overrides: Partial<EngineClient> = {}): EngineClient {
|
|
|
94
93
|
searchVariables: async () => [],
|
|
95
94
|
searchJobs: async () => [],
|
|
96
95
|
getProcessDefinitionXml: async () => null,
|
|
96
|
+
searchAgentInstances: async () => [],
|
|
97
|
+
searchAgentInstanceHistory: async () => [],
|
|
98
|
+
getAgentInstance: async () => null,
|
|
97
99
|
resolveIncident: async () => {},
|
|
98
100
|
updateJobRetries: async () => {},
|
|
99
101
|
setVariables: async () => {},
|
|
@@ -647,6 +649,9 @@ test("urban_debug_search_variables / search_jobs / get_process_definition_xml ar
|
|
|
647
649
|
seenXmlKeys.push(key);
|
|
648
650
|
return key === "pd-1" ? "<bpmn:definitions/>" : null;
|
|
649
651
|
},
|
|
652
|
+
searchAgentInstances: async () => [],
|
|
653
|
+
searchAgentInstanceHistory: async () => [],
|
|
654
|
+
getAgentInstance: async () => null,
|
|
650
655
|
});
|
|
651
656
|
const { router } = buildHarness({ engine });
|
|
652
657
|
const session = await connect(router);
|
|
@@ -769,13 +774,11 @@ test("the reserved-namespace drop warns once, not on every tools/list and tools/
|
|
|
769
774
|
assert.equal(drops.length, 1, "the reserved-namespace drop must warn exactly once across many requests");
|
|
770
775
|
});
|
|
771
776
|
|
|
772
|
-
test("
|
|
777
|
+
test("urban_debug_open_user_tasks reads the ADR 0065 canonical projection", async () => {
|
|
773
778
|
const dir = await mkdtemp(join(tmpdir(), "urban-mcp-proj-"));
|
|
774
779
|
const host = createNodeHost({ cwd: dir, log: () => {} });
|
|
775
780
|
const db: SqliteDb = host.openSqlite(join(dir, "test.db"));
|
|
776
781
|
try {
|
|
777
|
-
new InstanceStateStore(db, { clock: { now: () => 0 } }).ensureSchema();
|
|
778
|
-
new InstanceStateStore(db, { clock: { now: () => 0 } }).recordState("pi-7", "ACTIVE");
|
|
779
782
|
const openStore = new OpenUserTasksStore(db, { clock: { now: () => 0 } });
|
|
780
783
|
openStore.ensureSchema();
|
|
781
784
|
openStore.recordOpenTask("pi-7", { userTaskKey: "ut-1", elementId: "Task_A" });
|
|
@@ -792,13 +795,6 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
|
|
|
792
795
|
const { router } = buildHarness({ data });
|
|
793
796
|
const session = await connect(router);
|
|
794
797
|
|
|
795
|
-
const stateCall = await rpc(router, session, "tools/call", {
|
|
796
|
-
name: "urban_debug_instance_state",
|
|
797
|
-
arguments: { processInstanceKey: "pi-7" },
|
|
798
|
-
});
|
|
799
|
-
const state = JSON.parse(toolContentText(stateCall.result));
|
|
800
|
-
assert.equal(Reflect.get(state, "state"), "ACTIVE");
|
|
801
|
-
|
|
802
798
|
const tasksCall = await rpc(router, session, "tools/call", {
|
|
803
799
|
name: "urban_debug_open_user_tasks",
|
|
804
800
|
arguments: { processInstanceKey: "pi-7" },
|
|
@@ -812,6 +808,66 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
|
|
|
812
808
|
}
|
|
813
809
|
});
|
|
814
810
|
|
|
811
|
+
test("urban_debug_instance_state returns engine truth for a live (ACTIVE) instance", async () => {
|
|
812
|
+
const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
|
|
813
|
+
const engine = fakeEngine({
|
|
814
|
+
searchProcessInstances: async (filter) => {
|
|
815
|
+
assert.deepEqual(filter?.processInstanceKeys, ["pi-7"]);
|
|
816
|
+
return [snapshot];
|
|
817
|
+
},
|
|
818
|
+
openUserTasks: async () => [],
|
|
819
|
+
});
|
|
820
|
+
const { router } = buildHarness({ engine });
|
|
821
|
+
const session = await connect(router);
|
|
822
|
+
|
|
823
|
+
const call = await rpc(router, session, "tools/call", {
|
|
824
|
+
name: "urban_debug_instance_state",
|
|
825
|
+
arguments: { processInstanceKey: "pi-7" },
|
|
826
|
+
});
|
|
827
|
+
const state = JSON.parse(toolContentText(call.result));
|
|
828
|
+
assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: false });
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("urban_debug_instance_state reports waitingOnHuman from open user tasks", async () => {
|
|
832
|
+
const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
|
|
833
|
+
const engine = fakeEngine({
|
|
834
|
+
searchProcessInstances: async () => [snapshot],
|
|
835
|
+
openUserTasks: async (filter) => {
|
|
836
|
+
assert.equal(filter?.processInstanceKey, "pi-7");
|
|
837
|
+
return [{ userTaskKey: "ut-1", processInstanceKey: "pi-7" }];
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
const { router } = buildHarness({ engine });
|
|
841
|
+
const session = await connect(router);
|
|
842
|
+
|
|
843
|
+
const call = await rpc(router, session, "tools/call", {
|
|
844
|
+
name: "urban_debug_instance_state",
|
|
845
|
+
arguments: { processInstanceKey: "pi-7" },
|
|
846
|
+
});
|
|
847
|
+
const state = JSON.parse(toolContentText(call.result));
|
|
848
|
+
assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: true });
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("urban_debug_instance_state returns null when the engine has no such instance", async () => {
|
|
852
|
+
let openUserTasksCalled = false;
|
|
853
|
+
const engine = fakeEngine({
|
|
854
|
+
searchProcessInstances: async () => [],
|
|
855
|
+
openUserTasks: async () => {
|
|
856
|
+
openUserTasksCalled = true;
|
|
857
|
+
return [];
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
const { router } = buildHarness({ engine });
|
|
861
|
+
const session = await connect(router);
|
|
862
|
+
|
|
863
|
+
const call = await rpc(router, session, "tools/call", {
|
|
864
|
+
name: "urban_debug_instance_state",
|
|
865
|
+
arguments: { processInstanceKey: "nope" },
|
|
866
|
+
});
|
|
867
|
+
assert.equal(toolContentText(call.result), "null");
|
|
868
|
+
assert.equal(openUserTasksCalled, false, "must not probe user tasks for an instance the engine doesn't know");
|
|
869
|
+
});
|
|
870
|
+
|
|
815
871
|
// ---- mutation guard (Slice 3) -----------------------------------------------------------------
|
|
816
872
|
|
|
817
873
|
// A repeated credential header must read the SAME under the MCP guard as under OpenAPI route
|
|
@@ -82,7 +82,6 @@ import { resolveBindMode } from "../manifest.ts";
|
|
|
82
82
|
import { json, makeRouter, type Route } from "../router.ts";
|
|
83
83
|
import { API_BASE, readApiBinding } from "./api.ts";
|
|
84
84
|
import { resolveAppPath } from "./datasource.ts";
|
|
85
|
-
import { InstanceStateStore } from "./instance-state-store.ts";
|
|
86
85
|
import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
|
|
87
86
|
|
|
88
87
|
/** The mount path of the MCP endpoint — a sibling of `/app/api` / `/app/agent`, mounted
|
|
@@ -785,17 +784,19 @@ function buildDebugTools(app: AppApi): DebugTool[] {
|
|
|
785
784
|
},
|
|
786
785
|
{
|
|
787
786
|
name: `${DEBUG_PREFIX}instance_state`,
|
|
788
|
-
description:
|
|
787
|
+
description:
|
|
788
|
+
"Engine truth: the live lifecycle state of a process instance (ACTIVE/COMPLETED/TERMINATED) via searchProcessInstances, plus `waitingOnHuman` from its open user tasks. `null` means the engine has no such instance (not merely 'not yet terminal').",
|
|
789
789
|
inputSchema: {
|
|
790
790
|
type: "object",
|
|
791
791
|
properties: { processInstanceKey: OPTIONAL_STRING },
|
|
792
792
|
required: ["processInstanceKey"],
|
|
793
793
|
},
|
|
794
|
-
run: (args) => {
|
|
794
|
+
run: async (args) => {
|
|
795
795
|
const key = requireString(args, "processInstanceKey");
|
|
796
|
-
const
|
|
797
|
-
if (!
|
|
798
|
-
|
|
796
|
+
const [snapshot] = await app.engine.searchProcessInstances({ processInstanceKeys: [key] });
|
|
797
|
+
if (!snapshot) return null;
|
|
798
|
+
const openTasks = await app.engine.openUserTasks({ processInstanceKey: key });
|
|
799
|
+
return { processInstanceKey: key, state: snapshot.state, waitingOnHuman: openTasks.length > 0 };
|
|
799
800
|
},
|
|
800
801
|
},
|
|
801
802
|
{
|
|
@@ -55,6 +55,9 @@ function fakeEngine(): { engine: EngineClient; calls: FakeEngineCalls } {
|
|
|
55
55
|
async searchVariables() { return []; },
|
|
56
56
|
async searchJobs() { return []; },
|
|
57
57
|
async getProcessDefinitionXml() { return null; },
|
|
58
|
+
async searchAgentInstances() { return []; },
|
|
59
|
+
async searchAgentInstanceHistory() { return []; },
|
|
60
|
+
async getAgentInstance() { return null; },
|
|
58
61
|
async resolveIncident() {},
|
|
59
62
|
async updateJobRetries() {},
|
|
60
63
|
async setVariables() {},
|
|
@@ -41,6 +41,9 @@ function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurf
|
|
|
41
41
|
searchVariables: async () => [],
|
|
42
42
|
searchJobs: async () => [],
|
|
43
43
|
getProcessDefinitionXml: async () => null,
|
|
44
|
+
searchAgentInstances: async () => [],
|
|
45
|
+
searchAgentInstanceHistory: async () => [],
|
|
46
|
+
getAgentInstance: async () => null,
|
|
44
47
|
resolveIncident: async () => {},
|
|
45
48
|
updateJobRetries: async () => {},
|
|
46
49
|
setVariables: async () => {},
|
|
@@ -67,6 +70,9 @@ const fakeEngine: EngineClient = {
|
|
|
67
70
|
searchVariables: async () => [],
|
|
68
71
|
searchJobs: async () => [],
|
|
69
72
|
getProcessDefinitionXml: async () => null,
|
|
73
|
+
searchAgentInstances: async () => [],
|
|
74
|
+
searchAgentInstanceHistory: async () => [],
|
|
75
|
+
getAgentInstance: async () => null,
|
|
70
76
|
resolveIncident: async () => {},
|
|
71
77
|
updateJobRetries: async () => {},
|
|
72
78
|
setVariables: async () => {},
|
|
@@ -42,6 +42,9 @@ class MiniEngine implements EngineClient {
|
|
|
42
42
|
async searchVariables(): Promise<never[]> { return []; }
|
|
43
43
|
async searchJobs(): Promise<never[]> { return []; }
|
|
44
44
|
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
45
|
+
async searchAgentInstances(): Promise<never[]> { return []; }
|
|
46
|
+
async searchAgentInstanceHistory(): Promise<never[]> { return []; }
|
|
47
|
+
async getAgentInstance(): Promise<null> { return null; }
|
|
45
48
|
async resolveIncident(): Promise<void> {}
|
|
46
49
|
async updateJobRetries(): Promise<void> {}
|
|
47
50
|
async setVariables(): Promise<void> {}
|
|
@@ -39,6 +39,9 @@ class MiniEngine implements EngineClient {
|
|
|
39
39
|
async searchVariables(): Promise<never[]> { return []; }
|
|
40
40
|
async searchJobs(): Promise<never[]> { return []; }
|
|
41
41
|
async getProcessDefinitionXml(): Promise<null> { return null; }
|
|
42
|
+
async searchAgentInstances(): Promise<never[]> { return []; }
|
|
43
|
+
async searchAgentInstanceHistory(): Promise<never[]> { return []; }
|
|
44
|
+
async getAgentInstance(): Promise<null> { return null; }
|
|
42
45
|
async resolveIncident(): Promise<void> {}
|
|
43
46
|
async updateJobRetries(): Promise<void> {}
|
|
44
47
|
async setVariables(): Promise<void> {}
|
|
@@ -53,6 +53,15 @@ class NoopEngine implements EngineClient {
|
|
|
53
53
|
async getProcessDefinitionXml() {
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
|
+
async searchAgentInstances() {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
async searchAgentInstanceHistory() {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
async getAgentInstance() {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
56
65
|
async resolveIncident() {}
|
|
57
66
|
async updateJobRetries() {}
|
|
58
67
|
async setVariables() {}
|
|
@@ -74,6 +74,15 @@ class FakeEngine implements EngineClient {
|
|
|
74
74
|
async getProcessDefinitionXml() {
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
+
async searchAgentInstances() {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
async searchAgentInstanceHistory() {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
async getAgentInstance() {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
77
86
|
async resolveIncident() {}
|
|
78
87
|
async updateJobRetries() {}
|
|
79
88
|
async setVariables() {}
|