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.
- package/CHANGELOG.md +176 -1
- package/README.md +49 -6
- package/dist/agent/cancel-callback.d.ts +23 -0
- package/dist/agent/cancel-callback.js +108 -0
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/participant.d.ts +39 -0
- package/dist/agent/participant.js +114 -45
- package/dist/agent/runner.d.ts +13 -0
- package/dist/agent/runner.js +34 -0
- package/dist/agent/strategies.d.ts +2 -0
- package/dist/agent/strategies.js +8 -0
- package/dist/agent/transports.js +5 -0
- package/dist/base-session.d.ts +75 -0
- package/dist/base-session.js +127 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.js +130 -9
- package/dist/constants.d.ts +21 -16
- package/dist/constants.js +24 -17
- package/dist/decision.d.ts +10 -0
- package/dist/decision.js +20 -0
- package/dist/envelope.d.ts +38 -1
- package/dist/envelope.js +53 -1
- package/dist/errors.d.ts +17 -4
- package/dist/errors.js +19 -10
- package/dist/handoff.d.ts +10 -8
- package/dist/handoff.js +20 -16
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -2
- package/dist/logging.d.ts +21 -0
- package/dist/logging.js +64 -0
- package/dist/policy.d.ts +47 -42
- package/dist/policy.js +5 -6
- package/dist/projections/base.d.ts +20 -0
- package/dist/projections/base.js +37 -0
- package/dist/projections.d.ts +1 -0
- package/dist/projections.js +1 -0
- package/dist/proposal.d.ts +10 -0
- package/dist/proposal.js +20 -0
- package/dist/quorum.d.ts +10 -0
- package/dist/quorum.js +20 -0
- package/dist/task.d.ts +14 -4
- package/dist/task.js +24 -4
- package/dist/types.d.ts +30 -3
- package/dist/watchers.d.ts +37 -2
- package/dist/watchers.js +65 -5
- 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
|
|
177
|
-
|
|
178
|
-
|
|
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
|
}
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/runner.js
CHANGED
|
@@ -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;
|
package/dist/agent/strategies.js
CHANGED
|
@@ -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';
|
package/dist/agent/transports.js
CHANGED
|
@@ -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
|
}>;
|