macp-sdk-typescript 0.2.2 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/README.md +49 -6
  3. package/dist/agent/cancel-callback.d.ts +23 -0
  4. package/dist/agent/cancel-callback.js +108 -0
  5. package/dist/agent/index.d.ts +1 -0
  6. package/dist/agent/index.js +1 -0
  7. package/dist/agent/participant.d.ts +39 -0
  8. package/dist/agent/participant.js +114 -45
  9. package/dist/agent/runner.d.ts +13 -0
  10. package/dist/agent/runner.js +34 -0
  11. package/dist/agent/strategies.d.ts +2 -0
  12. package/dist/agent/strategies.js +8 -0
  13. package/dist/agent/transports.js +5 -0
  14. package/dist/base-session.d.ts +75 -0
  15. package/dist/base-session.js +127 -0
  16. package/dist/client.d.ts +54 -0
  17. package/dist/client.js +130 -9
  18. package/dist/constants.d.ts +21 -16
  19. package/dist/constants.js +24 -17
  20. package/dist/decision.d.ts +10 -0
  21. package/dist/decision.js +20 -0
  22. package/dist/envelope.d.ts +38 -1
  23. package/dist/envelope.js +53 -1
  24. package/dist/errors.d.ts +17 -4
  25. package/dist/errors.js +19 -10
  26. package/dist/handoff.d.ts +10 -8
  27. package/dist/handoff.js +20 -16
  28. package/dist/index.d.ts +3 -1
  29. package/dist/index.js +10 -2
  30. package/dist/logging.d.ts +21 -0
  31. package/dist/logging.js +64 -0
  32. package/dist/policy.d.ts +47 -42
  33. package/dist/policy.js +5 -6
  34. package/dist/projections/base.d.ts +20 -0
  35. package/dist/projections/base.js +37 -0
  36. package/dist/projections.d.ts +1 -0
  37. package/dist/projections.js +1 -0
  38. package/dist/proposal.d.ts +10 -0
  39. package/dist/proposal.js +20 -0
  40. package/dist/quorum.d.ts +10 -0
  41. package/dist/quorum.js +20 -0
  42. package/dist/task.d.ts +14 -4
  43. package/dist/task.js +24 -4
  44. package/dist/types.d.ts +30 -3
  45. package/dist/watchers.d.ts +37 -2
  46. package/dist/watchers.js +65 -5
  47. package/package.json +2 -2
@@ -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;
@@ -77,6 +77,8 @@ function fromBootstrap(bootstrapPath) {
77
77
  intent: ss.intent,
78
78
  participants: ss.participants,
79
79
  ttlMs: ss.ttl_ms,
80
+ contextId: ss.context_id,
81
+ extensions: encodeExtensions(ss.extensions),
80
82
  roots: ss.roots,
81
83
  },
82
84
  kickoff: payload.initiator.kickoff
@@ -98,6 +100,38 @@ function fromBootstrap(bootstrapPath) {
98
100
  configurationVersion: payload.configuration_version,
99
101
  policyVersion: payload.policy_version ?? constants_1.DEFAULT_POLICY_VERSION,
100
102
  initiator,
103
+ cancelCallback: payload.cancel_callback
104
+ ? {
105
+ host: payload.cancel_callback.host,
106
+ port: payload.cancel_callback.port,
107
+ path: payload.cancel_callback.path,
108
+ }
109
+ : undefined,
101
110
  };
102
111
  return new participant_1.Participant(config);
103
112
  }
113
+ /**
114
+ * Serialise a JSON-native extensions map (as delivered in the bootstrap
115
+ * payload) into the `Record<string, Buffer>` form the SessionStart envelope
116
+ * expects. Each value is encoded as UTF-8 JSON so arbitrary structures
117
+ * round-trip through the protobuf `bytes` field. `Buffer` / `Uint8Array`
118
+ * values are passed through untouched so callers that already hold raw bytes
119
+ * don't pay a double-encode.
120
+ */
121
+ function encodeExtensions(extensions) {
122
+ if (!extensions)
123
+ return undefined;
124
+ const out = {};
125
+ for (const [key, value] of Object.entries(extensions)) {
126
+ if (Buffer.isBuffer(value)) {
127
+ out[key] = value;
128
+ }
129
+ else if (value instanceof Uint8Array) {
130
+ out[key] = Buffer.from(value);
131
+ }
132
+ else {
133
+ out[key] = Buffer.from(JSON.stringify(value), 'utf8');
134
+ }
135
+ }
136
+ return out;
137
+ }
@@ -19,6 +19,7 @@ export interface VotingStrategy {
19
19
  decideVote(projection: DecisionProjection): Promise<VoteResult>;
20
20
  }
