macp-sdk-typescript 0.2.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -4,15 +4,138 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.3.0] - 2026-04-21
8
+
9
+ Parity release — brings TypeScript SDK to full feature parity with
10
+ `macp-sdk-python` 0.3.0. Plan: `plans/sdk-parity-plan.md`.
11
+
12
+ ### Added
13
+
14
+ - **Cancel-callback server** (Gap B, RFC-0001 §7.2 Option A):
15
+ `startCancelCallbackServer({ host, port, path, onCancel })` returns a
16
+ handle backed by Node's `http` module; `Participant` starts the server
17
+ automatically when `ParticipantConfig.cancelCallback` is set and closes
18
+ it on `stop()`. `fromBootstrap()` wires the new `cancel_callback`
19
+ field in the bootstrap JSON. Exports:
20
+ `startCancelCallbackServer`, `CancelCallbackServer`, `CancelHandler`.
21
+ - **Function-wrapped voting and commitment strategies** (Gap C):
22
+ `functionVoter(shouldVote, decideVote)` and
23
+ `functionCommitter(shouldCommit, decideCommitment)` in
24
+ `src/agent/strategies.ts` — the low-friction way to plug a custom rule
25
+ into a `Participant` without a class.
26
+ - **Standalone signal and progress builders** (Gap D):
27
+ `buildSignalPayload(input)` and `buildProgressPayload(input)` in
28
+ `src/envelope.ts`. `MacpClient.sendSignal` / `sendProgress` now use
29
+ the shared builders internally.
30
+ - **`serializeMessage(msg)` helper** (Gap N) — parity with
31
+ `python-sdk envelope.serialize_message`; dispatches to
32
+ `serializeBinary()` / `toBinary()` / `finish()` on the passed
33
+ protobuf message.
34
+ - **Structured logger** (Gap G): `src/logging.ts` exports `logger` with
35
+ `error`/`warn`/`info`/`debug` level methods and `configureLogging({ level, sink })`.
36
+ Reads `MACP_LOG_LEVEL` on module init (default `warn`). The two
37
+ remaining `console.*` callsites in `src/agent/participant.ts` and
38
+ `src/handoff.ts` now route through the structured logger.
39
+ - **`AckFailure` shape** (Gap I): structured NACK record exported from
40
+ `src/errors.ts`. Populated on `MacpAckError.failure` from both
41
+ `ack.error.details` and gRPC trailing metadata.
42
+ - **`BaseSession` / `BaseProjection`** (Gap J): abstract extension
43
+ points exported from `src/base-session.ts` and `src/projections/base.ts`
44
+ for custom mode helpers (modes registered via `registerExtMode`).
45
+ Mirrors `python-sdk`'s `BaseSession` / `BaseProjection`.
46
+ - **Named policy rule interfaces** (Gap K): `VotingRules`,
47
+ `ObjectionHandlingRules`, `EvaluationRules`, `CommitmentRules`,
48
+ `QuorumThreshold`, `AbstentionRules`, `ProposalAcceptanceRules`,
49
+ `CounterProposalRules`, `RejectionRules`, `TaskAssignmentRules`,
50
+ `TaskCompletionRules`, `HandoffAcceptanceRules` now exported as
51
+ first-class interfaces matching `macp-sdk-python` exports.
52
+ - **`STANDARD_MODES` tuple** (Gap L) in `src/constants.ts`.
53
+ - **Parity examples** (Gap M): `examples/policy-registration.ts`,
54
+ `examples/agent-policy-aware.ts`, `examples/direct-agent-auth-initiator.ts`,
55
+ `examples/direct-agent-auth-observer.ts`.
56
+
57
+ ### Changed
58
+
59
+ - **Error-code constants renamed** (Gap F, breaking): dropped the
60
+ `ERR_` prefix so TS constants match the on-the-wire strings and the
61
+ `macp-sdk-python` exports. Rename table —
62
+ `ERR_UNSUPPORTED_PROTOCOL_VERSION` → `UNSUPPORTED_PROTOCOL_VERSION`,
63
+ `ERR_INVALID_ENVELOPE` → `INVALID_ENVELOPE`,
64
+ `ERR_SESSION_NOT_FOUND` → `SESSION_NOT_FOUND`,
65
+ `ERR_SESSION_NOT_OPEN` → `SESSION_NOT_OPEN`,
66
+ `ERR_SESSION_ALREADY_EXISTS` → `SESSION_ALREADY_EXISTS`,
67
+ `ERR_MODE_NOT_SUPPORTED` → `MODE_NOT_SUPPORTED`,
68
+ `ERR_FORBIDDEN` → `FORBIDDEN`,
69
+ `ERR_UNAUTHENTICATED` → `UNAUTHENTICATED`,
70
+ `ERR_DUPLICATE_MESSAGE` → `DUPLICATE_MESSAGE`,
71
+ `ERR_PAYLOAD_TOO_LARGE` → `PAYLOAD_TOO_LARGE`,
72
+ `ERR_RATE_LIMITED` → `RATE_LIMITED`,
73
+ `ERR_INTERNAL_ERROR` → `INTERNAL_ERROR`,
74
+ `ERR_POLICY_DENIED` → `POLICY_DENIED`,
75
+ `ERR_INVALID_SESSION_ID` → `INVALID_SESSION_ID`,
76
+ `ERR_UNKNOWN_POLICY_VERSION` → `UNKNOWN_POLICY_VERSION`,
77
+ `ERR_INVALID_POLICY_DEFINITION` → `INVALID_POLICY_DEFINITION`.
78
+ - `MacpClient.clientVersion` default now `0.3.0`.
79
+
80
+ ### Removed
81
+
82
+ - **`HandoffSession.sendContext()`** (Gap A, breaking): the deprecated
83
+ alias (warning first emitted in 0.2.3) has been removed. Use
84
+ `HandoffSession.addContext()`.
85
+ - **`MacpAckError.reasons` getter** (breaking): use
86
+ `MacpAckError.failure.reasons` instead. `.failure` is the canonical
87
+ structured NACK record and matches `macp-sdk-python`'s
88
+ `MacpAckError.failure`.
89
+ - **Mode-prefixed policy rule type aliases** (breaking): the
90
+ `CommitmentRulesInput`, `DecisionVotingRules`,
91
+ `DecisionObjectionHandling`, `DecisionEvaluationRules`, and
92
+ `DecisionCommitmentRules` type aliases have been removed. Use the
93
+ unprefixed names (`CommitmentRules`, `VotingRules`,
94
+ `ObjectionHandlingRules`, `EvaluationRules`) introduced in Gap K.
95
+
96
+ ## [0.2.3] - 2026-04-21
8
97
 
