@nanobpm/urban 0.92.1 → 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.
@@ -1,3 +1,4 @@
1
+ import type { TranscriptContentBlock, TranscriptToolCall, TranscriptTurnMetrics, TranscriptTurnRole } from "@nanobpm/agentic/transcript";
1
2
  /** A minimal HTTP request as seen by a mounted surface/trigger handler. */
2
3
  export interface HttpRequest {
3
4
  method: string;
@@ -537,6 +538,95 @@ export interface JobSummary {
537
538
  readonly elementId?: string;
538
539
  readonly deadline?: string;
539
540
  }
541
+ /**
542
+ * Aggregated metrics for an agent instance across all of its model calls — the
543
+ * `AgentInstanceMetrics` the engine rolls up on the instance itself (distinct from the
544
+ * per-turn {@link TranscriptTurnMetrics} carried on an {@link AgentHistoryRecord}). Total
545
+ * `inputTokens`/`outputTokens` consumed, and the count of `modelCalls`/`toolCalls` made.
546
+ */
547
+ export interface AgentInstanceMetrics {
548
+ readonly inputTokens: number;
549
+ readonly outputTokens: number;
550
+ readonly modelCalls: number;
551
+ readonly toolCalls: number;
552
+ }
553
+ /**
554
+ * A single engine-native agent instance as {@link EngineClient.searchAgentInstances} /
555
+ * {@link EngineClient.getAgentInstance} report it — the projection of the engine's
556
+ * `AgentInstanceResult`. Carries the `agentInstanceKey` a caller passes to
557
+ * {@link EngineClient.searchAgentInstanceHistory}, the lifecycle `status` (a bare string —
558
+ * the engine's `AgentInstanceStatusEnum` is broad: `INITIALIZING`/`THINKING`/`TOOL_CALLING`/
559
+ * `IDLE`/`COMPLETED`/…), the owning `processInstanceKey`, and best-effort diagnostics
560
+ * (`elementId`, the owning `elementInstanceKeys`, the process-definition linkage, aggregated
561
+ * `metrics`, and the creation/update/completion timestamps). Optional fields are omitted when
562
+ * the engine does not report them.
563
+ */
564
+ export interface AgentInstanceSummary {
565
+ readonly agentInstanceKey: string;
566
+ readonly status: string;
567
+ readonly processInstanceKey: string;
568
+ readonly elementId?: string;
569
+ readonly elementInstanceKeys?: readonly string[];
570
+ readonly rootProcessInstanceKey?: string;
571
+ readonly processDefinitionKey?: string;
572
+ readonly processDefinitionId?: string;
573
+ readonly metrics?: AgentInstanceMetrics;
574
+ readonly creationDate?: string;
575
+ readonly lastUpdatedDate?: string;
576
+ readonly completionDate?: string;
577
+ }
578
+ /**
579
+ * A single agent-instance history item as {@link EngineClient.searchAgentInstanceHistory}
580
+ * reports it — the projection of the engine's `AgentInstanceHistoryItemResult`, i.e. one
581
+ * Camunda `AgentHistoryRecordValue`. Its conversation grammar (`role`, `content` blocks,
582
+ * `toolCalls`, per-turn `metrics`) reuses the `@nanobpm/agentic/transcript` parity types the
583
+ * transcript store already models, so the engine-read seam and the transcript store project
584
+ * the same shape (No Drift Surfaces). `historyItemKey` is the stable, creation-ordered
585
+ * identity; `loopIteration` is the agent-loop counter (one LLM call + its tool dispatches +
586
+ * their results share an iteration); `commitStatus` is the engine's `COMMITTED`/`PENDING`/
587
+ * `DISCARDED` flag (a bare string). `metrics` is present only when the engine recorded per-call
588
+ * metrics for the item; `elementInstanceKey`/`jobKey`/`producedAt` are best-effort diagnostics,
589
+ * omitted when unreported.
590
+ */
591
+ export interface AgentHistoryRecord {
592
+ readonly historyItemKey: string;
593
+ readonly agentInstanceKey: string;
594
+ readonly loopIteration: number;
595
+ readonly role: TranscriptTurnRole;
596
+ readonly content: readonly TranscriptContentBlock[];
597
+ readonly toolCalls: readonly TranscriptToolCall[];
598
+ readonly metrics?: TranscriptTurnMetrics;
599
+ readonly commitStatus: string;
600
+ readonly elementInstanceKey?: string;
601
+ readonly jobKey?: string;
602
+ readonly producedAt?: string;
603
+ }
604
+ /** Selectors for {@link EngineClient.searchAgentInstances}. Every field is optional; an
605
+ * omitted/blank selector is not applied (No Drift Surfaces — the same presence rule the key
606
+ * selectors elsewhere in this seam use). */
607
+ export interface AgentInstanceFilter {
608
+ /** Only agent instances owned by this process instance. */
609
+ readonly processInstanceKey?: string;
610
+ /** Only agent instances belonging to this *root* process-instance hierarchy. */
611
+ readonly rootProcessInstanceKey?: string;
612
+ /** Only agent instances in this lifecycle status (the engine's `AgentInstanceStatusEnum`). */
613
+ readonly status?: string;
614
+ /** Only agent instances owned by this BPMN element (the AI-agent task / ad-hoc sub-process). */
615
+ readonly elementId?: string;
616
+ }
617
+ /** Selectors for {@link EngineClient.searchAgentInstanceHistory} (the `agentInstanceKey` is a
618
+ * required positional argument, not a filter field). Every field is optional. Note the engine
619
+ * defaults an unfiltered history search to `COMMITTED` items only. */
620
+ export interface AgentHistoryFilter {
621
+ /** Only history items with this conversation role. Note that `TranscriptTurnRole` is the wider
622
+ * transcript union: only the engine-side subset `"USER" | "ASSISTANT" | "TOOL_RESULT"` is
623
+ * applied as a filter — `"CONFIGURATION"`/`"UNSPECIFIED"` are ignored (not forwarded). */
624
+ readonly role?: TranscriptTurnRole;
625
+ /** Only history items produced in this agent-loop iteration. */
626
+ readonly loopIteration?: number;
627
+ /** Only history items produced under this element instance. */
628
+ readonly elementInstanceKey?: string;
629
+ }
540
630
  /**
541
631
  * EngineClient is the seam onto a Nano engine. The SDK/REST-backed adapter implements
542
632
  * it against a live engine; tests implement it in-memory. Core modules depend only on this.
@@ -712,6 +802,39 @@ export interface EngineClient {
712
802
  * caller distinguishes "have it" from "don't" without a try/catch. A zero-wait read.
713
803
  */
714
804
  getProcessDefinitionXml(processDefinitionKey: string): Promise<string | null>;
805
+ /**
806
+ * Search engine-native agent instances (`POST /agent-instances/search`), optionally narrowed
807
+ * by owning `processInstanceKey`/`rootProcessInstanceKey`, lifecycle `status`, or `elementId`.
808
+ * The read entry point onto the engine's AgentInstance/AgentHistory model: an
809
+ * {@link AgentInstanceSummary} carries the `agentInstanceKey` a caller passes to
810
+ * {@link searchAgentInstanceHistory} to read that instance's conversation. An eventually
811
+ * consistent (zero-wait) read; each malformed row (no `agentInstanceKey`/`processInstanceKey`)
812
+ * is dropped rather than surfaced.
813
+ */
814
+ searchAgentInstances(filter?: AgentInstanceFilter): Promise<AgentInstanceSummary[]>;
815
+ /**
816
+ * Search one agent instance's conversation history
817
+ * (`POST /agent-instances/{agentInstanceKey}/history/search`) — the ordered
818
+ * {@link AgentHistoryRecord}s (turns) the engine retains for `agentInstanceKey`, optionally
819
+ * narrowed by `role`/`loopIteration`/`elementInstanceKey`. This is the production read path
820
+ * onto engine AgentHistory the historical-transcript + metrics consumer builds on
821
+ * (nanobpm/nano-workforce#747). Each record's conversation grammar reuses the
822
+ * `@nanobpm/agentic/transcript` parity shapes (roles, content blocks, per-turn metrics, tool
823
+ * calls). Note the engine defaults an unfiltered search to `COMMITTED` items only. A blank
824
+ * `agentInstanceKey` addresses no instance, so it returns the empty list rather than issuing a
825
+ * request with an empty path segment. An eventually consistent (zero-wait) read; malformed rows
826
+ * are dropped.
827
+ */
828
+ searchAgentInstanceHistory(agentInstanceKey: string, filter?: AgentHistoryFilter): Promise<AgentHistoryRecord[]>;
829
+ /**
830
+ * Fetch a single agent instance by its key (`GET /agent-instances/{agentInstanceKey}`), or
831
+ * `null` when it cannot be resolved — the key is blank, no such agent instance exists (a 404),
832
+ * or the fetch otherwise fails. A read is treated as absence rather than propagating (mirroring
833
+ * {@link getForm}/{@link getElementInstance}/{@link getProcessDefinitionXml}), so a caller
834
+ * distinguishes "have it" from "don't" without a try/catch. A zero-wait read returning the same
835
+ * {@link AgentInstanceSummary} shape as {@link searchAgentInstances}.
836
+ */
837
+ getAgentInstance(agentInstanceKey: string): Promise<AgentInstanceSummary | null>;
715
838
  /**
716
839
  * Resolve an open incident by key (`POST /v2/incidents/{incidentKey}/resolution`), unblocking
717
840
  * the parked token: a job incident returns the job to the activatable pool (so it must have
@@ -766,4 +889,4 @@ export interface EngineClient {
766
889
  * and `getForm` before it) that a purely structural `implements EngineClient` check no
767
890
  * longer catches once the fake is published/compiled against an older `@nanobpm/urban`.
768
891
  */
769
- export declare const ENGINE_CLIENT_METHODS: readonly ["deployResources", "createInstance", "cancelInstance", "publishMessage", "searchUserTasks", "openUserTasks", "getForm", "completeUserTask", "searchProcessInstances", "searchElementInstances", "searchElementInstanceWaitStates", "getElementInstance", "searchIncidents", "searchVariables", "searchJobs", "getProcessDefinitionXml", "resolveIncident", "updateJobRetries", "setVariables", "registerWorker", "close"];
892
+ export declare const ENGINE_CLIENT_METHODS: readonly ["deployResources", "createInstance", "cancelInstance", "publishMessage", "searchUserTasks", "openUserTasks", "getForm", "completeUserTask", "searchProcessInstances", "searchElementInstances", "searchElementInstanceWaitStates", "getElementInstance", "searchIncidents", "searchVariables", "searchJobs", "getProcessDefinitionXml", "searchAgentInstances", "searchAgentInstanceHistory", "getAgentInstance", "resolveIncident", "updateJobRetries", "setVariables", "registerWorker", "close"];
@@ -123,6 +123,9 @@ export const ENGINE_CLIENT_METHODS = [
123
123
  "searchVariables",
124
124
  "searchJobs",
125
125
  "getProcessDefinitionXml",
126
+ "searchAgentInstances",
127
+ "searchAgentInstanceHistory",
128
+ "getAgentInstance",
126
129
  "resolveIncident",
127
130
  "updateJobRetries",
128
131
  "setVariables",
@@ -1,4 +1,4 @@
1
- import type { EngineClient, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
1
+ import type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
2
2
  import type { EngineSdkClient } from "./sdk.ts";
3
3
  /** Coerce an engine response's process-instance key to a non-empty string, or
4
4
  * throw — a missing key means a malformed/partial response, not a real instance. */
@@ -55,6 +55,18 @@ export declare function mapVariableRow(row: Record<string, unknown>, log: Log):
55
55
  * `state` is passed through as a bare string (the engine's job-state enum is broad). The single
56
56
  * source of truth `searchJobs` extracts through. */
57
57
  export declare function mapJobRow(row: Record<string, unknown>, log: Log): JobSummary | undefined;
58
+ /** Map one engine `AgentInstanceResult` row onto an {@link AgentInstanceSummary}, or `undefined`
59
+ * when the row is malformed (missing the `agentInstanceKey` or owning `processInstanceKey`). The
60
+ * single source of truth `searchAgentInstances`/`getAgentInstance` extract through, so the two
61
+ * cannot drift. Optional linkage/metrics/timestamp fields are emitted only when the engine
62
+ * reports them. */
63
+ export declare function mapAgentInstanceRow(row: Record<string, unknown>, log: Log): AgentInstanceSummary | undefined;
64
+ /** Map one engine `AgentInstanceHistoryItemResult` row onto an {@link AgentHistoryRecord}, or
65
+ * `undefined` when the row is malformed (missing the `historyItemKey` or owning
66
+ * `agentInstanceKey`). `content`/`toolCalls` drop any individually-malformed block; `loopIteration`
67
+ * defaults to `0` when unreported; `commitStatus` passes through as a bare string. The single
68
+ * source of truth `searchAgentInstanceHistory` extracts through. */
69
+ export declare function mapAgentHistoryRow(row: Record<string, unknown>, log: Log): AgentHistoryRecord | undefined;
58
70
  /** A job as delivered to a nano-sdk job handler: the frame fields plus the
59
71
  * acknowledgement actions the handler must call. */
60
72
  export interface NanoSdkActivatedJob {
@@ -161,6 +173,18 @@ export interface NanoSdkClient {
161
173
  getProcessDefinitionXml(input: {
162
174
  processDefinitionKey: string;
163
175
  }, consistency?: unknown, options?: unknown): Promise<unknown>;
176
+ searchAgentInstances(input: {
177
+ filter?: Record<string, unknown>;
178
+ page?: Record<string, unknown>;
179
+ }, consistency?: unknown, options?: unknown): Promise<Record<string, unknown>>;
180
+ searchAgentInstanceHistory(input: {
181
+ agentInstanceKey: string;
182
+ filter?: Record<string, unknown>;
183
+ page?: Record<string, unknown>;
184
+ }, consistency?: unknown, options?: unknown): Promise<Record<string, unknown>>;
185
+ getAgentInstance(input: {
186
+ agentInstanceKey: string;
187
+ }, consistency?: unknown, options?: unknown): Promise<Record<string, unknown>>;
164
188
  resolveIncident(input: {
165
189
  incidentKey: string;
166
190
  }, options?: unknown): Promise<unknown>;
@@ -246,6 +270,9 @@ export declare class SdkEngineClient implements EngineClient {
246
270
  searchVariables(filter?: VariableFilter): Promise<VariableSummary[]>;
247
271
  searchJobs(filter?: JobFilter): Promise<JobSummary[]>;
248
272
  getProcessDefinitionXml(processDefinitionKey: string): Promise<string | null>;
273
+ searchAgentInstances(filter?: AgentInstanceFilter): Promise<AgentInstanceSummary[]>;
274
+ searchAgentInstanceHistory(agentInstanceKey: string, filter?: AgentHistoryFilter): Promise<AgentHistoryRecord[]>;
275
+ getAgentInstance(agentInstanceKey: string): Promise<AgentInstanceSummary | null>;
249
276
  resolveIncident(input: {
250
277
  incidentKey: string;
251
278
  }): Promise<void>;
@@ -405,6 +405,191 @@ export function mapJobRow(row, log) {
405
405
  ...(deadline ? { deadline } : {}),
406
406
  };
407
407
  }
408
+ /** The agent-instance conversation roles the engine's `AgentInstanceHistoryRoleEnum` reports —
409
+ * a strict subset of the transcript {@link TranscriptTurnRole} union it maps onto, so the two
410
+ * cannot drift. A row whose role is anything else is normalized to `"UNSPECIFIED"`. */
411
+ const AGENT_HISTORY_ROLES = ["USER", "ASSISTANT", "TOOL_RESULT"];
412
+ /** Map an engine history-item role onto the transcript {@link TranscriptTurnRole} union, or
413
+ * `"UNSPECIFIED"` when the engine reports an unknown/absent role — a lossy-but-safe fallback so a
414
+ * record is surfaced rather than dropped for a role the engine grows later. */
415
+ function normalizeAgentHistoryRole(value) {
416
+ const role = AGENT_HISTORY_ROLES.find((r) => r === value);
417
+ return role ?? "UNSPECIFIED";
418
+ }
419
+ /** The content-block type discriminators the engine's `AgentInstanceMessageContentTypeEnum`
420
+ * reports, mapped onto the transcript {@link TranscriptContentType} union. */
421
+ const AGENT_CONTENT_TYPES = ["TEXT", "DOCUMENT", "OBJECT"];
422
+ /** Map one engine `AgentInstanceMessageContent` row onto a transcript {@link TranscriptContentBlock},
423
+ * or `undefined` when malformed. TEXT carries `text`, DOCUMENT carries `documentReference`, OBJECT
424
+ * carries `object` — exactly the per-`contentType` payload the transcript shape pins, so the
425
+ * engine-read seam and the transcript store project content identically (No Drift Surfaces). An
426
+ * unknown/absent discriminator maps to an `UNSPECIFIED` block carrying no payload. */
427
+ function mapAgentContentBlock(value) {
428
+ if (!isRecord(value))
429
+ return undefined;
430
+ const contentType = AGENT_CONTENT_TYPES.find((t) => t === value.contentType) ?? "UNSPECIFIED";
431
+ if (contentType === "TEXT") {
432
+ return { contentType, text: typeof value.text === "string" ? value.text : "" };
433
+ }
434
+ if (contentType === "DOCUMENT") {
435
+ const documentReference = presentText(value.documentReference);
436
+ return { contentType, documentReference: documentReference ?? "" };
437
+ }
438
+ if (contentType === "OBJECT") {
439
+ return { contentType, object: value.object };
440
+ }
441
+ return { contentType };
442
+ }
443
+ /** Map one engine `AgentInstanceToolCall` row onto a transcript {@link TranscriptToolCall}, or
444
+ * `undefined` when it carries no `toolCallId`/`toolName` (the identity a caller correlates on). A
445
+ * TOOL_RESULT item reports `arguments: null`; normalize that to an empty object so the
446
+ * transcript shape's non-null `arguments` invariant holds. */
447
+ function mapAgentToolCall(value) {
448
+ if (!isRecord(value))
449
+ return undefined;
450
+ const toolCallId = presentText(value.toolCallId);
451
+ const toolName = presentText(value.toolName);
452
+ if (toolCallId === undefined || toolName === undefined)
453
+ return undefined;
454
+ const elementId = presentText(value.elementId);
455
+ const args = isRecord(value.arguments) ? value.arguments : {};
456
+ return {
457
+ toolCallId,
458
+ toolName,
459
+ ...(elementId ? { elementId } : {}),
460
+ arguments: args,
461
+ };
462
+ }
463
+ /** Map one engine `AgentInstanceHistoryItemMetrics` onto per-turn {@link TranscriptTurnMetrics},
464
+ * or `undefined` when the engine recorded none (a `null` metrics object). The engine's per-item
465
+ * metrics carry only `inputTokens`/`outputTokens`/`durationMs` (each nullable); the transcript
466
+ * shape additionally models reasoning/cache token counts the engine does not report per item, so
467
+ * those default to `0` (the "none reported" count) — a coercion, not a second projection. */
468
+ function mapAgentHistoryMetrics(value) {
469
+ if (!isRecord(value))
470
+ return undefined;
471
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
472
+ return {
473
+ inputTokens: num(value.inputTokens),
474
+ outputTokens: num(value.outputTokens),
475
+ reasoningTokenCount: 0,
476
+ cacheCreationTokenCount: 0,
477
+ cacheReadTokenCount: 0,
478
+ durationMs: num(value.durationMs),
479
+ };
480
+ }
481
+ /** Map one engine `AgentInstanceMetrics` (the instance-level aggregate) onto
482
+ * {@link AgentInstanceMetrics}, or `undefined` when the row carries none. */
483
+ function mapAgentInstanceMetrics(value) {
484
+ if (!isRecord(value))
485
+ return undefined;
486
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
487
+ return {
488
+ inputTokens: num(value.inputTokens),
489
+ outputTokens: num(value.outputTokens),
490
+ modelCalls: num(value.modelCalls),
491
+ toolCalls: num(value.toolCalls),
492
+ };
493
+ }
494
+ /** Map one engine `AgentInstanceResult` row onto an {@link AgentInstanceSummary}, or `undefined`
495
+ * when the row is malformed (missing the `agentInstanceKey` or owning `processInstanceKey`). The
496
+ * single source of truth `searchAgentInstances`/`getAgentInstance` extract through, so the two
497
+ * cannot drift. Optional linkage/metrics/timestamp fields are emitted only when the engine
498
+ * reports them. */
499
+ export function mapAgentInstanceRow(row, log) {
500
+ const agentInstanceKey = presentEngineKey(row.agentInstanceKey);
501
+ if (agentInstanceKey === undefined) {
502
+ log("warn", "skipping agent instance with no agentInstanceKey in engine response");
503
+ return undefined;
504
+ }
505
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
506
+ if (processInstanceKey === undefined) {
507
+ log("warn", "skipping agent instance with no processInstanceKey in engine response", {
508
+ agentInstanceKey,
509
+ });
510
+ return undefined;
511
+ }
512
+ const status = presentText(row.status) ?? "UNKNOWN";
513
+ const elementId = presentText(row.elementId);
514
+ const rootProcessInstanceKey = presentEngineKey(row.rootProcessInstanceKey);
515
+ const processDefinitionKey = presentEngineKey(row.processDefinitionKey);
516
+ const processDefinitionId = presentText(row.processDefinitionId);
517
+ const elementInstanceKeys = Array.isArray(row.elementInstanceKeys)
518
+ ? row.elementInstanceKeys
519
+ .map((k) => presentEngineKey(k))
520
+ .filter((k) => k !== undefined)
521
+ : undefined;
522
+ const metrics = mapAgentInstanceMetrics(row.metrics);
523
+ const creationDate = presentText(row.creationDate);
524
+ const lastUpdatedDate = presentText(row.lastUpdatedDate);
525
+ const completionDate = presentText(row.completionDate);
526
+ return {
527
+ agentInstanceKey,
528
+ status,
529
+ processInstanceKey,
530
+ ...(elementId ? { elementId } : {}),
531
+ ...(elementInstanceKeys && elementInstanceKeys.length > 0 ? { elementInstanceKeys } : {}),
532
+ ...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
533
+ ...(processDefinitionKey ? { processDefinitionKey } : {}),
534
+ ...(processDefinitionId ? { processDefinitionId } : {}),
535
+ ...(metrics ? { metrics } : {}),
536
+ ...(creationDate ? { creationDate } : {}),
537
+ ...(lastUpdatedDate ? { lastUpdatedDate } : {}),
538
+ ...(completionDate ? { completionDate } : {}),
539
+ };
540
+ }
541
+ /** Map one engine `AgentInstanceHistoryItemResult` row onto an {@link AgentHistoryRecord}, or
542
+ * `undefined` when the row is malformed (missing the `historyItemKey` or owning
543
+ * `agentInstanceKey`). `content`/`toolCalls` drop any individually-malformed block; `loopIteration`
544
+ * defaults to `0` when unreported; `commitStatus` passes through as a bare string. The single
545
+ * source of truth `searchAgentInstanceHistory` extracts through. */
546
+ export function mapAgentHistoryRow(row, log) {
547
+ const historyItemKey = presentEngineKey(row.historyItemKey);
548
+ if (historyItemKey === undefined) {
549
+ log("warn", "skipping agent history item with no historyItemKey in engine response");
550
+ return undefined;
551
+ }
552
+ const agentInstanceKey = presentEngineKey(row.agentInstanceKey);
553
+ if (agentInstanceKey === undefined) {
554
+ log("warn", "skipping agent history item with no agentInstanceKey in engine response", {
555
+ historyItemKey,
556
+ });
557
+ return undefined;
558
+ }
559
+ const loopIteration = typeof row.loopIteration === "number" && Number.isFinite(row.loopIteration)
560
+ ? row.loopIteration
561
+ : 0;
562
+ const content = Array.isArray(row.content)
563
+ ? row.content.flatMap((c) => {
564
+ const block = mapAgentContentBlock(c);
565
+ return block ? [block] : [];
566
+ })
567
+ : [];
568
+ const toolCalls = Array.isArray(row.toolCalls)
569
+ ? row.toolCalls.flatMap((t) => {
570
+ const call = mapAgentToolCall(t);
571
+ return call ? [call] : [];
572
+ })
573
+ : [];
574
+ const metrics = mapAgentHistoryMetrics(row.metrics);
575
+ const commitStatus = presentText(row.commitStatus) ?? "COMMITTED";
576
+ const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
577
+ const jobKey = presentEngineKey(row.jobKey);
578
+ const producedAt = presentText(row.producedAt);
579
+ return {
580
+ historyItemKey,
581
+ agentInstanceKey,
582
+ loopIteration,
583
+ role: normalizeAgentHistoryRole(row.role),
584
+ content,
585
+ toolCalls,
586
+ ...(metrics ? { metrics } : {}),
587
+ commitStatus,
588
+ ...(elementInstanceKey ? { elementInstanceKey } : {}),
589
+ ...(jobKey ? { jobKey } : {}),
590
+ ...(producedAt ? { producedAt } : {}),
591
+ };
592
+ }
408
593
  /**
409
594
  * An `EngineClient` backed entirely by a `@nanobpm/nano-sdk` client. Deploy, create,
410
595
  * message, and user-task calls map one-to-one onto the SDK's client methods; workers
@@ -780,6 +965,83 @@ export class SdkEngineClient {
780
965
  // as "no XML" (null) so a caller distinguishes "have it" from "don't".
781
966
  return typeof xml === "string" && xml.trim() !== "" ? xml : null;
782
967
  }
968
+ async searchAgentInstances(filter) {
969
+ // Same shape as `searchIncidents`: build the engine filter from the transport-agnostic
970
+ // selectors, read at zero-wait consistency (an agent-instance search is eventually
971
+ // consistent), and map each row through the single `mapAgentInstanceRow` gate, dropping
972
+ // malformed rows. Normalize the request key selectors the same way the response mapper
973
+ // normalizes keys so a blank/padded selector cannot reach the engine as a garbage filter.
974
+ const f = {};
975
+ const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
976
+ if (processInstanceKey)
977
+ f.processInstanceKey = processInstanceKey;
978
+ const rootProcessInstanceKey = presentEngineKey(filter?.rootProcessInstanceKey);
979
+ if (rootProcessInstanceKey)
980
+ f.rootProcessInstanceKey = rootProcessInstanceKey;
981
+ const status = filter?.status?.trim();
982
+ if (status)
983
+ f.status = status;
984
+ const elementId = filter?.elementId?.trim();
985
+ if (elementId)
986
+ f.elementId = elementId;
987
+ const body = await this.client.searchAgentInstances({ filter: f }, { consistency: { waitUpToMs: 0 } });
988
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
989
+ return items.flatMap((it) => {
990
+ const mapped = mapAgentInstanceRow(it, this.log);
991
+ return mapped ? [mapped] : [];
992
+ });
993
+ }
994
+ async searchAgentInstanceHistory(agentInstanceKey, filter) {
995
+ // A blank key can never address an agent instance — short-circuit to the empty list rather
996
+ // than issue a `POST /agent-instances//history/search` with an empty path segment. Normalize a
997
+ // padded-but-valid key so `" 5 "` addresses the same instance, mirroring `getElementInstance`.
998
+ const key = presentEngineKey(agentInstanceKey);
999
+ if (key === undefined)
1000
+ return [];
1001
+ const f = {};
1002
+ // `AgentHistoryFilter.role` is typed as the wider transcript `TranscriptTurnRole` union, which
1003
+ // also carries `CONFIGURATION`/`UNSPECIFIED` — roles the engine's history filter does not
1004
+ // accept and would 4xx on. Gate the selector through the same `AGENT_HISTORY_ROLES` subset the
1005
+ // response mapper uses (No Drift Surfaces) so an unsupported role is dropped, not forwarded.
1006
+ if (filter?.role && AGENT_HISTORY_ROLES.includes(filter.role))
1007
+ f.role = filter.role;
1008
+ if (typeof filter?.loopIteration === "number" && Number.isFinite(filter.loopIteration)) {
1009
+ f.loopIteration = filter.loopIteration;
1010
+ }
1011
+ const elementInstanceKey = presentEngineKey(filter?.elementInstanceKey);
1012
+ if (elementInstanceKey)
1013
+ f.elementInstanceKey = elementInstanceKey;
1014
+ const body = await this.client.searchAgentInstanceHistory({ agentInstanceKey: key, filter: f }, { consistency: { waitUpToMs: 0 } });
1015
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
1016
+ return items.flatMap((it) => {
1017
+ const mapped = mapAgentHistoryRow(it, this.log);
1018
+ return mapped ? [mapped] : [];
1019
+ });
1020
+ }
1021
+ async getAgentInstance(agentInstanceKey) {
1022
+ // A blank key can never address an agent instance — short-circuit to null rather than issue a
1023
+ // `GET /agent-instances/` with an empty segment. Normalize a padded-but-valid key, mirroring
1024
+ // `getElementInstance`/`getProcessDefinitionXml`.
1025
+ const key = presentEngineKey(agentInstanceKey);
1026
+ if (key === undefined)
1027
+ return null;
1028
+ let body;
1029
+ try {
1030
+ body = await this.client.getAgentInstance({ agentInstanceKey: key }, { consistency: { waitUpToMs: 0 } });
1031
+ }
1032
+ catch (err) {
1033
+ // A 404 (no such agent instance) is an expected "not found", not a fault — mirror `getForm`/
1034
+ // `getElementInstance`, which treat a failed fetch as absence (null) rather than propagating.
1035
+ this.log("warn", "getAgentInstance: engine fetch failed", {
1036
+ agentInstanceKey: key,
1037
+ err: err instanceof Error ? err.message : String(err),
1038
+ });
1039
+ return null;
1040
+ }
1041
+ if (!isRecord(body))
1042
+ return null;
1043
+ return mapAgentInstanceRow(body, this.log) ?? null;
1044
+ }
783
1045
  async resolveIncident(input) {
784
1046
  await this.client.resolveIncident({
785
1047
  incidentKey: requireEngineKey(input.incidentKey, "incidentKey"),
@@ -8,7 +8,8 @@ export { bindModeToHost, expandEnv, expandEnvString, isBindMode, loadManifest, p
8
8
  export type { ActionDecl, AppManifest, BindMode, ChatSurface, DataSource, DomainField, DomainType, InstanceTracking, LlmBinding, NetworkConfig, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
9
9
  export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
10
10
  export type { ValidationIssue } from "./core/validate.ts";
11
- export type { EngineClient, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
11
+ export type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceMetrics, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
12
+ export type { TranscriptContentBlock, TranscriptContentType, TranscriptToolCall, TranscriptTurnMetrics, TranscriptTurnRole, } from "@nanobpm/agentic/transcript";
12
13
  export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.ts";
13
14
  export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
14
15
  export { createLogger } from "./core/logger.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.92.1",
3
+ "version": "0.93.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -103,6 +103,7 @@
103
103
  },
104
104
  "dependencies": {
105
105
  "@modelcontextprotocol/sdk": "^1.30.0",
106
+ "@nanobpm/agentic": "^0.13.0",
106
107
  "@nanobpm/nano-app-schema": "^0.9.0",
107
108
  "@nanobpm/nano-sdk": "^1.2.5",
108
109
  "@nanobpm/workflow": "^0.14.0",