21
21
  export declare function votingHandler(strategy: VotingStrategy): MessageHandler;
22
+ export declare function functionVoter(shouldVote: (projection: DecisionProjection) => boolean, decideVote: (projection: DecisionProjection) => Promise<VoteResult>): VotingStrategy;
22
23
  export declare function majorityVoter(options?: {
23
24
  positiveThreshold?: number;
24
25
  }): VotingStrategy;
@@ -33,6 +34,7 @@ export interface CommitmentStrategy {
33
34
  decideCommitment(projection: DecisionProjection): Promise<CommitmentResult>;
34
35
  }
35
36
  export declare function commitmentHandler(strategy: CommitmentStrategy): MessageHandler;
37
+ export declare function functionCommitter(shouldCommit: (projection: DecisionProjection) => boolean, decideCommitment: (projection: DecisionProjection) => Promise<CommitmentResult>): CommitmentStrategy;
36
38
  export declare function majorityCommitter(options?: {
37
39
  quorumSize?: number;
38
40
  action?: string;
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.evaluationHandler = evaluationHandler;
4
4
  exports.functionEvaluator = functionEvaluator;
5
5
  exports.votingHandler = votingHandler;
6
+ exports.functionVoter = functionVoter;
6
7
  exports.majorityVoter = majorityVoter;
7
8
  exports.commitmentHandler = commitmentHandler;
9
+ exports.functionCommitter = functionCommitter;
8
10
  exports.majorityCommitter = majorityCommitter;
9
11
  const envelope_1 = require("../envelope");
10
12
  function evaluationHandler(strategy) {
@@ -43,6 +45,9 @@ function votingHandler(strategy) {
43
45
  }
44
46
  };
45
47
  }
48
+ function functionVoter(shouldVote, decideVote) {
49
+ return { shouldVote, decideVote };
50
+ }
46
51
  function majorityVoter(options) {
47
52
  const threshold = options?.positiveThreshold ?? 0.5;
48
53
  return {
@@ -80,6 +85,9 @@ function commitmentHandler(strategy) {
80
85
  }
81
86
  };
82
87
  }
88
+ function functionCommitter(shouldCommit, decideCommitment) {
89
+ return { shouldCommit, decideCommitment };
90
+ }
83
91
  function majorityCommitter(options) {
84
92
  const quorum = options?.quorumSize ?? 1;
85
93
  const action = options?.action ?? 'commit';
@@ -25,6 +25,11 @@ class GrpcTransportAdapter {
25
25
  }
26
26
  async *start() {
27
27
  this.stream = this.client.openStream({ auth: this.auth });
28
+ // RFC-MACP-0006-A1: Subscribe to the session with history replay.
29
+ // The runtime replays accepted envelopes then switches to live broadcast.
30
+ // This ensures non-initiator agents receive the SessionStart + Proposal
31
+ // regardless of spawn order or connection timing.
32
+ await this.stream.sendSubscribe(this.sessionId);
28
33
  for await (const envelope of this.stream.responses()) {
29
34
  if (envelope.sessionId !== this.sessionId)
30
35
  continue;
@@ -0,0 +1,75 @@
1
+ import { type AuthConfig } from './auth';
2
+ import type { MacpClient, MacpStream } from './client';
3
+ import type { BaseProjection } from './projections/base';
4
+ import type { Ack, Envelope, Root, SessionMetadata } from './types';
5
+ export interface BaseSessionOptions {
6
+ sessionId?: string;
7
+ modeVersion?: string;
8
+ configurationVersion?: string;
9
+ policyVersion?: string;
10
+ auth?: AuthConfig;
11
+ }
12
+ /**
13
+ * Abstract base class for mode session helpers — parity with python-sdk's
14
+ * `macp_sdk.base_session.BaseSession`. Provides the shared lifecycle
15
+ * (start, commit, cancel, metadata, openStream) and the two identity-guarded
16
+ * helpers (`senderFor`, `sendAndTrack`) that every mode session needs.
17
+ *
18
+ * Concrete session classes (DecisionSession, etc.) pre-date this base class
19
+ * and implement their own equivalents; BaseSession is the recommended
20
+ * extension point for custom modes registered via `registerExtMode`.
21
+ */
22
+ export declare abstract class BaseSession<P extends BaseProjection> {
23
+ readonly client: MacpClient;
24
+ readonly sessionId: string;
25
+ readonly modeVersion: string;
26
+ readonly configurationVersion: string;
27
+ readonly policyVersion: string;
28
+ readonly auth?: AuthConfig;
29
+ readonly projection: P;
30
+ protected abstract readonly mode: string;
31
+ constructor(client: MacpClient, options?: BaseSessionOptions);
32
+ /** Return a new projection instance. Subclasses construct their mode-specific projection here. */
33
+ protected abstract createProjection(): P;
34
+ /** Resolve the envelope sender, raising {@link MacpIdentityMismatchError} on conflict. */
35
+ protected senderFor(sender: string | undefined, auth?: AuthConfig): string;
36
+ /** Send an envelope and, on ACK, feed it to the projection. */
37
+ protected sendAndTrack(envelope: Envelope, auth?: AuthConfig): Promise<Ack>;
38
+ /** Send SessionStart and begin tracking via the projection. */
39
+ start(input: {
40
+ intent: string;
41
+ participants: string[];
42
+ ttlMs: number;
43
+ contextId?: string;
44
+ extensions?: Record<string, Buffer>;
45
+ roots?: Root[];
46
+ sender?: string;
47
+ auth?: AuthConfig;
48
+ }): Promise<Ack>;
49
+ /** Send a Commitment envelope to resolve the session. */
50
+ commit(input: {
51
+ action: string;
52
+ authorityScope: string;
53
+ reason: string;
54
+ commitmentId?: string;
55
+ outcomePositive?: boolean;
56
+ sender?: string;
57
+ auth?: AuthConfig;
58
+ }): Promise<Ack>;
59
+ metadata(auth?: AuthConfig): Promise<{
60
+ metadata: SessionMetadata;
61
+ }>;
62
+ cancel(reason?: string, auth?: AuthConfig): Promise<Ack>;
63
+ /**
64
+ * Pause this session via `SuspendSession` (proto 0.1.3+). Non-terminal: TTL
65
+ * is banked and messages are rejected until {@link resume}. See
66
+ * {@link MacpClient.suspendSession}.
67
+ */
68
+ suspend(reason?: string, auth?: AuthConfig): Promise<Ack>;
69
+ /**
70
+ * Resume this suspended session via `ResumeSession` (proto 0.1.3+),
71
+ * restoring OPEN and the banked TTL. See {@link MacpClient.resumeSession}.
72
+ */
73
+ resume(reason?: string, auth?: AuthConfig): Promise<Ack>;
74
+ openStream(auth?: AuthConfig): MacpStream;
75
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseSession = void 0;
4
+ const auth_1 = require("./auth");
5
+ const constants_1 = require("./constants");
6
+ const envelope_1 = require("./envelope");
7
+ const logging_1 = require("./logging");
8
+ const validation_1 = require("./validation");
9
+ /**
10
+ * Abstract base class for mode session helpers — parity with python-sdk's
11
+ * `macp_sdk.base_session.BaseSession`. Provides the shared lifecycle
12
+ * (start, commit, cancel, metadata, openStream) and the two identity-guarded
13
+ * helpers (`senderFor`, `sendAndTrack`) that every mode session needs.
14
+ *
15
+ * Concrete session classes (DecisionSession, etc.) pre-date this base class
16
+ * and implement their own equivalents; BaseSession is the recommended
17
+ * extension point for custom modes registered via `registerExtMode`.
18
+ */
19
+ class BaseSession {
20
+ client;
21
+ sessionId;
22
+ modeVersion;
23
+ configurationVersion;
24
+ policyVersion;
25
+ auth;
26
+ projection;
27
+ constructor(client, options = {}) {
28
+ this.client = client;
29
+ if (options.sessionId)
30
+ (0, validation_1.validateSessionId)(options.sessionId);
31
+ this.sessionId = options.sessionId ?? (0, envelope_1.newSessionId)();
32
+ this.modeVersion = options.modeVersion ?? constants_1.DEFAULT_MODE_VERSION;
33
+ this.configurationVersion = options.configurationVersion ?? constants_1.DEFAULT_CONFIGURATION_VERSION;
34
+ this.policyVersion = options.policyVersion ?? constants_1.DEFAULT_POLICY_VERSION;
35
+ this.auth = options.auth;
36
+ this.projection = this.createProjection();
37
+ }
38
+ /** Resolve the envelope sender, raising {@link MacpIdentityMismatchError} on conflict. */
39
+ senderFor(sender, auth) {
40
+ const effectiveAuth = auth ?? this.auth ?? this.client.auth;
41
+ (0, auth_1.assertSenderMatchesIdentity)(effectiveAuth, sender);
42
+ return sender ?? (0, auth_1.authSender)(effectiveAuth) ?? '';
43
+ }
44
+ /** Send an envelope and, on ACK, feed it to the projection. */
45
+ async sendAndTrack(envelope, auth) {
46
+ logging_1.logger.debug('send', {
47
+ session: envelope.sessionId,
48
+ messageType: envelope.messageType,
49
+ sender: envelope.sender,
50
+ });
51
+ const ack = await this.client.send(envelope, { auth: auth ?? this.auth });
52
+ if (ack.ok)
53
+ this.projection.applyEnvelope(envelope, this.client.protoRegistry);
54
+ else
55
+ logging_1.logger.warn('nack', { session: envelope.sessionId, code: ack.error?.code });
56
+ return ack;
57
+ }
58
+ /** Send SessionStart and begin tracking via the projection. */
59
+ async start(input) {
60
+ (0, validation_1.validateParticipantCount)(input.participants.length);
61
+ const payload = (0, envelope_1.buildSessionStartPayload)({
62
+ intent: input.intent,
63
+ participants: input.participants,
64
+ ttlMs: input.ttlMs,
65
+ contextId: input.contextId,
66
+ extensions: input.extensions,
67
+ roots: input.roots,
68
+ modeVersion: this.modeVersion,
69
+ configurationVersion: this.configurationVersion,
70
+ policyVersion: this.policyVersion,
71
+ });
72
+ const envelope = (0, envelope_1.buildEnvelope)({
73
+ mode: this.mode,
74
+ messageType: 'SessionStart',
75
+ sessionId: this.sessionId,
76
+ sender: this.senderFor(input.sender, input.auth),
77
+ payload: this.client.protoRegistry.encodeKnownPayload(this.mode, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
78
+ });
79
+ return this.sendAndTrack(envelope, input.auth ?? this.auth);
80
+ }
81
+ /** Send a Commitment envelope to resolve the session. */
82
+ async commit(input) {
83
+ const payload = (0, envelope_1.buildCommitmentPayload)({
84
+ action: input.action,
85
+ authorityScope: input.authorityScope,
86
+ reason: input.reason,
87
+ commitmentId: input.commitmentId,
88
+ outcomePositive: input.outcomePositive,
89
+ modeVersion: this.modeVersion,
90
+ configurationVersion: this.configurationVersion,
91
+ policyVersion: this.policyVersion,
92
+ });
93
+ const envelope = (0, envelope_1.buildEnvelope)({
94
+ mode: this.mode,
95
+ messageType: 'Commitment',
96
+ sessionId: this.sessionId,
97
+ sender: this.senderFor(input.sender, input.auth),
98
+ payload: this.client.protoRegistry.encodeKnownPayload(this.mode, 'Commitment', (0, envelope_1.toProtoPayload)(payload)),
99
+ });
100
+ return this.sendAndTrack(envelope, input.auth);
101
+ }
102
+ metadata(auth) {
103
+ return this.client.getSession(this.sessionId, { auth: auth ?? this.auth });
104
+ }
105
+ cancel(reason = '', auth) {
106
+ return this.client.cancelSession(this.sessionId, reason, { auth: auth ?? this.auth });
107
+ }
108
+ /**
109
+ * Pause this session via `SuspendSession` (proto 0.1.3+). Non-terminal: TTL
110
+ * is banked and messages are rejected until {@link resume}. See
111
+ * {@link MacpClient.suspendSession}.
112
+ */
113
+ suspend(reason = '', auth) {
114
+ return this.client.suspendSession(this.sessionId, reason, { auth: auth ?? this.auth });
115
+ }
116
+ /**
117
+ * Resume this suspended session via `ResumeSession` (proto 0.1.3+),
118
+ * restoring OPEN and the banked TTL. See {@link MacpClient.resumeSession}.
119
+ */
120
+ resume(reason = '', auth) {
121
+ return this.client.resumeSession(this.sessionId, reason, { auth: auth ?? this.auth });
122
+ }
123
+ openStream(auth) {
124
+ return this.client.openStream({ auth: auth ?? this.auth });
125
+ }
126
+ }
127
+ exports.BaseSession = BaseSession;
package/dist/client.d.ts CHANGED
@@ -35,7 +35,27 @@ export declare class MacpStream {
35
35
  constructor(call: grpc.ClientDuplexStream<any, any>);
36
36
  onInlineError(callback: InlineErrorCallback): void;
37
37
  send(envelope: Envelope): Promise<void>;
38
+ /**
39
+ * RFC-MACP-0006-A1: Send a subscribe-only frame to receive session history
40
+ * + live broadcast. The runtime replays accepted envelopes from
41
+ * `afterSequence` onwards, then continues with live broadcast.
42
+ */
43
+ sendSubscribe(sessionId: string, afterSequence?: number): Promise<void>;
38
44
  responses(): AsyncGenerator<Envelope, void, void>;
45
+ /**
46
+ * Read a single envelope from the stream. Parity with python-sdk's
47
+ * {@code MacpStream.read(timeout)}.
48
+ *
49
+ * @param timeoutMs Maximum time to wait for the next envelope. If omitted,
50
+ * blocks indefinitely until an envelope arrives, the stream
51
+ * ends, or an error occurs.
52
+ * @returns The next envelope, or {@code null} if the stream has
53
+ * ended.
54
+ * @throws MacpTimeoutError If {@code timeoutMs} elapses before an envelope
55
+ * arrives.
56
+ * @throws MacpTransportError If the underlying stream errored.
57
+ */
58
+ read(timeoutMs?: number): Promise<Envelope | null>;
39
59
  close(): void;
40
60
  }
41
61
  export declare class MacpClient {
@@ -63,12 +83,46 @@ export declare class MacpClient {
63
83
  }): Promise<{
64
84
  metadata: SessionMetadata;
65
85
  }>;
86
+ /**
87
+ * Terminate a session via the `CancelSession` control-plane RPC.
88
+ *
89
+ * As of proto 0.1.3 the resulting `Ack.sessionState` is
90
+ * `SESSION_STATE_CANCELLED` (previously `SESSION_STATE_EXPIRED`) — distinct
91
+ * from TTL/policy expiry so consumers can tell explicit cancellation apart.
92
+ */
66
93
  cancelSession(sessionId: string, reason: string, options?: {
67
94
  auth?: AuthConfig;
68
95
  deadlineMs?: number;
69
96
  raiseOnNack?: boolean;
70
97
  cancelledBy?: string;
71
98
  }): Promise<Ack>;
99
+ /**
100
+ * Pause a session via the `SuspendSession` control-plane RPC (proto 0.1.3+).
101
+ *
102
+ * Suspension is non-terminal: the resulting `Ack.sessionState` is
103
+ * `SESSION_STATE_SUSPENDED`, the session's remaining TTL is banked, and
104
+ * messages sent to a suspended session are rejected until {@link resumeSession}
105
+ * restores it to `SESSION_STATE_OPEN`. Restricted to the initiator and
106
+ * policy-delegated roles, mirroring {@link cancelSession}.
107
+ */
108
+ suspendSession(sessionId: string, reason: string, options?: {
109
+ auth?: AuthConfig;
110
+ deadlineMs?: number;
111
+ raiseOnNack?: boolean;
112
+ }): Promise<Ack>;
113
+ /**
114
+ * Resume a suspended session via the `ResumeSession` control-plane RPC
115
+ * (proto 0.1.3+).
116
+ *
117
+ * Restores `SESSION_STATE_OPEN` and adds the banked TTL back to the session's
118
+ * absolute deadline. Restricted to the initiator and policy-delegated roles,
119
+ * mirroring {@link suspendSession}.
120
+ */
121
+ resumeSession(sessionId: string, reason: string, options?: {
122
+ auth?: AuthConfig;
123
+ deadlineMs?: number;
124
+ raiseOnNack?: boolean;
125
+ }): Promise<Ack>;
72
126
  getManifest(agentId?: string, deadlineMs?: number): Promise<{
73
127
  manifest: AgentManifest;
74
128
  }>;