9
98
  ### Added
99
+ - `MacpStream.sendSubscribe(sessionId, afterSequence?)` — subscribe-only stream
100
+ frame (RFC-MACP-0006-A1). The runtime replays accepted envelopes from the
101
+ cursor before switching to live broadcast, so non-initiator agents observe
102
+ `SessionStart` and earlier mode envelopes regardless of join order.
103
+ - `GrpcTransportAdapter` now sends a subscribe frame automatically on start so
104
+ `Participant`-based agents pick up history without bespoke wiring.
105
+ - Unit coverage for `MacpStream.sendSubscribe` (default cursor, custom cursor,
106
+ closed-stream rejection, write-error propagation, sequential resubscribe) and
107
+ `GrpcTransportAdapter` (subscribe ordering, empty-stream subscribe, auth
108
+ pass-through to `openStream`).
109
+ - Integration tests for late-subscriber replay and future-cursor skip
110
+ (`tests/integration/runtime.test.ts`).
111
+ - `InitiatorConfig.sessionStart` now accepts `contextId` and `extensions`, and
112
+ `Participant.emitInitiatorEnvelopes()` forwards both to the mode-session
113
+ `start()` call (SDK-TS-1). `fromBootstrap()` decodes `context_id` plus a
114
+ JSON-native `extensions` map from the bootstrap file — each value is
115
+ serialised as UTF-8 JSON into the `Record<string, Buffer>` shape the
116
+ envelope requires; pre-encoded `Buffer` / `Uint8Array` values pass through
117
+ unchanged.
118
+ - Documented `MacpClient.listSessions()` (parity with the python-sdk SDK-PY-2
119
+ gap) and `SessionLifecycleWatcher` (parity with python-sdk SDK-PY-3). Both
120
+ were already implemented — this release adds the API reference, a
121
+ `docs/guides/streaming.md` section, unit coverage for `listSessions`, and
122
+ integration tests that exercise enumeration + a `CREATED` lifecycle event
123
+ round-trip against a live runtime.
10
124
  - Warn-once migration hint when the deprecated `HandoffSession.sendContext()`
