@nanobpm/agentic 0.1.0 → 0.4.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/README.md +1 -0
- package/dist/demand/model.d.ts +7 -4
- package/dist/demand/model.js +22 -4
- package/dist/demand/taskdef.d.ts +13 -1
- package/dist/demand/taskdef.js +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol/conformance/frames.js +32 -4
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/payloads.d.ts +44 -0
- package/dist/protocol/payloads.js +61 -7
- package/dist/session/acp/client.d.ts +109 -0
- package/dist/session/acp/client.js +254 -0
- package/dist/session/acp/index.d.ts +27 -0
- package/dist/session/acp/index.js +27 -0
- package/dist/session/acp/jsonrpc.d.ts +25 -0
- package/dist/session/acp/jsonrpc.js +148 -0
- package/dist/session/acp/normalize.d.ts +48 -0
- package/dist/session/acp/normalize.js +162 -0
- package/dist/session/acp/protocol.d.ts +94 -0
- package/dist/session/acp/protocol.js +136 -0
- package/dist/session/acp/spawn.d.ts +36 -0
- package/dist/session/acp/spawn.js +68 -0
- package/dist/session/acp/transport.d.ts +62 -0
- package/dist/session/acp/transport.js +126 -0
- package/dist/session/adapter.d.ts +135 -0
- package/dist/session/adapter.js +24 -0
- package/dist/session/backend.d.ts +43 -0
- package/dist/session/backend.js +95 -0
- package/dist/session/events.d.ts +152 -0
- package/dist/session/events.js +192 -0
- package/dist/session/index.d.ts +31 -0
- package/dist/session/index.js +5 -0
- package/dist/session/log.d.ts +107 -0
- package/dist/session/log.js +351 -0
- package/dist/session/normalizer/claude.d.ts +23 -0
- package/dist/session/normalizer/claude.js +138 -0
- package/dist/session/normalizer/copilot.d.ts +27 -0
- package/dist/session/normalizer/copilot.js +105 -0
- package/dist/session/normalizer/deepseek.d.ts +11 -0
- package/dist/session/normalizer/deepseek.js +68 -0
- package/dist/session/normalizer/index.d.ts +36 -0
- package/dist/session/normalizer/index.js +29 -0
- package/dist/session/normalizer/kimi.d.ts +10 -0
- package/dist/session/normalizer/kimi.js +80 -0
- package/dist/session/normalizer/link.d.ts +36 -0
- package/dist/session/normalizer/link.js +56 -0
- package/dist/session/normalizer/pi.d.ts +13 -0
- package/dist/session/normalizer/pi.js +61 -0
- package/dist/session/normalizer/qwen.d.ts +11 -0
- package/dist/session/normalizer/qwen.js +65 -0
- package/dist/session/normalizer/record.d.ts +21 -0
- package/dist/session/normalizer/record.js +87 -0
- package/dist/session/normalizer/types.d.ts +139 -0
- package/dist/session/normalizer/types.js +31 -0
- package/dist/session/schema.d.ts +38 -0
- package/dist/session/schema.js +74 -0
- package/package.json +17 -1
- package/src/demand/model.test.ts +82 -4
- package/src/demand/model.ts +30 -9
- package/src/demand/taskdef.test.ts +51 -6
- package/src/demand/taskdef.ts +31 -2
- package/src/index.ts +1 -0
- package/src/protocol/conformance/frames.ts +32 -4
- package/src/protocol/index.ts +4 -0
- package/src/protocol/payloads.test.ts +31 -1
- package/src/protocol/payloads.ts +110 -7
- package/src/session/acp/client.test.ts +222 -0
- package/src/session/acp/client.ts +356 -0
- package/src/session/acp/fake-agent.ts +71 -0
- package/src/session/acp/index.ts +68 -0
- package/src/session/acp/integration.test.ts +37 -0
- package/src/session/acp/jsonrpc.test.ts +75 -0
- package/src/session/acp/jsonrpc.ts +171 -0
- package/src/session/acp/normalize.test.ts +150 -0
- package/src/session/acp/normalize.ts +204 -0
- package/src/session/acp/protocol.ts +178 -0
- package/src/session/acp/spawn.test.ts +45 -0
- package/src/session/acp/spawn.ts +91 -0
- package/src/session/acp/transport.test.ts +82 -0
- package/src/session/acp/transport.ts +155 -0
- package/src/session/adapter.ts +159 -0
- package/src/session/backend.test.ts +198 -0
- package/src/session/backend.ts +128 -0
- package/src/session/events.test.ts +168 -0
- package/src/session/events.ts +347 -0
- package/src/session/index.ts +67 -0
- package/src/session/log.test.ts +215 -0
- package/src/session/log.ts +525 -0
- package/src/session/normalizer/backend-integration.test.ts +103 -0
- package/src/session/normalizer/claude.test.ts +68 -0
- package/src/session/normalizer/claude.ts +136 -0
- package/src/session/normalizer/copilot.test.ts +59 -0
- package/src/session/normalizer/copilot.ts +133 -0
- package/src/session/normalizer/deepseek.ts +80 -0
- package/src/session/normalizer/index.ts +61 -0
- package/src/session/normalizer/kimi.ts +82 -0
- package/src/session/normalizer/link.test.ts +24 -0
- package/src/session/normalizer/link.ts +81 -0
- package/src/session/normalizer/pi.ts +75 -0
- package/src/session/normalizer/probe.test.ts +49 -0
- package/src/session/normalizer/qwen.test.ts +20 -0
- package/src/session/normalizer/qwen.ts +77 -0
- package/src/session/normalizer/record.test.ts +68 -0
- package/src/session/normalizer/record.ts +88 -0
- package/src/session/normalizer/resume.test.ts +25 -0
- package/src/session/normalizer/types.ts +152 -0
- package/src/session/normalizer/vectors.test.ts +180 -0
- package/src/session/schema.test.ts +84 -0
- package/src/session/schema.ts +78 -0
- package/src/session/test-db.ts +56 -0
- package/dist/blackboard/test-db.d.ts +0 -5
- package/dist/blackboard/test-db.js +0 -42
- package/dist/presence/test-db.d.ts +0 -5
- package/dist/presence/test-db.js +0 -42
- package/dist/transcript/test-db.d.ts +0 -5
- package/dist/transcript/test-db.js +0 -41
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@nanobpm/agentic/session/normalizer` contract — ADR 0062, slice 3 (the
|
|
3
|
+
* `stream-json`/native-transcript **fallback** ingestion backend).
|
|
4
|
+
*
|
|
5
|
+
* Slice 2 speaks ACP directly; this slice covers every harness that does *not*
|
|
6
|
+
* (yet) speak ACP. ADR 0062 §5 frames `stream-json` as a *transport with N
|
|
7
|
+
* vendor dialects*, so there is no single "stream-json backend": each harness
|
|
8
|
+
* gets a small **normalizer** that maps its native/streaming session output onto
|
|
9
|
+
* Nano's canonical {@link SessionEvent} (slice 1), plus a **resume shim** over
|
|
10
|
+
* that harness's native `--resume <id>` (or SDK equivalent).
|
|
11
|
+
*
|
|
12
|
+
* The three moving parts a harness normalizer exposes:
|
|
13
|
+
*
|
|
14
|
+
* - {@link HarnessNormalizer.toDrafts} — the dialect map: one native record →
|
|
15
|
+
* zero-or-more {@link DraftEvent}s (canonical events *minus* the causal-chain
|
|
16
|
+
* fields the shared {@link linkDrafts} threads in, so a per-harness dialect
|
|
17
|
+
* never re-implements chaining).
|
|
18
|
+
* - {@link HarnessNormalizer.resume} — the resume shim: given a native session
|
|
19
|
+
* id, the exact native invocation ({@link ResumeShim}) that restores it.
|
|
20
|
+
* - {@link HarnessNormalizer.capabilities} — the {@link HarnessCapabilities}
|
|
21
|
+
* the {@link capabilityProbe} folds into a `durable-resume` advertisement
|
|
22
|
+
* (slice 5's enrolment gate reads this).
|
|
23
|
+
*
|
|
24
|
+
* Nothing here interprets a harness schema as *ours*: the native shapes are
|
|
25
|
+
* ingestion details owned entirely by each dialect module; the union they all
|
|
26
|
+
* target is the stable slice-1 contract.
|
|
27
|
+
*/
|
|
28
|
+
import type { SessionEvent } from "../events.ts";
|
|
29
|
+
/**
|
|
30
|
+
* Distributive `Omit` over a discriminated union: applies `Omit` to *each* union
|
|
31
|
+
* member, preserving the `type` discriminant. A plain `Omit<Union, K>` collapses
|
|
32
|
+
* to the members' common properties (losing the per-member fields), so we cannot
|
|
33
|
+
* use it to describe "a session event without its chain fields".
|
|
34
|
+
*/
|
|
35
|
+
export type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
36
|
+
/**
|
|
37
|
+
* A canonical {@link SessionEvent} as a dialect first produces it — the full
|
|
38
|
+
* semantic payload (`type` + type-specific fields) but *without* the causal-chain
|
|
39
|
+
* responsibilities (`parentId`, and an optional-only `id`). The shared
|
|
40
|
+
* {@link linkDrafts} threads `parentId` in emission order and fills any missing
|
|
41
|
+
* `id`, so an individual dialect never re-implements chain bookkeeping; it just
|
|
42
|
+
* says "here is the event this record means". A dialect that already knows a
|
|
43
|
+
* stable native id (a tool-call id, a provider message id) may supply it as
|
|
44
|
+
* `id` to preserve correlation across a resume.
|
|
45
|
+
*/
|
|
46
|
+
export type DraftEvent = DistributiveOmit<SessionEvent, "id" | "parentId"> & {
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The native resume invocation a harness's shim resolves for a session id. Two
|
|
51
|
+
* transports cover the fleet:
|
|
52
|
+
*
|
|
53
|
+
* - `cli` — a flag-driven harness: `args` are the argv tail to append to the
|
|
54
|
+
* harness command to resume that session (e.g. Claude's `["--resume", id]`,
|
|
55
|
+
* Qwen's `["-r", id]`). Nano spawns; it never parses the harness's output
|
|
56
|
+
* beyond the dialect map.
|
|
57
|
+
* - `sdk` — an in-process harness (Copilot's `copilot-sdk`, the DeepSeek live
|
|
58
|
+
* feed): `call` names the SDK method and `args` are its arguments (e.g.
|
|
59
|
+
* `resumeSession(id)`), so the host invokes it directly rather than spawning.
|
|
60
|
+
*
|
|
61
|
+
* `sessionId` echoes the id the shim resumed, so a caller that only kept the
|
|
62
|
+
* {@link ResumeShim} still knows which session it targets.
|
|
63
|
+
*/
|
|
64
|
+
export type ResumeShim = {
|
|
65
|
+
readonly transport: "cli";
|
|
66
|
+
readonly sessionId: string;
|
|
67
|
+
readonly args: readonly string[];
|
|
68
|
+
} | {
|
|
69
|
+
readonly transport: "sdk";
|
|
70
|
+
readonly sessionId: string;
|
|
71
|
+
readonly call: string;
|
|
72
|
+
readonly args: readonly unknown[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* What a harness can do, from the perspective of durable resume. `streaming` is
|
|
76
|
+
* "exposes a machine-readable streaming/native mind source we can normalize";
|
|
77
|
+
* `resumeById` is "can restore a *specific* prior session by id" (not merely
|
|
78
|
+
* `--continue` the latest). {@link CapabilityAdvertisement.durableResume} is
|
|
79
|
+
* derived, never declared — see {@link capabilityProbe}.
|
|
80
|
+
*/
|
|
81
|
+
export interface HarnessCapabilities {
|
|
82
|
+
readonly streaming: boolean;
|
|
83
|
+
readonly resumeById: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The advertisement {@link capabilityProbe} produces: the raw capabilities plus
|
|
87
|
+
* the single **derived** `durableResume` bit slice 5's enrolment gate consumes.
|
|
88
|
+
* Keeping `durableResume` derived (never a hand-set field on a normalizer)
|
|
89
|
+
* eliminates the drift surface where a harness claims durability it can't honour.
|
|
90
|
+
*/
|
|
91
|
+
export interface CapabilityAdvertisement {
|
|
92
|
+
readonly harness: string;
|
|
93
|
+
readonly streaming: boolean;
|
|
94
|
+
readonly resumeById: boolean;
|
|
95
|
+
/** `true` iff the harness both streams a mind source AND resumes by id. */
|
|
96
|
+
readonly durableResume: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One harness's fallback ingestion adapter: a dialect map, a resume shim, and its
|
|
100
|
+
* capabilities. Independent per harness (they fan out in parallel), and all
|
|
101
|
+
* target the one canonical {@link SessionEvent} union.
|
|
102
|
+
*/
|
|
103
|
+
export interface HarnessNormalizer {
|
|
104
|
+
/** The harness id this normalizer speaks for, e.g. `"@github/copilot"`. */
|
|
105
|
+
readonly harness: string;
|
|
106
|
+
/** Raw capabilities; `durable-resume` is derived from these by {@link capabilityProbe}. */
|
|
107
|
+
readonly capabilities: HarnessCapabilities;
|
|
108
|
+
/**
|
|
109
|
+
* Map one native record (a parsed `stream-json` line, an SDK `SessionEvent`, a
|
|
110
|
+
* live-feed frame) to zero-or-more canonical {@link DraftEvent}s. Returns `[]`
|
|
111
|
+
* for records that carry no session-log meaning (transport keep-alives, init
|
|
112
|
+
* frames the canonical model does not represent). Throws
|
|
113
|
+
* {@link NormalizerDialectError} on a record that *should* map but is
|
|
114
|
+
* structurally invalid — a corrupt transcript fails loudly, it never
|
|
115
|
+
* fabricates an event.
|
|
116
|
+
*/
|
|
117
|
+
toDrafts(record: unknown): readonly DraftEvent[];
|
|
118
|
+
/** Resolve the native invocation that resumes `sessionId` for this harness. */
|
|
119
|
+
resume(sessionId: string): ResumeShim;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Derive a harness's `durable-resume` advertisement from its raw capabilities.
|
|
123
|
+
* A harness advertises `durable-resume` **iff** it both exposes a streaming mind
|
|
124
|
+
* source we can normalize AND can restore a specific session by id — either half
|
|
125
|
+
* alone is insufficient (a stream we can't resume, or a resume with no mind to
|
|
126
|
+
* replay). This is the single place the bit is computed.
|
|
127
|
+
*/
|
|
128
|
+
export declare function capabilityProbe(normalizer: HarnessNormalizer): CapabilityAdvertisement;
|
|
129
|
+
/**
|
|
130
|
+
* Raised when a native record that a dialect *should* map is structurally
|
|
131
|
+
* invalid (a missing tool-call id, a message with no content). Mirrors slice 1's
|
|
132
|
+
* `SessionEventShapeError` at the ingestion boundary: normalization is a trusted
|
|
133
|
+
* map, so a malformed native record surfaces loudly rather than silently
|
|
134
|
+
* dropping or fabricating a canonical event.
|
|
135
|
+
*/
|
|
136
|
+
export declare class NormalizerDialectError extends Error {
|
|
137
|
+
readonly harness: string;
|
|
138
|
+
constructor(harness: string, message: string);
|
|
139
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a harness's `durable-resume` advertisement from its raw capabilities.
|
|
3
|
+
* A harness advertises `durable-resume` **iff** it both exposes a streaming mind
|
|
4
|
+
* source we can normalize AND can restore a specific session by id — either half
|
|
5
|
+
* alone is insufficient (a stream we can't resume, or a resume with no mind to
|
|
6
|
+
* replay). This is the single place the bit is computed.
|
|
7
|
+
*/
|
|
8
|
+
export function capabilityProbe(normalizer) {
|
|
9
|
+
const { streaming, resumeById } = normalizer.capabilities;
|
|
10
|
+
return {
|
|
11
|
+
harness: normalizer.harness,
|
|
12
|
+
streaming,
|
|
13
|
+
resumeById,
|
|
14
|
+
durableResume: streaming && resumeById,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Raised when a native record that a dialect *should* map is structurally
|
|
19
|
+
* invalid (a missing tool-call id, a message with no content). Mirrors slice 1's
|
|
20
|
+
* `SessionEventShapeError` at the ingestion boundary: normalization is a trusted
|
|
21
|
+
* map, so a malformed native record surfaces loudly rather than silently
|
|
22
|
+
* dropping or fabricating a canonical event.
|
|
23
|
+
*/
|
|
24
|
+
export class NormalizerDialectError extends Error {
|
|
25
|
+
harness;
|
|
26
|
+
constructor(harness, message) {
|
|
27
|
+
super(`[${harness}] ${message}`);
|
|
28
|
+
this.name = "NormalizerDialectError";
|
|
29
|
+
this.harness = harness;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical authoritative session-log schema — ADR 0062, slice 1.
|
|
3
|
+
*
|
|
4
|
+
* The DDL here is the single source of truth {@link SqliteSessionLog.ensureSchema}
|
|
5
|
+
* applies, and is mirrored statement-for-statement by the app-boot migration
|
|
6
|
+
* `db/migrations/005_agentic_session.sql`. `schema.test.ts` normalises both and
|
|
7
|
+
* asserts they are identical — drift is a red test, not a silent boot mismatch.
|
|
8
|
+
* This mirrors the S6 transcript store's drift-guard exactly (ADR 0056 §12), the
|
|
9
|
+
* advisory precedent this authoritative log is promoted from.
|
|
10
|
+
*
|
|
11
|
+
* Three tables back the log:
|
|
12
|
+
* - `agentic_session_log` — one row per **activation** `(processInstanceKey,
|
|
13
|
+
* elementId)`: its fence high-water (`incarnation`), retention lifecycle,
|
|
14
|
+
* status, and the retained offset window (`first_offset` … `next_offset`).
|
|
15
|
+
* - `agentic_session_event` — the durable, authoritative events, keyed
|
|
16
|
+
* `(process_instance_key, element_id, event_offset)` so a re-lease can replay
|
|
17
|
+
* from any offset and a resuming incarnation can overwrite an uncommitted tail
|
|
18
|
+
* idempotently.
|
|
19
|
+
* - `agentic_session_checkpoint` — the mind/world join points, keyed
|
|
20
|
+
* `(process_instance_key, element_id, checkpoint_id)` with the pinned offset.
|
|
21
|
+
*
|
|
22
|
+
* `event_offset`/`checkpoint_offset` (not `offset`) is deliberate: `OFFSET` is a
|
|
23
|
+
* SQLite keyword, so the columns are named to avoid quoting it everywhere.
|
|
24
|
+
*/
|
|
25
|
+
/** The per-activation metadata + fence table name. */
|
|
26
|
+
export declare const SESSION_LOG_TABLE = "agentic_session_log";
|
|
27
|
+
/** The durable per-event table name. */
|
|
28
|
+
export declare const SESSION_EVENT_TABLE = "agentic_session_event";
|
|
29
|
+
/** The checkpoint table name. */
|
|
30
|
+
export declare const SESSION_CHECKPOINT_TABLE = "agentic_session_checkpoint";
|
|
31
|
+
/**
|
|
32
|
+
* The canonical session-log DDL. Forward-only and additive; every column added
|
|
33
|
+
* here must also be added to the boot migration (the drift guard enforces it).
|
|
34
|
+
* Events are immutable once committed under an incarnation; a resume overwrites
|
|
35
|
+
* only the *uncommitted* tail past the last checkpoint (a re-key at the same
|
|
36
|
+
* `(activation, offset)`), never a committed row.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SESSION_SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS agentic_session_log (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n incarnation INTEGER NOT NULL DEFAULT 0,\n lifecycle TEXT NOT NULL DEFAULT 'activation',\n status TEXT NOT NULL DEFAULT 'open',\n created_at TEXT NOT NULL,\n completed_at TEXT,\n first_offset INTEGER,\n next_offset INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (process_instance_key, element_id)\n);\nCREATE TABLE IF NOT EXISTS agentic_session_event (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n event_offset INTEGER NOT NULL,\n incarnation INTEGER NOT NULL,\n event_id TEXT NOT NULL,\n parent_id TEXT,\n event_type TEXT NOT NULL,\n payload TEXT NOT NULL,\n appended_at TEXT NOT NULL,\n PRIMARY KEY (process_instance_key, element_id, event_offset)\n);\nCREATE TABLE IF NOT EXISTS agentic_session_checkpoint (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n checkpoint_id TEXT NOT NULL,\n checkpoint_offset INTEGER NOT NULL,\n incarnation INTEGER NOT NULL,\n commit_sha TEXT NOT NULL,\n effect_ledger TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (process_instance_key, element_id, checkpoint_id)\n);\nCREATE INDEX IF NOT EXISTS idx_agentic_session_checkpoint_offset ON agentic_session_checkpoint (process_instance_key, element_id, checkpoint_offset);\nCREATE INDEX IF NOT EXISTS idx_agentic_session_log_retention ON agentic_session_log (lifecycle, status, completed_at);";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical authoritative session-log schema — ADR 0062, slice 1.
|
|
3
|
+
*
|
|
4
|
+
* The DDL here is the single source of truth {@link SqliteSessionLog.ensureSchema}
|
|
5
|
+
* applies, and is mirrored statement-for-statement by the app-boot migration
|
|
6
|
+
* `db/migrations/005_agentic_session.sql`. `schema.test.ts` normalises both and
|
|
7
|
+
* asserts they are identical — drift is a red test, not a silent boot mismatch.
|
|
8
|
+
* This mirrors the S6 transcript store's drift-guard exactly (ADR 0056 §12), the
|
|
9
|
+
* advisory precedent this authoritative log is promoted from.
|
|
10
|
+
*
|
|
11
|
+
* Three tables back the log:
|
|
12
|
+
* - `agentic_session_log` — one row per **activation** `(processInstanceKey,
|
|
13
|
+
* elementId)`: its fence high-water (`incarnation`), retention lifecycle,
|
|
14
|
+
* status, and the retained offset window (`first_offset` … `next_offset`).
|
|
15
|
+
* - `agentic_session_event` — the durable, authoritative events, keyed
|
|
16
|
+
* `(process_instance_key, element_id, event_offset)` so a re-lease can replay
|
|
17
|
+
* from any offset and a resuming incarnation can overwrite an uncommitted tail
|
|
18
|
+
* idempotently.
|
|
19
|
+
* - `agentic_session_checkpoint` — the mind/world join points, keyed
|
|
20
|
+
* `(process_instance_key, element_id, checkpoint_id)` with the pinned offset.
|
|
21
|
+
*
|
|
22
|
+
* `event_offset`/`checkpoint_offset` (not `offset`) is deliberate: `OFFSET` is a
|
|
23
|
+
* SQLite keyword, so the columns are named to avoid quoting it everywhere.
|
|
24
|
+
*/
|
|
25
|
+
/** The per-activation metadata + fence table name. */
|
|
26
|
+
export const SESSION_LOG_TABLE = "agentic_session_log";
|
|
27
|
+
/** The durable per-event table name. */
|
|
28
|
+
export const SESSION_EVENT_TABLE = "agentic_session_event";
|
|
29
|
+
/** The checkpoint table name. */
|
|
30
|
+
export const SESSION_CHECKPOINT_TABLE = "agentic_session_checkpoint";
|
|
31
|
+
/**
|
|
32
|
+
* The canonical session-log DDL. Forward-only and additive; every column added
|
|
33
|
+
* here must also be added to the boot migration (the drift guard enforces it).
|
|
34
|
+
* Events are immutable once committed under an incarnation; a resume overwrites
|
|
35
|
+
* only the *uncommitted* tail past the last checkpoint (a re-key at the same
|
|
36
|
+
* `(activation, offset)`), never a committed row.
|
|
37
|
+
*/
|
|
38
|
+
export const SESSION_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${SESSION_LOG_TABLE} (
|
|
39
|
+
process_instance_key TEXT NOT NULL,
|
|
40
|
+
element_id TEXT NOT NULL,
|
|
41
|
+
incarnation INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
lifecycle TEXT NOT NULL DEFAULT 'activation',
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
44
|
+
created_at TEXT NOT NULL,
|
|
45
|
+
completed_at TEXT,
|
|
46
|
+
first_offset INTEGER,
|
|
47
|
+
next_offset INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
PRIMARY KEY (process_instance_key, element_id)
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS ${SESSION_EVENT_TABLE} (
|
|
51
|
+
process_instance_key TEXT NOT NULL,
|
|
52
|
+
element_id TEXT NOT NULL,
|
|
53
|
+
event_offset INTEGER NOT NULL,
|
|
54
|
+
incarnation INTEGER NOT NULL,
|
|
55
|
+
event_id TEXT NOT NULL,
|
|
56
|
+
parent_id TEXT,
|
|
57
|
+
event_type TEXT NOT NULL,
|
|
58
|
+
payload TEXT NOT NULL,
|
|
59
|
+
appended_at TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (process_instance_key, element_id, event_offset)
|
|
61
|
+
);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS ${SESSION_CHECKPOINT_TABLE} (
|
|
63
|
+
process_instance_key TEXT NOT NULL,
|
|
64
|
+
element_id TEXT NOT NULL,
|
|
65
|
+
checkpoint_id TEXT NOT NULL,
|
|
66
|
+
checkpoint_offset INTEGER NOT NULL,
|
|
67
|
+
incarnation INTEGER NOT NULL,
|
|
68
|
+
commit_sha TEXT NOT NULL,
|
|
69
|
+
effect_ledger TEXT NOT NULL,
|
|
70
|
+
created_at TEXT NOT NULL,
|
|
71
|
+
PRIMARY KEY (process_instance_key, element_id, checkpoint_id)
|
|
72
|
+
);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_${SESSION_CHECKPOINT_TABLE}_offset ON ${SESSION_CHECKPOINT_TABLE} (process_instance_key, element_id, checkpoint_offset);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_${SESSION_LOG_TABLE}_retention ON ${SESSION_LOG_TABLE} (lifecycle, status, completed_at);`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/agentic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The Nano agentic protocol (ADR 0056): one app-tier channel carrying agent presence/registry, demand×supply, a shared blackboard and live terminal relay — with the wire contract, channel/hub, family modules and the operator cockpit, as subpath exports.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"transcript",
|
|
16
16
|
"blackboard",
|
|
17
17
|
"cockpit",
|
|
18
|
+
"session",
|
|
18
19
|
"conformance"
|
|
19
20
|
],
|
|
20
21
|
"publishConfig": {
|
|
@@ -63,6 +64,21 @@
|
|
|
63
64
|
"default": "./dist/relay/index.js"
|
|
64
65
|
},
|
|
65
66
|
"./source/relay": "./src/relay/index.ts",
|
|
67
|
+
"./session": {
|
|
68
|
+
"types": "./dist/session/index.d.ts",
|
|
69
|
+
"default": "./dist/session/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./source/session": "./src/session/index.ts",
|
|
72
|
+
"./session/acp": {
|
|
73
|
+
"types": "./dist/session/acp/index.d.ts",
|
|
74
|
+
"default": "./dist/session/acp/index.js"
|
|
75
|
+
},
|
|
76
|
+
"./source/session/acp": "./src/session/acp/index.ts",
|
|
77
|
+
"./session/normalizer": {
|
|
78
|
+
"types": "./dist/session/normalizer/index.d.ts",
|
|
79
|
+
"default": "./dist/session/normalizer/index.js"
|
|
80
|
+
},
|
|
81
|
+
"./source/session/normalizer": "./src/session/normalizer/index.ts",
|
|
66
82
|
"./transcript": {
|
|
67
83
|
"types": "./dist/transcript/index.d.ts",
|
|
68
84
|
"default": "./dist/transcript/index.js"
|
package/src/demand/model.test.ts
CHANGED
|
@@ -25,8 +25,8 @@ const AMBER_VOCAB: VocabDocument = {
|
|
|
25
25
|
};
|
|
26
26
|
const amberResolver = new VocabResolver(AMBER_VOCAB);
|
|
27
27
|
|
|
28
|
-
function leaf(taskType: string, process = "p", elementId = "e"): TaskDefinitionLeaf {
|
|
29
|
-
return { taskType, process, elementId };
|
|
28
|
+
function leaf(taskType: string, process = "p", elementId = "e", agentic = true): TaskDefinitionLeaf {
|
|
29
|
+
return { taskType, process, elementId, agentic };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function worker(instance: string, cognition: string, family: string, weight?: number): RegisteredWorker {
|
|
@@ -155,9 +155,9 @@ test("distinct demand: a token demanded by many elements is one entry", () => {
|
|
|
155
155
|
assert.equal(qa.tokens[0].supply, 1);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
test("non-
|
|
158
|
+
test("non-agentic (prompt-less) task types are surfaced but excluded from accounting", () => {
|
|
159
159
|
const report = computeDemandSupply({
|
|
160
|
-
taskDefinitions: [leaf("planning.planner"), leaf("legacy:job#bad#token")],
|
|
160
|
+
taskDefinitions: [leaf("planning.planner"), leaf("legacy:job#bad#token", "p", "e", false)],
|
|
161
161
|
workers: [worker("w", "planning", "gpt")],
|
|
162
162
|
resolver,
|
|
163
163
|
});
|
|
@@ -169,6 +169,84 @@ test("non-routing-token task types are surfaced but excluded from accounting", (
|
|
|
169
169
|
assert.deepEqual(report.missing, []);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
test("a prompt-less pr.* leaf is nonAgentic (not falsely reported missing)", () => {
|
|
173
|
+
// `pr.retro-record` parses as a routing token, but with no prompt link it is a
|
|
174
|
+
// deterministic in-process worker — it must NOT show as missing agentic demand.
|
|
175
|
+
const report = computeDemandSupply({
|
|
176
|
+
taskDefinitions: [leaf("pr.retro-record", "p", "e", false)],
|
|
177
|
+
workers: [],
|
|
178
|
+
resolver,
|
|
179
|
+
});
|
|
180
|
+
assert.deepEqual(report.nonAgentic, ["pr.retro-record"]);
|
|
181
|
+
assert.deepEqual(report.networks, []);
|
|
182
|
+
assert.deepEqual(report.missing, []);
|
|
183
|
+
assert.equal(report.status, "green");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a prompt-bearing colon-form fleet type is agentic demand, not nonAgentic", () => {
|
|
187
|
+
// `senior:retro` / `senior:rebase` do not parse as routing tokens, but carry a
|
|
188
|
+
// prompt link → they must appear as agentic demand (bucketed), never dropped.
|
|
189
|
+
const report = computeDemandSupply({
|
|
190
|
+
taskDefinitions: [leaf("senior:retro"), leaf("senior:rebase")],
|
|
191
|
+
workers: [],
|
|
192
|
+
resolver,
|
|
193
|
+
});
|
|
194
|
+
assert.deepEqual(report.nonAgentic, []);
|
|
195
|
+
assert.deepEqual(
|
|
196
|
+
report.networks.map((n) => n.network),
|
|
197
|
+
["senior"],
|
|
198
|
+
);
|
|
199
|
+
const senior = report.networks[0];
|
|
200
|
+
assert.deepEqual(
|
|
201
|
+
senior.tokens.map((t) => t.token),
|
|
202
|
+
["senior:rebase", "senior:retro"],
|
|
203
|
+
);
|
|
204
|
+
assert.deepEqual(senior.missing, ["senior:rebase", "senior:retro"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Defect-class guard: classification must follow the prompt signal, NOT the
|
|
208
|
+
// token string. This table pairs adversarial type strings (colon-form, dot-form,
|
|
209
|
+
// bare, malformed) with a hasPrompt flag and asserts agentic-ness tracks the
|
|
210
|
+
// flag alone — locking the class so a future naming convention can't re-invert
|
|
211
|
+
// the buckets.
|
|
212
|
+
const CLASSIFICATION_CASES: ReadonlyArray<{ type: string; hasPrompt: boolean }> = [
|
|
213
|
+
{ type: "senior:retro", hasPrompt: true },
|
|
214
|
+
{ type: "senior:retro", hasPrompt: false },
|
|
215
|
+
{ type: "planning.planner", hasPrompt: true },
|
|
216
|
+
{ type: "planning.planner", hasPrompt: false },
|
|
217
|
+
{ type: "pr.retro-record", hasPrompt: true },
|
|
218
|
+
{ type: "pr.retro-record", hasPrompt: false },
|
|
219
|
+
{ type: "decide", hasPrompt: true },
|
|
220
|
+
{ type: "decide", hasPrompt: false },
|
|
221
|
+
{ type: "legacy:job#bad#token", hasPrompt: true },
|
|
222
|
+
{ type: "legacy:job#bad#token", hasPrompt: false },
|
|
223
|
+
{ type: "UPPER::weird!!", hasPrompt: true },
|
|
224
|
+
{ type: "UPPER::weird!!", hasPrompt: false },
|
|
225
|
+
{ type: "", hasPrompt: true },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const { type, hasPrompt } of CLASSIFICATION_CASES) {
|
|
229
|
+
test(`classification follows the prompt signal, not the token: ${JSON.stringify(type)} hasPrompt=${hasPrompt}`, () => {
|
|
230
|
+
const report = computeDemandSupply({
|
|
231
|
+
taskDefinitions: [{ taskType: type, process: "p", elementId: "e", agentic: hasPrompt }],
|
|
232
|
+
workers: [],
|
|
233
|
+
resolver,
|
|
234
|
+
});
|
|
235
|
+
if (hasPrompt) {
|
|
236
|
+
// Prompt-bearing → agentic demand: bucketed, never dumped in nonAgentic,
|
|
237
|
+
// and admitted without throwing regardless of how pathological the string.
|
|
238
|
+
assert.deepEqual(report.nonAgentic, []);
|
|
239
|
+
assert.equal(report.networks.length, 1);
|
|
240
|
+
assert.equal(report.networks[0].tokens[0].token, type);
|
|
241
|
+
} else {
|
|
242
|
+
// Prompt-less → nonAgentic, whatever the string parses to.
|
|
243
|
+
assert.deepEqual(report.nonAgentic, [type]);
|
|
244
|
+
assert.deepEqual(report.networks, []);
|
|
245
|
+
assert.deepEqual(report.missing, []);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
172
250
|
test("the report is deterministic: lists sorted regardless of input order", () => {
|
|
173
251
|
const a = computeDemandSupply({
|
|
174
252
|
taskDefinitions: [leaf("qa.tester"), leaf("ci.runner"), leaf("planning.planner")],
|
package/src/demand/model.ts
CHANGED
|
@@ -72,10 +72,13 @@ export interface DemandSupplyReport {
|
|
|
72
72
|
/** The overall SLO state: worst of the missing-agent signal and diversity. */
|
|
73
73
|
readonly status: SloStatus;
|
|
74
74
|
/**
|
|
75
|
-
* Deployed `taskDefinition` types that
|
|
76
|
-
* (non-agentic) C8 jobs the engine also runs
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* Deployed `taskDefinition` types that carry NO `linkName="prompt"` linked
|
|
76
|
+
* resource — ordinary (non-agentic) in-process C8 jobs the engine also runs
|
|
77
|
+
* (e.g. the deterministic `pr.*` workers). Classification is by the prompt
|
|
78
|
+
* signal, not the token string, so a prompt-less type is `nonAgentic` even when
|
|
79
|
+
* it happens to parse as a routing token. Surfaced (not silently dropped) so an
|
|
80
|
+
* operator can spot a mis-modelled task, but excluded from the agentic
|
|
81
|
+
* demand×supply accounting.
|
|
79
82
|
*/
|
|
80
83
|
readonly nonAgentic: readonly string[];
|
|
81
84
|
}
|
|
@@ -90,14 +93,23 @@ export interface DemandSupplyInput {
|
|
|
90
93
|
readonly resolver: VocabResolver;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
96
|
+
/**
|
|
97
|
+
* A demanded token's bucket. For a valid routing token this is its network
|
|
98
|
+
* prefix (or a bare token's own role). For an arbitrary agentic type that is not
|
|
99
|
+
* a routing token (colon-form `senior:retro`, etc.) this is best-effort: the
|
|
100
|
+
* segment before the first `:` when present, else the whole type — a synthetic
|
|
101
|
+
* bucket so the leaf still surfaces as demand rather than being dropped. This is
|
|
102
|
+
* only ever called for prompt-bearing (agentic) leaves; non-agentic leaves are
|
|
103
|
+
* classified out before bucketing.
|
|
104
|
+
*/
|
|
105
|
+
function bucketOf(token: string): string {
|
|
95
106
|
try {
|
|
96
107
|
const parsed = parseToken(token);
|
|
97
108
|
// A bare (network-less) token like `decide` buckets under its own role name.
|
|
98
109
|
return parsed.network ?? parsed.role;
|
|
99
110
|
} catch {
|
|
100
|
-
|
|
111
|
+
const colon = token.indexOf(":");
|
|
112
|
+
return colon > 0 ? token.slice(0, colon) : token;
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -127,15 +139,24 @@ export function computeDemandSupply(input: DemandSupplyInput): DemandSupplyRepor
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
const demandTokens = distinctTaskTypes(input.taskDefinitions);
|
|
142
|
+
// Agentic-ness is the prompt signal, OR-folded across every leaf of a type: a
|
|
143
|
+
// type is agentic demand iff at least one deployed leaf carries a
|
|
144
|
+
// `linkName="prompt"` linked resource. This is independent of the type string,
|
|
145
|
+
// so colon-form fleet types (`senior:retro`) are admitted and prompt-less
|
|
146
|
+
// deterministic `pr.*` tasks are excluded — regardless of token grammar.
|
|
147
|
+
const agenticByToken = new Map<string, boolean>();
|
|
148
|
+
for (const leaf of input.taskDefinitions) {
|
|
149
|
+
agenticByToken.set(leaf.taskType, (agenticByToken.get(leaf.taskType) ?? false) || leaf.agentic);
|
|
150
|
+
}
|
|
130
151
|
const byNetwork = new Map<string, Map<string, TokenDemand>>();
|
|
131
152
|
const nonAgentic: string[] = [];
|
|
132
153
|
|
|
133
154
|
for (const token of demandTokens) {
|
|
134
|
-
|
|
135
|
-
if (network === undefined) {
|
|
155
|
+
if (!(agenticByToken.get(token) ?? false)) {
|
|
136
156
|
nonAgentic.push(token);
|
|
137
157
|
continue;
|
|
138
158
|
}
|
|
159
|
+
const network = bucketOf(token);
|
|
139
160
|
const instances = sortedUnique(supplyByToken.get(token) ?? []);
|
|
140
161
|
const demand: TokenDemand = {
|
|
141
162
|
token,
|
|
@@ -27,8 +27,8 @@ const MODEL = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
27
27
|
test("scans taskDefinition leaves with process and element provenance", () => {
|
|
28
28
|
const leaves = scanTaskDefinitions(MODEL);
|
|
29
29
|
assert.deepEqual(leaves, [
|
|
30
|
-
{ taskType: "planning.planner", process: "plan-fanout", elementId: "plan" },
|
|
31
|
-
{ taskType: "qa.tester", process: "plan-fanout", elementId: "test" },
|
|
30
|
+
{ taskType: "planning.planner", process: "plan-fanout", elementId: "plan", agentic: false },
|
|
31
|
+
{ taskType: "qa.tester", process: "plan-fanout", elementId: "test", agentic: false },
|
|
32
32
|
]);
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -69,17 +69,62 @@ test("tolerates whitespace around attribute equals signs (id / type / process id
|
|
|
69
69
|
</bpmn:definitions>`;
|
|
70
70
|
const leaves = scanTaskDefinitions(xml);
|
|
71
71
|
assert.deepEqual(leaves, [
|
|
72
|
-
{ taskType: "ci.runner", process: "spaced-proc", elementId: "t" },
|
|
72
|
+
{ taskType: "ci.runner", process: "spaced-proc", elementId: "t", agentic: false },
|
|
73
73
|
]);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test("distinctTaskTypes de-duplicates in first-occurrence order", () => {
|
|
77
77
|
assert.deepEqual(
|
|
78
78
|
distinctTaskTypes([
|
|
79
|
-
{ taskType: "b", process: "p", elementId: "1" },
|
|
80
|
-
{ taskType: "a", process: "p", elementId: "2" },
|
|
81
|
-
{ taskType: "b", process: "p", elementId: "3" },
|
|
79
|
+
{ taskType: "b", process: "p", elementId: "1", agentic: false },
|
|
80
|
+
{ taskType: "a", process: "p", elementId: "2", agentic: false },
|
|
81
|
+
{ taskType: "b", process: "p", elementId: "3", agentic: false },
|
|
82
82
|
]),
|
|
83
83
|
["b", "a"],
|
|
84
84
|
);
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
test("marks a leaf agentic when it carries a linkName=\"prompt\" linked resource", () => {
|
|
88
|
+
const xml = `<bpmn:definitions xmlns:bpmn="x" xmlns:zeebe="y">
|
|
89
|
+
<bpmn:process id="nwf-retro">
|
|
90
|
+
<bpmn:serviceTask id="retro">
|
|
91
|
+
<bpmn:extensionElements>
|
|
92
|
+
<zeebe:taskDefinition type="senior:retro" />
|
|
93
|
+
<zeebe:linkedResources>
|
|
94
|
+
<zeebe:linkedResource resourceId="retro.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
95
|
+
</zeebe:linkedResources>
|
|
96
|
+
</bpmn:extensionElements>
|
|
97
|
+
</bpmn:serviceTask>
|
|
98
|
+
<bpmn:serviceTask id="gather">
|
|
99
|
+
<bpmn:extensionElements>
|
|
100
|
+
<zeebe:taskDefinition type="pr.retro-gather" />
|
|
101
|
+
</bpmn:extensionElements>
|
|
102
|
+
</bpmn:serviceTask>
|
|
103
|
+
</bpmn:process>
|
|
104
|
+
</bpmn:definitions>`;
|
|
105
|
+
const leaves = scanTaskDefinitions(xml);
|
|
106
|
+
assert.deepEqual(leaves, [
|
|
107
|
+
{ taskType: "senior:retro", process: "nwf-retro", elementId: "retro", agentic: true },
|
|
108
|
+
{ taskType: "pr.retro-gather", process: "nwf-retro", elementId: "gather", agentic: false },
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("prompt detection tolerates attribute ordering (linkName before resourceId)", () => {
|
|
113
|
+
const xml = `<bpmn:definitions xmlns:bpmn="x" xmlns:zeebe="y">
|
|
114
|
+
<bpmn:serviceTask id="t">
|
|
115
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
116
|
+
<zeebe:linkedResource linkName="prompt" resourceId="feature.md" />
|
|
117
|
+
</bpmn:serviceTask>
|
|
118
|
+
</bpmn:definitions>`;
|
|
119
|
+
assert.equal(scanTaskDefinitions(xml)[0].agentic, true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a non-prompt linked resource (other linkName) does not mark a leaf agentic", () => {
|
|
123
|
+
const xml = `<bpmn:definitions xmlns:bpmn="x" xmlns:zeebe="y">
|
|
124
|
+
<bpmn:serviceTask id="t">
|
|
125
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
126
|
+
<zeebe:linkedResource resourceId="schema.md" linkName="schema" />
|
|
127
|
+
</bpmn:serviceTask>
|
|
128
|
+
</bpmn:definitions>`;
|
|
129
|
+
assert.equal(scanTaskDefinitions(xml)[0].agentic, false);
|
|
130
|
+
});
|
package/src/demand/taskdef.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface TaskDefinitionLeaf {
|
|
|
22
22
|
readonly process: string;
|
|
23
23
|
/** The service-task element id carrying the leaf (best-effort, may be empty). */
|
|
24
24
|
readonly elementId: string;
|
|
25
|
+
/**
|
|
26
|
+
* True when the service task carries a `<zeebe:linkedResource … linkName="prompt">`
|
|
27
|
+
* — its base-prompt side-car. This is the *structural* agentic signal (per nwf
|
|
28
|
+
* `SPEC.md` §"Agent job contract"): every external agent task delivers its base
|
|
29
|
+
* prompt through a `linkName="prompt"` linked resource, and no in-process
|
|
30
|
+
* (deterministic) worker task does. Agentic-ness is read from this flag, NOT
|
|
31
|
+
* from the `taskType` string — the deployed models use arbitrary type strings
|
|
32
|
+
* (colon-form `senior:retro`, dot-form, bare) that a routing-token grammar
|
|
33
|
+
* would mis-classify.
|
|
34
|
+
*/
|
|
35
|
+
readonly agentic: boolean;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
function attr(tag: string, name: string): string {
|
|
@@ -34,13 +45,31 @@ function processId(xml: string): string {
|
|
|
34
45
|
return match ? match[1] : "";
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
/**
|
|
49
|
+
* True when a service-task body carries a `linkName="prompt"` linked resource —
|
|
50
|
+
* the base-prompt side-car that marks a task as external agentic work.
|
|
51
|
+
*
|
|
52
|
+
* Scans every `<zeebe:linkedResource …>` in the body (tolerating attribute
|
|
53
|
+
* ordering and self-closing tags, consistent with {@link scanTaskDefinitions})
|
|
54
|
+
* and returns true as soon as one carries `linkName="prompt"`.
|
|
55
|
+
*/
|
|
56
|
+
function hasPromptLink(body: string): boolean {
|
|
57
|
+
const linkRe = /<zeebe:linkedResource\b[^>]*?\/?>/g;
|
|
58
|
+
let link: RegExpExecArray | null;
|
|
59
|
+
while ((link = linkRe.exec(body)) !== null) {
|
|
60
|
+
if (attr(link[0], "linkName") === "prompt") return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
/**
|
|
38
66
|
* Scan one BPMN document for its service-task `taskDefinition` leaves.
|
|
39
67
|
*
|
|
40
68
|
* Every `<bpmn:serviceTask>` carrying a `<zeebe:taskDefinition type="…">` with a
|
|
41
69
|
* non-empty type yields a leaf; tasks without a task definition (or with an empty
|
|
42
70
|
* type) are skipped. The scan is order-preserving and tolerant of attribute
|
|
43
|
-
* ordering and self-closing task-definition tags.
|
|
71
|
+
* ordering and self-closing task-definition tags. Each leaf also records whether
|
|
72
|
+
* the task carries a `linkName="prompt"` linked resource (its `agentic` flag).
|
|
44
73
|
*/
|
|
45
74
|
export function scanTaskDefinitions(xml: string): TaskDefinitionLeaf[] {
|
|
46
75
|
const proc = processId(xml);
|
|
@@ -55,7 +84,7 @@ export function scanTaskDefinitions(xml: string): TaskDefinitionLeaf[] {
|
|
|
55
84
|
if (!tdMatch) continue;
|
|
56
85
|
const taskType = attr(tdMatch[0], "type");
|
|
57
86
|
if (!taskType) continue;
|
|
58
|
-
out.push({ taskType, elementId, process: proc });
|
|
87
|
+
out.push({ taskType, elementId, process: proc, agentic: hasPromptLink(body) });
|
|
59
88
|
}
|
|
60
89
|
return out;
|
|
61
90
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * as presence from "./presence/index.ts";
|
|
|
12
12
|
export * as vocab from "./vocab/index.ts";
|
|
13
13
|
export * as demand from "./demand/index.ts";
|
|
14
14
|
export * as relay from "./relay/index.ts";
|
|
15
|
+
export * as session from "./session/index.ts";
|
|
15
16
|
export * as transcript from "./transcript/index.ts";
|
|
16
17
|
export * as blackboard from "./blackboard/index.ts";
|
|
17
18
|
export * as cockpit from "./cockpit/index.ts";
|