pmx-canvas 0.1.34 → 0.1.36

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.
@@ -157,6 +157,22 @@ export interface PmxAxFocusContext extends PmxAxFocusState {
157
157
  nodes: AgentContextNode[];
158
158
  }
159
159
 
160
+ // ── Pending agent-actionable digest (open canvas-bound items) ──────
161
+ export interface PendingAxActivityItem {
162
+ kind: 'work-item' | 'approval-gate' | 'elicitation' | 'mode-request';
163
+ id: string;
164
+ title: string;
165
+ status: string;
166
+ nodeIds: string[];
167
+ source: PmxAxSource | null;
168
+ }
169
+
170
+ // ── Delivery lead block (compact, un-truncated; for per-turn injection) ──
171
+ export interface PmxAxDeliveryContext {
172
+ pendingSteering: PmxAxSteeringMessage[];
173
+ pendingActivity: PendingAxActivityItem[];
174
+ }
175
+
160
176
  export interface PmxAxContext {
161
177
  version: 1;
162
178
  generatedAt: string;
@@ -164,6 +180,10 @@ export interface PmxAxContext {
164
180
  nodeCount: number;
165
181
  edgeCount: number;
166
182
  };
183
+ // Compact, loop-safe lead block of what's awaiting the agent right now
184
+ // (undelivered steering + open work/approvals/elicitations/mode requests).
185
+ // Sits ABOVE the full dump so a host adapter can inject it un-truncated.
186
+ delivery: PmxAxDeliveryContext;
167
187
  pinned: PmxAxPinnedContext;
168
188
  focus: PmxAxFocusContext;
169
189
  workItems: PmxAxWorkItem[];
@@ -176,6 +196,44 @@ export interface PmxAxContext {
176
196
  host: PmxAxHostCapability | null;
177
197
  }
178
198
 
199
+ const OPEN_AX_WORK_STATUSES = new Set<PmxAxWorkItemStatus>(['todo', 'in-progress', 'blocked']);
200
+
201
+ /**
202
+ * Open, agent-actionable canvas-bound AX items (open work items + pending approval
203
+ * gates / elicitations / mode requests). Unlike steering (a directive routed through
204
+ * the claim/ack delivery queue), these are STATE the human curates in the browser —
205
+ * they fire `ax-state-changed` (so resource-subscribers are pushed canvas://ax-work),
206
+ * but an adapterless client that only POLLS the delivery surface never saw them.
207
+ * Optionally excludes items the consumer itself originated (loop prevention),
208
+ * mirroring getPendingSteering. Shared by the MCP delivery surface and the HTTP
209
+ * context lead block so the digest never drifts between the two.
210
+ */
211
+ export function buildPendingAxActivity(state: PmxAxState, consumer?: string): PendingAxActivityItem[] {
212
+ const notMine = (source: PmxAxSource | null) => !consumer || source !== consumer;
213
+ const out: PendingAxActivityItem[] = [];
214
+ for (const w of state.workItems) {
215
+ if (OPEN_AX_WORK_STATUSES.has(w.status) && notMine(w.source)) {
216
+ out.push({ kind: 'work-item', id: w.id, title: w.title, status: w.status, nodeIds: w.nodeIds, source: w.source });
217
+ }
218
+ }
219
+ for (const g of state.approvalGates) {
220
+ if (g.status === 'pending' && notMine(g.source)) {
221
+ out.push({ kind: 'approval-gate', id: g.id, title: g.title, status: g.status, nodeIds: g.nodeIds, source: g.source });
222
+ }
223
+ }
224
+ for (const e of state.elicitations) {
225
+ if (e.status === 'pending' && notMine(e.source)) {
226
+ out.push({ kind: 'elicitation', id: e.id, title: e.prompt, status: e.status, nodeIds: e.nodeIds, source: e.source });
227
+ }
228
+ }
229
+ for (const m of state.modeRequests) {
230
+ if (m.status === 'pending' && notMine(m.source)) {
231
+ out.push({ kind: 'mode-request', id: m.id, title: m.reason ? `${m.mode}: ${m.reason}` : `mode: ${m.mode}`, status: m.status, nodeIds: m.nodeIds, source: m.source });
232
+ }
233
+ }
234
+ return out;
235
+ }
236
+
179
237
  const AX_SOURCES = new Set<PmxAxSource>(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']);
180
238
 
181
239
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -207,6 +265,33 @@ function normalizeNodeIds(value: unknown, validNodeIds?: Set<string>): string[]
207
265
 
208
266
  const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command']);
209
267
 
268
+ // ── Activity ingestion (harness-forwarded tool/session events) ─────
269
+ // A normalized activity the agent's harness forwards; the board auto-reacts.
270
+ // The precise activity kind is preserved on the recorded event's `data.activityKind`;
271
+ // only the (coarser) timeline event kind is constrained to PmxAxEventKind.
272
+ export type PmxAxActivityKind =
273
+ | 'tool-start' | 'tool-result' | 'failure' | 'error'
274
+ | 'session-start' | 'session-end' | 'command' | 'note';
275
+ const AX_ACTIVITY_KINDS = new Set<PmxAxActivityKind>([
276
+ 'tool-start', 'tool-result', 'failure', 'error', 'session-start', 'session-end', 'command', 'note',
277
+ ]);
278
+ const ACTIVITY_TO_EVENT_KIND: Record<PmxAxActivityKind, PmxAxEventKind> = {
279
+ 'tool-start': 'tool-start',
280
+ 'tool-result': 'tool-result',
281
+ 'failure': 'failure',
282
+ 'error': 'failure',
283
+ 'session-start': 'assistant-message',
284
+ 'session-end': 'assistant-message',
285
+ 'command': 'command',
286
+ 'note': 'assistant-message',
287
+ };
288
+ export function isAxActivityKind(value: unknown): value is PmxAxActivityKind {
289
+ return typeof value === 'string' && AX_ACTIVITY_KINDS.has(value as PmxAxActivityKind);
290
+ }
291
+ export function mapAxActivityKindToEventKind(kind: PmxAxActivityKind): PmxAxEventKind {
292
+ return ACTIVITY_TO_EVENT_KIND[kind];
293
+ }
294
+
210
295
  // ── Command registry (plan-004 Phase 5) ────────────────────────
211
296
  // Named, registry-gated PMX intents — NOT arbitrary execution. Invoking a command
212
297
  // records a timeline event a host/agent can observe and act on; unknown names are
@@ -725,6 +810,7 @@ export function normalizeAxState(input: unknown, validNodeIds?: Set<string>): Pm
725
810
 
726
811
  export function buildAxContext(input: {
727
812
  layout: CanvasLayout;
813
+ delivery: PmxAxDeliveryContext;
728
814
  pinned: PmxAxPinnedContext;
729
815
  focus: PmxAxFocusState;
730
816
  focusNodes: AgentContextNode[];
@@ -744,6 +830,7 @@ export function buildAxContext(input: {
744
830
  nodeCount: input.layout.nodes.length,
745
831
  edgeCount: input.layout.edges.length,
746
832
  },
833
+ delivery: input.delivery,
747
834
  pinned: input.pinned,
748
835
  focus: {
749
836
  ...input.focus,
@@ -0,0 +1,56 @@
1
+ import { canvasState } from './canvas-state.js';
2
+
3
+ /** Hard ceiling on a single blocking wait, regardless of the requested timeout. */
4
+ export const AX_WAIT_MAX_MS = 120000;
5
+
6
+ export interface AxWaitResult<T> {
7
+ /** Latest value, or null if the item does not exist / vanished mid-wait. */
8
+ value: T | null;
9
+ /** True only when the item still exists and is still pending after the wait. */
10
+ pending: boolean;
11
+ }
12
+
13
+ /**
14
+ * Block until a canvas-bound AX item resolves (its status leaves `pending`), the
15
+ * timeout elapses, or the request aborts — the server side of report primitive D
16
+ * ("gates that actually gate"). Resolves immediately when the item is already
17
+ * resolved, missing, or `timeoutMs <= 0` (a plain single read). Subscribes to the
18
+ * `ax` change channel and always disposes the listener + timer.
19
+ */
20
+ export async function waitForAxResolution<T extends { status: string }>(opts: {
21
+ read: () => T | null;
22
+ isResolved: (value: T) => boolean;
23
+ timeoutMs: number;
24
+ signal?: AbortSignal;
25
+ }): Promise<AxWaitResult<T>> {
26
+ const { read, isResolved } = opts;
27
+ const timeoutMs = Math.max(0, Math.min(opts.timeoutMs, AX_WAIT_MAX_MS));
28
+ const pendingOf = (v: T | null): boolean => (v ? !isResolved(v) : false);
29
+
30
+ const current = read();
31
+ if (!current || isResolved(current) || timeoutMs === 0 || opts.signal?.aborted) {
32
+ return { value: current, pending: pendingOf(current) };
33
+ }
34
+
35
+ return new Promise<AxWaitResult<T>>((resolve) => {
36
+ let done = false;
37
+ const finish = (): void => {
38
+ if (done) return;
39
+ done = true;
40
+ clearTimeout(timer);
41
+ dispose();
42
+ opts.signal?.removeEventListener('abort', onSettle);
43
+ const v = read();
44
+ resolve({ value: v, pending: pendingOf(v) });
45
+ };
46
+ const onSettle = finish;
47
+ const check = (type: string): void => {
48
+ if (type !== 'ax') return;
49
+ const v = read();
50
+ if (!v || isResolved(v)) finish();
51
+ };
52
+ const timer = setTimeout(finish, timeoutMs);
53
+ const dispose = canvasState.onChange(check);
54
+ opts.signal?.addEventListener('abort', onSettle, { once: true });
55
+ });
56
+ }
@@ -70,6 +70,8 @@ import {
70
70
  listAxCommands,
71
71
  AX_COMMAND_REGISTRY,
72
72
  normalizeAxPolicy,
73
+ mapAxActivityKindToEventKind,
74
+ type PmxAxActivityKind,
73
75
  type PmxAxElicitation,
74
76
  type PmxAxModeRequest,
75
77
  type PmxAxMode,
@@ -334,13 +336,25 @@ class CanvasStateManager {
334
336
  // ── Change listeners (for MCP resource notifications) ──────
335
337
  private _changeListeners: ((type: CanvasChangeType) => void)[] = [];
336
338
 
337
- /** Register a listener for state changes. Used by MCP server to emit resource notifications. */
338
- onChange(cb: (type: CanvasChangeType) => void): void {
339
+ /**
340
+ * Register a listener for state changes. Used by MCP server to emit resource
341
+ * notifications and by the blocking-wait endpoints to await an AX transition.
342
+ * Returns a disposer that unregisters the listener (callers that don't need it
343
+ * — e.g. the long-lived MCP subscription — may ignore the return value).
344
+ */
345
+ onChange(cb: (type: CanvasChangeType) => void): () => void {
339
346
  this._changeListeners.push(cb);
347
+ return () => {
348
+ const i = this._changeListeners.indexOf(cb);
349
+ if (i >= 0) this._changeListeners.splice(i, 1);
350
+ };
340
351
  }
341
352
 
342
353
  private notifyChange(type: CanvasChangeType): void {
343
- for (const cb of this._changeListeners) {
354
+ // Iterate a snapshot: a listener (e.g. a blocking-wait via onChange) may dispose
355
+ // itself synchronously here, and splicing the live array mid-iteration would skip
356
+ // the next listener for this notification.
357
+ for (const cb of [...this._changeListeners]) {
344
358
  try {
345
359
  cb(type);
346
360
  } catch (error) {
@@ -2049,6 +2063,19 @@ class CanvasStateManager {
2049
2063
  return applied.modeRequests.find((m) => m.id === id) ?? null;
2050
2064
  }
2051
2065
 
2066
+ // ── Single-item AX readers (canvas-bound; for the blocking-wait endpoints) ──
2067
+ getApproval(id: string): PmxAxApprovalGate | null {
2068
+ return this.getAxState().approvalGates.find((g) => g.id === id) ?? null;
2069
+ }
2070
+
2071
+ getElicitation(id: string): PmxAxElicitation | null {
2072
+ return this.getAxState().elicitations.find((e) => e.id === id) ?? null;
2073
+ }
2074
+
2075
+ getModeRequest(id: string): PmxAxModeRequest | null {
2076
+ return this.getAxState().modeRequests.find((m) => m.id === id) ?? null;
2077
+ }
2078
+
2052
2079
  getCommandRegistry(): PmxAxCommandDescriptor[] {
2053
2080
  return listAxCommands();
2054
2081
  }
@@ -2171,6 +2198,107 @@ class CanvasStateManager {
2171
2198
  }
2172
2199
  }
2173
2200
 
2201
+ /**
2202
+ * Ingest a normalized agent activity (a tool/session event a harness forwards)
2203
+ * and apply kind-driven board reactions, so the agent's real work flows back into
2204
+ * the board without it remembering to push each item (report primitive A — makes
2205
+ * AX bidirectional). Always records a timeline event; then, unless the caller
2206
+ * overrides/suppresses via `reactions`, applies defaults by kind/outcome:
2207
+ * • failure | error | outcome==='failure' → work item (blocked) + review
2208
+ * (finding/error, anchored to a valid nodeId else the `ref` file) + evidence (logs)
2209
+ * • tool-result + outcome==='success' → evidence (tool-result)
2210
+ * • everything else (tool-start, session-*, command, note) → event only
2211
+ * A reaction value of `false` suppresses it; an object overrides its fields/forces it on.
2212
+ */
2213
+ ingestActivity(
2214
+ input: {
2215
+ kind: PmxAxActivityKind;
2216
+ title: string;
2217
+ summary?: string | null;
2218
+ outcome?: 'success' | 'failure';
2219
+ ref?: string | null;
2220
+ nodeIds?: string[];
2221
+ data?: Record<string, unknown> | null;
2222
+ reactions?: {
2223
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
2224
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
2225
+ review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
2226
+ };
2227
+ },
2228
+ options: { source?: PmxAxSource } = {},
2229
+ ): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
2230
+ const source = options.source ?? 'api';
2231
+ const summary = input.summary ?? input.title;
2232
+ const isFailure = input.kind === 'failure' || input.kind === 'error' || input.outcome === 'failure';
2233
+ const isToolSuccess = input.kind === 'tool-result' && input.outcome === 'success';
2234
+ const nodeIds = input.nodeIds ?? [];
2235
+ const anchorNodeId = nodeIds.find((n) => this.nodes.has(n)) ?? null;
2236
+
2237
+ // (1) Always record the activity on the timeline (precise kind on data.activityKind).
2238
+ const event = this.recordAxEvent(
2239
+ {
2240
+ kind: mapAxActivityKindToEventKind(input.kind),
2241
+ summary: input.title,
2242
+ detail: input.summary ?? null,
2243
+ nodeIds,
2244
+ // Caller data first so the canonical fields always win — a malformed/hostile
2245
+ // payload can't overwrite activityKind/outcome/ref (which the docstring +
2246
+ // reaction logic treat as authoritative).
2247
+ data: {
2248
+ ...(input.data ?? {}),
2249
+ activityKind: input.kind,
2250
+ ...(input.outcome ? { outcome: input.outcome } : {}),
2251
+ ...(input.ref ? { ref: input.ref } : {}),
2252
+ },
2253
+ },
2254
+ { source },
2255
+ );
2256
+
2257
+ // (2) Resolve reactions: kind-driven defaults, overridable per call.
2258
+ const r = input.reactions ?? {};
2259
+ const wantWorkItem = r.workItem === false ? null : (r.workItem ?? (isFailure ? {} : null));
2260
+ const wantEvidence = r.evidence === false
2261
+ ? null
2262
+ : (r.evidence ?? (isFailure ? { kind: 'logs' as PmxAxEvidenceKind } : isToolSuccess ? { kind: 'tool-result' as PmxAxEvidenceKind } : null));
2263
+ const wantReview = r.review === false ? null : (r.review ?? (isFailure ? {} : null));
2264
+
2265
+ let workItem: PmxAxWorkItem | null = null;
2266
+ if (wantWorkItem) {
2267
+ workItem = this.addWorkItem(
2268
+ { title: input.title, status: wantWorkItem.status ?? 'blocked', detail: wantWorkItem.detail ?? summary, nodeIds },
2269
+ { source },
2270
+ );
2271
+ }
2272
+
2273
+ let evidence: PmxAxEvidence | null = null;
2274
+ if (wantEvidence) {
2275
+ evidence = this.addEvidence(
2276
+ { kind: wantEvidence.kind ?? 'logs', title: input.title, body: wantEvidence.body ?? input.summary ?? null, ref: input.ref ?? null, nodeIds },
2277
+ { source },
2278
+ );
2279
+ }
2280
+
2281
+ let review: PmxAxReviewAnnotation | null = null;
2282
+ if (wantReview) {
2283
+ const reviewNodeId = wantReview.nodeId ?? anchorNodeId;
2284
+ // addReviewAnnotation returns null on a bad node anchor — that just skips the
2285
+ // review; it never fails the whole ingest (the event + other reactions stand).
2286
+ review = this.addReviewAnnotation(
2287
+ {
2288
+ body: summary,
2289
+ kind: wantReview.kind ?? 'finding',
2290
+ severity: wantReview.severity ?? 'error',
2291
+ ...(wantReview.anchorType ? { anchorType: wantReview.anchorType } : {}),
2292
+ ...(reviewNodeId ? { nodeId: reviewNodeId } : {}),
2293
+ ...(input.ref ? { file: input.ref } : {}),
2294
+ },
2295
+ { source },
2296
+ );
2297
+ }
2298
+
2299
+ return { event, workItem, evidence, review };
2300
+ }
2301
+
2174
2302
  getAxEvents(q: AxTimelineQuery = {}): PmxAxEvent[] {
2175
2303
  return this._db ? loadAxEventsFromDB(this._db, q) : [];
2176
2304
  }
@@ -108,22 +108,60 @@ function injectIntoHead(html: string, content: string): string {
108
108
  * injected when the node's AX capabilities are enabled (opt-in for `html`), and
109
109
  * the server re-validates every interaction — so this is a convenience surface,
110
110
  * not a trust boundary.
111
+ *
112
+ * `emit` returns a Promise that resolves with the interaction result once the
113
+ * parent acks it (report #55 — built-in confirmation so a click no longer looks
114
+ * like "nothing happened"). Authors can also `window.PMX_AX.on('ack', cb)` or
115
+ * listen for the `pmx-ax-ack` CustomEvent. Resolves with an `ax-ack-timeout`
116
+ * result after 10s if no ack arrives (e.g. an older parent), so `await emit()`
117
+ * never hangs.
111
118
  */
112
119
  export function buildAxBridge(axToken: string, nodeId: string): string {
113
120
  const token = JSON.stringify(axToken);
114
121
  const node = JSON.stringify(nodeId);
115
122
  return `<script data-pmx-canvas-ax-bridge>
116
- const PMX_AX_TOKEN = ${token};
117
- const PMX_AX_NODE_ID = ${node};
118
- window.PMX_AX = window.PMX_AX || {};
119
- window.PMX_AX.emit = function (type, payload) {
120
- window.parent.postMessage({
121
- source: 'pmx-canvas-ax',
122
- token: PMX_AX_TOKEN,
123
- nodeId: PMX_AX_NODE_ID,
124
- interaction: { type: String(type), payload: payload && typeof payload === 'object' ? payload : {} },
125
- }, '*');
126
- };
123
+ (function () {
124
+ const PMX_AX_TOKEN = ${token};
125
+ const PMX_AX_NODE_ID = ${node};
126
+ window.PMX_AX = window.PMX_AX || {};
127
+ const pending = new Map();
128
+ const ackListeners = [];
129
+ let seq = 0;
130
+ window.PMX_AX.emit = function (type, payload) {
131
+ seq += 1;
132
+ const correlationId = PMX_AX_NODE_ID + '-' + seq + '-' + (Date.now ? Date.now() : 0);
133
+ window.parent.postMessage({
134
+ source: 'pmx-canvas-ax',
135
+ token: PMX_AX_TOKEN,
136
+ nodeId: PMX_AX_NODE_ID,
137
+ correlationId: correlationId,
138
+ interaction: { type: String(type), payload: payload && typeof payload === 'object' ? payload : {} },
139
+ }, '*');
140
+ return new Promise(function (resolve) {
141
+ const timer = setTimeout(function () {
142
+ pending.delete(correlationId);
143
+ // Match the real reject shape ({ ok:false, status, code:string, error }) so a
144
+ // surface inspecting r.code/r.status sees a consistent type on timeout vs reject.
145
+ resolve({ ok: false, status: 504, code: 'ax-ack-timeout', error: 'ax-ack-timeout' });
146
+ }, 10000);
147
+ pending.set(correlationId, function (result) { clearTimeout(timer); resolve(result); });
148
+ });
149
+ };
150
+ window.PMX_AX.on = function (eventType, cb) {
151
+ if (eventType === 'ack' && typeof cb === 'function') ackListeners.push(cb);
152
+ };
153
+ window.addEventListener('message', function (event) {
154
+ const m = event.data;
155
+ if (!m || m.source !== 'pmx-canvas-ax-ack' || m.token !== PMX_AX_TOKEN) return;
156
+ const result = m.result || { ok: false };
157
+ const resolver = m.correlationId ? pending.get(m.correlationId) : undefined;
158
+ if (resolver) { pending.delete(m.correlationId); resolver(result); }
159
+ for (let i = 0; i < ackListeners.length; i += 1) {
160
+ try { ackListeners[i](result, m.interaction || null); } catch (e) {}
161
+ }
162
+ try { window.dispatchEvent(new CustomEvent('pmx-ax-ack', { detail: { result: result, interaction: m.interaction || null } })); } catch (e) {}
163
+ });
164
+ })();
127
165
  </script>`;
128
166
  }
129
167
 
@@ -3,7 +3,9 @@ import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
4
4
  import { buildCanvasAxContext } from './ax-context.js';
5
5
  import { applyAxInteraction, type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
6
+ import { waitForAxResolution } from './ax-wait.js';
6
7
  import type {
8
+ PmxAxActivityKind,
7
9
  PmxAxApprovalGate,
8
10
  PmxAxCommandDescriptor,
9
11
  PmxAxContext,
@@ -512,8 +514,8 @@ export class PmxCanvas extends EventEmitter {
512
514
  return canvasState.getAxState();
513
515
  }
514
516
 
515
- getAxContext(): PmxAxContext {
516
- return buildCanvasAxContext();
517
+ getAxContext(options?: { consumer?: string }): PmxAxContext {
518
+ return buildCanvasAxContext(options?.consumer);
517
519
  }
518
520
 
519
521
  setAxFocus(nodeIds: string[], options?: { source?: PmxAxSource }): PmxAxFocusState {
@@ -710,6 +712,84 @@ export class PmxCanvas extends EventEmitter {
710
712
  return modeRequest;
711
713
  }
712
714
 
715
+ // ── Activity ingestion (primitive A — bidirectional board) ────────
716
+ ingestActivity(
717
+ input: {
718
+ kind: PmxAxActivityKind;
719
+ title: string;
720
+ summary?: string | null;
721
+ outcome?: 'success' | 'failure';
722
+ ref?: string | null;
723
+ nodeIds?: string[];
724
+ data?: Record<string, unknown> | null;
725
+ reactions?: {
726
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
727
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
728
+ review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
729
+ };
730
+ },
731
+ options?: { source?: PmxAxSource },
732
+ ): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
733
+ const result = canvasState.ingestActivity(input, { source: options?.source ?? 'sdk' });
734
+ emitPrimaryWorkbenchEvent('ax-event-created', { event: result.event });
735
+ if (result.workItem) emitPrimaryWorkbenchEvent('ax-state-changed', { workItem: result.workItem });
736
+ if (result.evidence) emitPrimaryWorkbenchEvent('ax-event-created', { evidence: result.evidence });
737
+ if (result.review) emitPrimaryWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review });
738
+ return result;
739
+ }
740
+
741
+ // ── Single-item readers + blocking waits (primitive D — gates that gate) ──
742
+ getApproval(id: string): PmxAxApprovalGate | null {
743
+ return canvasState.getApproval(id);
744
+ }
745
+
746
+ getElicitation(id: string): PmxAxElicitation | null {
747
+ return canvasState.getElicitation(id);
748
+ }
749
+
750
+ getModeRequest(id: string): PmxAxModeRequest | null {
751
+ return canvasState.getModeRequest(id);
752
+ }
753
+
754
+ async awaitApproval(
755
+ id: string,
756
+ options?: { timeoutMs?: number; signal?: AbortSignal },
757
+ ): Promise<{ approvalGate: PmxAxApprovalGate | null; pending: boolean }> {
758
+ const { value, pending } = await waitForAxResolution<PmxAxApprovalGate>({
759
+ read: () => canvasState.getApproval(id),
760
+ isResolved: (g) => g.status !== 'pending',
761
+ timeoutMs: options?.timeoutMs ?? 30000,
762
+ ...(options?.signal ? { signal: options.signal } : {}),
763
+ });
764
+ return { approvalGate: value, pending };
765
+ }
766
+
767
+ async awaitElicitation(
768
+ id: string,
769
+ options?: { timeoutMs?: number; signal?: AbortSignal },
770
+ ): Promise<{ elicitation: PmxAxElicitation | null; pending: boolean }> {
771
+ const { value, pending } = await waitForAxResolution<PmxAxElicitation>({
772
+ read: () => canvasState.getElicitation(id),
773
+ isResolved: (e) => e.status !== 'pending',
774
+ timeoutMs: options?.timeoutMs ?? 30000,
775
+ ...(options?.signal ? { signal: options.signal } : {}),
776
+ });
777
+ return { elicitation: value, pending };
778
+ }
779
+
780
+ async awaitMode(
781
+ id: string,
782
+ options?: { timeoutMs?: number; signal?: AbortSignal },
783
+ ): Promise<{ modeRequest: PmxAxModeRequest | null; pending: boolean }> {
784
+ const { value, pending } = await waitForAxResolution<PmxAxModeRequest>({
785
+ read: () => canvasState.getModeRequest(id),
786
+ isResolved: (m) => m.status !== 'pending',
787
+ timeoutMs: options?.timeoutMs ?? 30000,
788
+ ...(options?.signal ? { signal: options.signal } : {}),
789
+ });
790
+ return { modeRequest: value, pending };
791
+ }
792
+
713
793
  getCommandRegistry(): PmxAxCommandDescriptor[] {
714
794
  return canvasState.getCommandRegistry();
715
795
  }