11
125
  alias is invoked. Scheduled for removal in `0.4.0`; use `addContext()`.
12
126
  - `docs/api/sessions.md` now documents the identity-guard contract.
13
127
  - `docs/guides/architecture.md` rewritten to reflect the three-layer design
14
128
  (transport / session helpers / agent framework).
15
129
 
130
+ ### Changed
131
+ - Integration tests updated to match the proto field renames that landed in
132
+ `@multiagentcoordinationprotocol/proto@0.1.x`: `Evaluation.analysis` →
133
+ `reason`, `Proposal.description` → `summary`, `TaskRequest.assignee` →
134
+ `requestedAssignee` (Task accept/update/complete/fail/reject now carry an
135
+ `assignee` field), `TaskUpdate.progress` is required, `HandoffContext.data`
136
+ → `context` (+ `contentType`), `HandoffAccept.acceptedBy` and
137
+ `HandoffDecline.declinedBy` are now required.
138
+
16
139
  ### Deprecated
17
140
  - `HandoffSession.sendContext()` remains available as an alias but emits a
18
141
  one-shot `console.warn`. Use `HandoffSession.addContext()` instead.
package/README.md CHANGED
@@ -141,10 +141,10 @@ import { TaskSession } from 'macp-sdk-typescript';
141
141
 
142
142
  const session = new TaskSession(client);
143
143
  await session.start({ intent: '...', participants: ['worker'], ttlMs: 120_000 });
144
- await session.request({ taskId: 't1', title: 'Build feature', instructions: '...' });
144
+ await session.requestTask({ taskId: 't1', title: 'Build feature', instructions: '...' });
145
145
  await session.acceptTask({ taskId: 't1', assignee: 'worker' });
146
- await session.update({ taskId: 't1', status: 'working', progress: 0.5, message: 'halfway' });
147
- await session.complete({ taskId: 't1', assignee: 'worker', summary: 'done' });
146
+ await session.updateTask({ taskId: 't1', status: 'working', progress: 0.5, message: 'halfway' });
147
+ await session.completeTask({ taskId: 't1', assignee: 'worker', summary: 'done' });
148
148
  await session.commit({ action: 'task.completed', authorityScope: 'lead', reason: '...' });
149
149
 
150
150
  // Projection queries
