macp-sdk-typescript 0.2.0 → 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 +124 -1
- package/README.md +99 -3
- 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 +58 -0
- package/dist/agent/participant.js +143 -45
- package/dist/agent/runner.d.ts +34 -1
- package/dist/agent/runner.js +53 -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 +64 -0
- package/dist/base-session.js +112 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +100 -10
- package/dist/constants.d.ts +21 -16
- package/dist/constants.js +24 -17
- package/dist/decision.d.ts +2 -1
- package/dist/decision.js +2 -1
- package/dist/envelope.d.ts +25 -3
- package/dist/envelope.js +42 -11
- package/dist/errors.d.ts +17 -4
- package/dist/errors.js +19 -10
- package/dist/handoff.d.ts +2 -9
- package/dist/handoff.js +2 -17
- 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 +6 -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 +2 -1
- package/dist/proposal.js +2 -1
- package/dist/quorum.d.ts +2 -1
- package/dist/quorum.js +2 -1
- package/dist/task.d.ts +6 -5
- package/dist/task.js +6 -5
- package/dist/types.d.ts +11 -2
- package/dist/watchers.d.ts +23 -1
- package/dist/watchers.js +59 -1
- 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;
|
|
@@ -21,8 +24,15 @@ class Participant {
|
|
|
21
24
|
dispatcher;
|
|
22
25
|
session;
|
|
23
26
|
transport;
|
|
27
|
+
initiatorConfig;
|
|
28
|
+
participants;
|
|
29
|
+
modeVersion;
|
|
30
|
+
configurationVersion;
|
|
31
|
+
policyVersion;
|
|
24
32
|
running = false;
|
|
25
33
|
lastPhase;
|
|
34
|
+
cancelCallbackServer;
|
|
35
|
+
cancelCallbackConfig;
|
|
26
36
|
constructor(config) {
|
|
27
37
|
this.participantId = config.participantId;
|
|
28
38
|
this.sessionId = config.sessionId;
|
|
@@ -43,6 +53,12 @@ class Participant {
|
|
|
43
53
|
this.lastPhase = projection.phase;
|
|
44
54
|
this.actions = this.buildActions();
|
|
45
55
|
this.transport = config.transport ?? new transports_1.GrpcTransportAdapter(config.client, config.sessionId, config.auth);
|
|
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;
|
|
46
62
|
}
|
|
47
63
|
createModeSession(mode, client, opts) {
|
|
48
64
|
switch (mode) {
|
|
@@ -154,68 +170,150 @@ class Participant {
|
|
|
154
170
|
this.dispatcher.onTerminal(handler);
|
|
155
171
|
return this;
|
|
156
172
|
}
|
|
173
|
+
/** Whether {@link stop} has been called (or the loop has exited). */
|
|
174
|
+
get isStopped() {
|
|
175
|
+
return !this.running;
|
|
176
|
+
}
|
|
157
177
|
async run() {
|
|
158
178
|
if (this.running)
|
|
159
179
|
return;
|
|
160
180
|
this.running = true;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
191
|
+
if (this.initiatorConfig && this.session) {
|
|
192
|
+
await this.emitInitiatorEnvelopes();
|
|
193
|
+
}
|
|
167
194
|
try {
|
|
168
195
|
for await (const message of this.transport.start()) {
|
|
169
196
|
if (!this.running)
|
|
170
197
|
break;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
actions: this.actions,
|
|
175
|
-
session: sessionInfo,
|
|
176
|
-
log: (msg, details) => {
|
|
177
|
-
// eslint-disable-next-line no-console
|
|
178
|
-
console.log(`[${this.participantId}] ${msg}`, details ?? '');
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
// Apply envelope to projection if we have a mode session
|
|
182
|
-
if (this.session && message.raw) {
|
|
183
|
-
const applyMethod = this.session
|
|
184
|
-
.projection.applyEnvelope;
|
|
185
|
-
if (typeof applyMethod === 'function') {
|
|
186
|
-
applyMethod.call(this.session.projection, message.raw, this.client.protoRegistry);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// Dispatch message
|
|
190
|
-
await this.dispatcher.dispatch(message, ctx);
|
|
191
|
-
// Check for phase change
|
|
192
|
-
const currentPhase = this.projection.phase;
|
|
193
|
-
if (currentPhase !== this.lastPhase) {
|
|
194
|
-
this.lastPhase = currentPhase;
|
|
195
|
-
await this.dispatcher.dispatchPhaseChange(currentPhase, ctx);
|
|
196
|
-
// Check for terminal state
|
|
197
|
-
if (currentPhase === 'Committed' ||
|
|
198
|
-
currentPhase === 'Accepted' ||
|
|
199
|
-
currentPhase === 'Declined' ||
|
|
200
|
-
currentPhase === 'Cancelled' ||
|
|
201
|
-
currentPhase === 'TerminalRejected') {
|
|
202
|
-
const terminalResult = {
|
|
203
|
-
state: currentPhase,
|
|
204
|
-
commitment: this.projection.commitment,
|
|
205
|
-
};
|
|
206
|
-
await this.dispatcher.dispatchTerminal(terminalResult);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
198
|
+
const reachedTerminal = await this.processMessage(message);
|
|
199
|
+
if (reachedTerminal)
|
|
200
|
+
break;
|
|
210
201
|
}
|
|
211
202
|
}
|
|
212
203
|
finally {
|
|
213
204
|
this.running = false;
|
|
214
205
|
}
|
|
215
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
|
+
}
|
|
216
270
|
async stop() {
|
|
217
271
|
this.running = false;
|
|
218
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;
|
|
291
|
+
}
|
|
292
|
+
async emitInitiatorEnvelopes() {
|
|
293
|
+
if (!this.initiatorConfig || !this.session)
|
|
294
|
+
return;
|
|
295
|
+
const ss = this.initiatorConfig.sessionStart;
|
|
296
|
+
if ('start' in this.session && typeof this.session.start === 'function') {
|
|
297
|
+
await this.session.start({
|
|
298
|
+
intent: ss.intent,
|
|
299
|
+
participants: ss.participants,
|
|
300
|
+
ttlMs: ss.ttlMs,
|
|
301
|
+
contextId: ss.contextId,
|
|
302
|
+
extensions: ss.extensions,
|
|
303
|
+
roots: ss.roots,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const kickoff = this.initiatorConfig.kickoff;
|
|
307
|
+
if (!kickoff)
|
|
308
|
+
return;
|
|
309
|
+
if (kickoff.messageType === 'Proposal' && this.session instanceof decision_1.DecisionSession) {
|
|
310
|
+
const payload = kickoff.payload;
|
|
311
|
+
await this.session.propose({
|
|
312
|
+
proposalId: String(payload.proposalId ?? payload.proposal_id ?? `${this.sessionId}-kickoff`),
|
|
313
|
+
option: String(payload.option ?? 'decide'),
|
|
314
|
+
rationale: payload.rationale !== undefined ? String(payload.rationale) : undefined,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
219
317
|
}
|
|
220
318
|
}
|
|
221
319
|
exports.Participant = Participant;
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -11,8 +11,41 @@ export interface BootstrapPayload {
|
|
|
11
11
|
auth_token?: string;
|
|
12
12
|
agent_id?: string;
|
|
13
13
|
secure?: boolean;
|
|
14
|
-
/** Mirror of {@link MacpClientOptions.allowInsecure}; required when `secure` is false. */
|
|
15
14
|
allow_insecure?: boolean;
|
|
16
15
|
participants?: string[];
|
|
16
|
+
initiator?: {
|
|
17
|
+
session_start: {
|
|
18
|
+
intent: string;
|
|
19
|
+
participants: string[];
|
|
20
|
+
ttl_ms: number;
|
|
21
|
+
mode_version?: string;
|
|
22
|
+
configuration_version?: string;
|
|
23
|
+
policy_version?: string;
|
|
24
|
+
context?: Record<string, unknown>;
|
|
25
|
+
context_id?: string;
|
|
26
|
+
extensions?: Record<string, unknown>;
|
|
27
|
+
roots?: Array<{
|
|
28
|
+
uri: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
kickoff?: {
|
|
33
|
+
message_type: string;
|
|
34
|
+
payload_type?: string;
|
|
35
|
+
payload: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
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
|
+
};
|
|
17
50
|
}
|
|
18
51
|
export declare function fromBootstrap(bootstrapPath?: string): Participant;
|
package/dist/agent/runner.js
CHANGED
|
@@ -69,6 +69,26 @@ function fromBootstrap(bootstrapPath) {
|
|
|
69
69
|
allowInsecure: payload.allow_insecure,
|
|
70
70
|
auth,
|
|
71
71
|
});
|
|
72
|
+
let initiator;
|
|
73
|
+
if (payload.initiator) {
|
|
74
|
+
const ss = payload.initiator.session_start;
|
|
75
|
+
initiator = {
|
|
76
|
+
sessionStart: {
|
|
77
|
+
intent: ss.intent,
|
|
78
|
+
participants: ss.participants,
|
|
79
|
+
ttlMs: ss.ttl_ms,
|
|
80
|
+
contextId: ss.context_id,
|
|
81
|
+
extensions: encodeExtensions(ss.extensions),
|
|
82
|
+
roots: ss.roots,
|
|
83
|
+
},
|
|
84
|
+
kickoff: payload.initiator.kickoff
|
|
85
|
+
? {
|
|
86
|
+
messageType: payload.initiator.kickoff.message_type,
|
|
87
|
+
payload: payload.initiator.kickoff.payload,
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
72
92
|
const config = {
|
|
73
93
|
participantId: payload.participant_id,
|
|
74
94
|
sessionId: payload.session_id,
|
|
@@ -79,6 +99,39 @@ function fromBootstrap(bootstrapPath) {
|
|
|
79
99
|
modeVersion: payload.mode_version,
|
|
80
100
|
configurationVersion: payload.configuration_version,
|
|
81
101
|
policyVersion: payload.policy_version ?? constants_1.DEFAULT_POLICY_VERSION,
|
|
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,
|
|
82
110
|
};
|
|
83
111
|
return new participant_1.Participant(config);
|
|
84
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,64 @@
|
|
|
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
|
+
openStream(auth?: AuthConfig): MacpStream;
|
|
64
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
openStream(auth) {
|
|
109
|
+
return this.client.openStream({ auth: auth ?? this.auth });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
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 {
|
|
@@ -125,6 +145,11 @@ export declare class MacpClient {
|
|
|
125
145
|
auth?: AuthConfig;
|
|
126
146
|
deadlineMs?: number;
|
|
127
147
|
}): Promise<PolicyDescriptor[]>;
|
|
148
|
+
listSessions(options?: {
|
|
149
|
+
auth?: AuthConfig;
|
|
150
|
+
deadlineMs?: number;
|
|
151
|
+
}): Promise<SessionMetadata[]>;
|
|
152
|
+
watchSessions(auth?: AuthConfig): grpc.ClientReadableStream<any>;
|
|
128
153
|
watchPolicies(auth?: AuthConfig): grpc.ClientReadableStream<any>;
|
|
129
154
|
/** @deprecated Use {@link watchPolicies}. Scheduled for removal in 0.4.0. */
|
|
130
155
|
_watchPolicies(auth?: AuthConfig): grpc.ClientReadableStream<any>;
|