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 +124 -1
- package/README.md +14 -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 +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 +64 -0
- package/dist/base-session.js +112 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +90 -9
- package/dist/constants.d.ts +21 -16
- package/dist/constants.js +24 -17
- package/dist/envelope.d.ts +23 -1
- package/dist/envelope.js +40 -0
- package/dist/errors.d.ts +17 -4
- package/dist/errors.js +19 -10
- package/dist/handoff.d.ts +0 -8
- package/dist/handoff.js +0 -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 +1 -1
- 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/task.d.ts +4 -4
- package/dist/task.js +4 -4
- package/dist/watchers.d.ts +13 -1
- package/dist/watchers.js +29 -4
- package/package.json +2 -2
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
|
-
## [
|
|
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.
|
|
144
|
+
await session.requestTask({ taskId: 't1', title: 'Build feature', instructions: '...' });
|
|
145
145
|
await session.acceptTask({ taskId: 't1', assignee: 'worker' });
|
|
146
|
-
await session.
|
|
147
|
-
await session.
|
|
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
|
+
}
|
package/dist/agent/index.d.ts
CHANGED
package/dist/agent/index.js
CHANGED
|
@@ -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
|
|
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;
|