@@ -319,6 +319,17 @@ for await (const envelope of stream.responses()) {
319
319
  stream.close();
320
320
  ```
321
321
 
322
+ To attach to a session that is already in flight and receive accepted history before live broadcast (RFC-MACP-0006 §3.2 passive subscribe):
323
+
324
+ ```typescript
325
+ const stream = client.openStream({ auth });
326
+ await stream.sendSubscribe(sessionId); // full replay then live
327
+ // or, after a reconnect:
328
+ await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
329
+ ```
330
+
331
+ The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order.
332
+
322
333
  ### Registry & Root Watchers
323
334
 
324
335
  ```typescript
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Cancel-callback HTTP endpoint — RFC-0001 §7.2 Option A.
3
+ *
4
+ * Parity with python-sdk's `macp_sdk.agent.cancel_callback`. The
5
+ * examples-service's `BootstrapPayload.cancel_callback` field asks each
6
+ * agent to listen on `http://host:port{path}` for a `POST` whose JSON
7
+ * body is `{"runId": ..., "reason": ...}`. Receipt of the POST should
8
+ * stop the participant cleanly — typically by calling `participant.stop()`.
9
+ */
10
+ export type CancelHandler = (runId: string, reason: string) => void | Promise<void>;
11
+ export interface CancelCallbackServerOptions {
12
+ host: string;
13
+ port: number;
14
+ path: string;
15
+ onCancel: CancelHandler;
16
+ }
17
+ export interface CancelCallbackServer {
18
+ readonly host: string;
19
+ readonly port: number;
20
+ readonly path: string;
21
+ close(): Promise<void>;
22
+ }
23
+ export declare function startCancelCallbackServer(options: CancelCallbackServerOptions): Promise<CancelCallbackServer>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startCancelCallbackServer = startCancelCallbackServer;
37
+ const http = __importStar(require("node:http"));
38
+ const logging_1 = require("../logging");
39
+ function normalisePath(path) {
40
+ return path.startsWith('/') ? path : `/${path}`;
41
+ }
42
+ async function readBody(req) {
43
+ return new Promise((resolve, reject) => {
44
+ const chunks = [];
45
+ req.on('data', (chunk) => chunks.push(chunk));
46
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
47
+ req.on('error', reject);
48
+ });
49
+ }
50
+ function startCancelCallbackServer(options) {
51
+ const path = normalisePath(options.path);
52
+ return new Promise((resolve, reject) => {
53
+ const server = http.createServer(async (req, res) => {
54
+ if (req.method !== 'POST' || req.url !== path) {
55
+ res.statusCode = 404;
56
+ res.end();
57
+ return;
58
+ }
59
+ let body = {};
60
+ try {
61
+ const raw = await readBody(req);
62
+ if (raw)
63
+ body = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ body = {};
67
+ }
68
+ const runId = String(body.runId ?? body.run_id ?? '');
69
+ const reason = String(body.reason ?? '');
70
+ logging_1.logger.info('cancel_callback invoked', { runId, reason });
71
+ try {
72
+ await options.onCancel(runId, reason);
73
+ }
74
+ catch (err) {
75
+ logging_1.logger.error('cancel_callback handler raised', err);
76
+ res.statusCode = 500;
77
+ res.end();
78
+ return;
79
+ }
80
+ res.statusCode = 202;
81
+ res.setHeader('Content-Type', 'application/json');
82
+ res.end('{"ok":true}');
83
+ });
84
+ server.once('error', reject);
85
+ server.listen(options.port, options.host, () => {
86
+ server.removeListener('error', reject);
87
+ const addr = server.address();
88
+ const boundHost = addr.address;
89
+ const boundPort = addr.port;
90
+ logging_1.logger.debug(`cancel_callback listening on http://${boundHost}:${boundPort}${path}`);
91
+ let closing = false;
92
+ const handle = {
93
+ host: boundHost,
94
+ port: boundPort,
95
+ path,
96
+ close() {
97
+ if (closing)
98
+ return Promise.resolve();
99
+ closing = true;
100
+ return new Promise((res, rej) => {
101
+ server.close((err) => (err ? rej(err) : res()));
102
+ });
103
+ },
104
+ };
105
+ resolve(handle);
106
+ });
107
+ });
108
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './types';
2
+ export * from './cancel-callback';
2
3
  export * from './dispatcher';
3
4
  export * from './participant';
4
5
  export * from './strategies';
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.HttpTransportAdapter = exports.GrpcTransportAdapter = void 0;
18
18
  __exportStar(require("./types"), exports);
19
+ __exportStar(require("./cancel-callback"), exports);
19
20
  __exportStar(require("./dispatcher"), exports);
20
21
  __exportStar(require("./participant"), exports);
21
22
  __exportStar(require("./strategies"), exports);
@@ -1,5 +1,7 @@
1
1
  import type { AuthConfig } from '../auth';
2
2
  import type { MacpClient } from '../client';
3
+ import type { Envelope } from '../types';
4
+ import { type CancelCallbackServer } from './cancel-callback';
3
5
  import { type TransportAdapter } from './transports';
4
6
  import type { MessageHandler, PhaseChangeHandler, ProjectionLike, SessionActions, TerminalHandler } from './types';
5
7
  export interface InitiatorConfig {
@@ -7,6 +9,8 @@ export interface InitiatorConfig {
7
9
  intent: string;
8
10
  participants: string[];
9
11
  ttlMs: number;
12
+ contextId?: string;
13
+ extensions?: Record<string, Buffer>;
10
14
  context?: Record<string, unknown>;
11
15
  roots?: Array<{
12
16
  uri: string;
@@ -30,6 +34,17 @@ export interface ParticipantConfig {
30
34
  policyVersion?: string;
31
35
  transport?: TransportAdapter;
32
36
  initiator?: InitiatorConfig;
37
+ /**
38
+ * Bind a cancel-callback HTTP endpoint (RFC-0001 §7.2 Option A) when
39
+ * this participant's event loop starts. The server is closed
40
+ * automatically when {@link Participant.stop} is called. Parity with
41
+ * python-sdk's bootstrap `cancel_callback` field.
42
+ */
43
+ cancelCallback?: {
44
+ host: string;
45
+ port: number;
46
+ path: string;
47
+ };
33
48
  }
34
49
  export declare class Participant {
35
50
  readonly participantId: string;
@@ -43,15 +58,39 @@ export declare class Participant {
43
58
  private readonly session;
44
59
  private readonly transport;
45
60
  private readonly initiatorConfig?;
61
+ private readonly participants;
62
+ private readonly modeVersion?;
63
+ private readonly configurationVersion?;
64
+ private readonly policyVersion?;
46
65
  private running;
47
66
  private lastPhase;
67
+ private cancelCallbackServer?;
68
+ private readonly cancelCallbackConfig?;
48
69
  constructor(config: ParticipantConfig);
49
70
  private createModeSession;
50
71
  private buildActions;
51
72
  on(messageType: string, handler: MessageHandler): Participant;
52
73
  onPhaseChange(phase: string, handler: PhaseChangeHandler): Participant;
53
74
  onTerminal(handler: TerminalHandler): Participant;
75
+ /** Whether {@link stop} has been called (or the loop has exited). */
76
+ get isStopped(): boolean;
54
77
  run(): Promise<void>;
78
+ /**
79
+ * Manually process a single envelope. Parity with python-sdk's
80
+ * {@code Participant.process_event}. Useful for deterministic unit tests
81
+ * and for driving the participant from a foreign event loop without
82
+ * installing a {@link TransportAdapter}.
83
+ */
84
+ processEvent(envelope: Envelope): Promise<void>;
85
+ private buildHandlerContext;
86
+ /** Returns true if a terminal state was reached. */
87
+ private processMessage;
55
88
  stop(): Promise<void>;
89
+ /**
90
+ * Attach a running cancel-callback HTTP server to this participant.
91
+ * The server is closed automatically when {@link stop} is called.
92
+ * Parity with python-sdk's `Participant.attach_cancel_callback_server`.
93
+ */
94
+ attachCancelCallbackServer(server: CancelCallbackServer): void;
56
95
  private emitInitiatorEnvelopes;
57
96
  }
@@ -4,12 +4,15 @@ exports.Participant = void 0;
4
4
  const constants_1 = require("../constants");
5
5
  const decision_1 = require("../decision");
6
6
  const handoff_1 = require("../handoff");
7
+ const logging_1 = require("../logging");
7
8
  const decision_2 = require("../projections/decision");
8
9
  const proposal_1 = require("../proposal");
9
10
  const quorum_1 = require("../quorum");
10
11
  const task_1 = require("../task");
12
+ const cancel_callback_1 = require("./cancel-callback");
11
13
  const dispatcher_1 = require("./dispatcher");
12
14
  const transports_1 = require("./transports");
15
+ const TERMINAL_PHASES = new Set(['Committed', 'Accepted', 'Declined', 'Cancelled', 'TerminalRejected']);
13
16
  class Participant {
14
17
  participantId;
15
18
  sessionId;
@@ -22,8 +25,14 @@ class Participant {
22
25
  session;
23
26
  transport;
24
27
  initiatorConfig;
28
+ participants;
29
+ modeVersion;
30
+ configurationVersion;
31
+ policyVersion;
25
32
  running = false;
26
33
  lastPhase;
34
+ cancelCallbackServer;
35
+ cancelCallbackConfig;
27
36
  constructor(config) {
28
37
  this.participantId = config.participantId;
29
38
  this.sessionId = config.sessionId;
@@ -45,6 +54,11 @@ class Participant {
45
54
  this.actions = this.buildActions();
46
55
  this.transport = config.transport ?? new transports_1.GrpcTransportAdapter(config.client, config.sessionId, config.auth);
47
56
  this.initiatorConfig = config.initiator;
57
+ this.participants = config.participants ?? [];
58
+ this.modeVersion = config.modeVersion;
59
+ this.configurationVersion = config.configurationVersion;
60
+ this.policyVersion = config.policyVersion;
61
+ this.cancelCallbackConfig = config.cancelCallback;
48
62
  }
49
63
  createModeSession(mode, client, opts) {
50
64
  switch (mode) {
@@ -156,71 +170,124 @@ class Participant {
156
170
  this.dispatcher.onTerminal(handler);
157
171
  return this;
158
172
  }
173
+ /** Whether {@link stop} has been called (or the loop has exited). */
174
+ get isStopped() {
175
+ return !this.running;
176
+ }
159
177
  async run() {
160
178
  if (this.running)
161
179
  return;
162
180
  this.running = true;
181
+ if (this.cancelCallbackConfig && !this.cancelCallbackServer) {
182
+ this.cancelCallbackServer = await (0, cancel_callback_1.startCancelCallbackServer)({
183
+ host: this.cancelCallbackConfig.host,
184
+ port: this.cancelCallbackConfig.port,
185
+ path: this.cancelCallbackConfig.path,
186
+ onCancel: () => {
187
+ void this.stop();
188
+ },
189
+ });
190
+ }
163
191
  if (this.initiatorConfig && this.session) {
164
192
  await this.emitInitiatorEnvelopes();
165
193
  }
166
- const sessionInfo = {
167
- sessionId: this.sessionId,
168
- mode: this.mode,
169
- participants: [],
170
- policyVersion: undefined,
171
- };
172
194
  try {
173
195
  for await (const message of this.transport.start()) {
174
196
  if (!this.running)
175
197
  break;
176
- const ctx = {
177
- participant: this,
178
- projection: this.projection,
179
- actions: this.actions,
180
- session: sessionInfo,
181
- log: (msg, details) => {
182
- // eslint-disable-next-line no-console
183
- console.log(`[${this.participantId}] ${msg}`, details ?? '');
184
- },
185
- };
186
- // Apply envelope to projection if we have a mode session
187
- if (this.session && message.raw) {
188
- const applyMethod = this.session
189
- .projection.applyEnvelope;
190
- if (typeof applyMethod === 'function') {
191
- applyMethod.call(this.session.projection, message.raw, this.client.protoRegistry);
192
- }
193
- }
194
- // Dispatch message
195
- await this.dispatcher.dispatch(message, ctx);
196
- // Check for phase change
197
- const currentPhase = this.projection.phase;
198
- if (currentPhase !== this.lastPhase) {
199
- this.lastPhase = currentPhase;
200
- await this.dispatcher.dispatchPhaseChange(currentPhase, ctx);
201
- // Check for terminal state
202
- if (currentPhase === 'Committed' ||
203
- currentPhase === 'Accepted' ||
204
- currentPhase === 'Declined' ||
205
- currentPhase === 'Cancelled' ||
206
- currentPhase === 'TerminalRejected') {
207
- const terminalResult = {
208
- state: currentPhase,
209
- commitment: this.projection.commitment,
210
- };
211
- await this.dispatcher.dispatchTerminal(terminalResult);
212
- break;
213
- }
214
- }
198
+ const reachedTerminal = await this.processMessage(message);
199
+ if (reachedTerminal)
200
+ break;
215
201
  }
216
202
  }
217
203
  finally {
218
204
  this.running = false;
219
205
  }
220
206
  }
207
+ /**
208
+ * Manually process a single envelope. Parity with python-sdk's
209
+ * {@code Participant.process_event}. Useful for deterministic unit tests
210
+ * and for driving the participant from a foreign event loop without
211
+ * installing a {@link TransportAdapter}.
212
+ */
213
+ async processEvent(envelope) {
214
+ const payload = this.client.protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload) ?? {};
215
+ const message = {
216
+ messageType: envelope.messageType,
217
+ sender: envelope.sender,
218
+ payload,
219
+ proposalId: payload.proposalId ?? payload.proposal_id,
220
+ raw: envelope,
221
+ seq: 0,
222
+ };
223
+ await this.processMessage(message);
224
+ }
225
+ buildHandlerContext() {
226
+ const sessionInfo = {
227
+ sessionId: this.sessionId,
228
+ mode: this.mode,
229
+ participants: this.participants,
230
+ modeVersion: this.modeVersion,
231
+ configurationVersion: this.configurationVersion,
232
+ policyVersion: this.policyVersion,
233
+ };
234
+ return {
235
+ participant: this,
236
+ projection: this.projection,
237
+ actions: this.actions,
238
+ session: sessionInfo,
239
+ log: (msg, details) => {
240
+ logging_1.logger.debug(`[${this.participantId}] ${msg}`, details ?? '');
241
+ },
242
+ };
243
+ }
244
+ /** Returns true if a terminal state was reached. */
245
+ async processMessage(message) {
246
+ const ctx = this.buildHandlerContext();
247
+ if (this.session && message.raw) {
248
+ const applyMethod = this.session.projection
249
+ .applyEnvelope;
250
+ if (typeof applyMethod === 'function') {
251
+ applyMethod.call(this.session.projection, message.raw, this.client.protoRegistry);
252
+ }
253
+ }
254
+ await this.dispatcher.dispatch(message, ctx);
255
+ const currentPhase = this.projection.phase;
256
+ if (currentPhase !== this.lastPhase) {
257
+ this.lastPhase = currentPhase;
258
+ await this.dispatcher.dispatchPhaseChange(currentPhase, ctx);
259
+ if (TERMINAL_PHASES.has(currentPhase)) {
260
+ const terminalResult = {
261
+ state: currentPhase,
262
+ commitment: this.projection.commitment,
263
+ };
264
+ await this.dispatcher.dispatchTerminal(terminalResult);
265
+ return true;
266
+ }
267
+ }
268
+ return false;
269
+ }
221
270
  async stop() {
222
271
  this.running = false;
223
272
  await this.transport.stop();
273
+ if (this.cancelCallbackServer) {
274
+ const srv = this.cancelCallbackServer;
275
+ this.cancelCallbackServer = undefined;
276
+ try {
277
+ await srv.close();
278
+ }
279
+ catch (err) {
280
+ logging_1.logger.debug('cancel_callback close failed', err);
281
+ }
282
+ }
283
+ }
284
+ /**
285
+ * Attach a running cancel-callback HTTP server to this participant.
286
+ * The server is closed automatically when {@link stop} is called.
287
+ * Parity with python-sdk's `Participant.attach_cancel_callback_server`.
288
+ */
289
+ attachCancelCallbackServer(server) {
290
+ this.cancelCallbackServer = server;
224
291
  }
225
292
  async emitInitiatorEnvelopes() {
226
293
  if (!this.initiatorConfig || !this.session)
@@ -231,6 +298,8 @@ class Participant {
231
298
  intent: ss.intent,
232
299
  participants: ss.participants,
233
300
  ttlMs: ss.ttlMs,
301
+ contextId: ss.contextId,
302
+ extensions: ss.extensions,
234
303
  roots: ss.roots,
235
304
  });
236
305
  }
@@ -22,6 +22,8 @@ export interface BootstrapPayload {
22
22
  configuration_version?: string;
23
23
  policy_version?: string;
24
24
  context?: Record<string, unknown>;
25
+ context_id?: string;
26
+ extensions?: Record<string, unknown>;
25
27
  roots?: Array<{
26
28
  uri: string;
27
29
  name?: string;
@@ -34,5 +36,16 @@ export interface BootstrapPayload {
34
36
  };
35
37
  };
36
38
  metadata?: Record<string, unknown>;
39
+ /**
40
+ * Bind a cancel-callback HTTP endpoint (RFC-0001 §7.2 Option A). The
41
+ * orchestrator POSTs `{runId, reason}` to `http://host:port{path}` to
42
+ * request a clean shutdown. The runner starts the endpoint before
43
+ * entering the event loop and closes it when the participant stops.
44
+ */
45
+ cancel_callback?: {
46
+ host: string;
47
+ port: number;
48
+ path: string;
49
+ };
37
50
  }
38
51
  export declare function fromBootstrap(bootstrapPath?: string): Participant;