macp-sdk-typescript 0.4.1 → 0.6.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 +118 -0
- package/README.md +17 -6
- package/dist/agent/participant.d.ts +2 -0
- package/dist/agent/participant.js +1 -0
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.js +1 -0
- package/dist/agent/transports.d.ts +10 -0
- package/dist/agent/transports.js +13 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +15 -15
- package/dist/base-session.d.ts +1 -0
- package/dist/base-session.js +3 -0
- package/dist/client.d.ts +60 -14
- package/dist/client.js +88 -30
- package/dist/decision.d.ts +1 -0
- package/dist/decision.js +2 -0
- package/dist/envelope.d.ts +20 -0
- package/dist/envelope.js +15 -0
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +11 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.js +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/policy.d.ts +20 -0
- package/dist/policy.js +9 -0
- package/dist/projections/handoff.d.ts +12 -0
- package/dist/projections/handoff.js +13 -0
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -0
- package/dist/proto-registry.d.ts +10 -0
- package/dist/proto-registry.js +34 -1
- package/dist/quorum.d.ts +1 -0
- package/dist/quorum.js +2 -0
- package/dist/retry.d.ts +7 -0
- package/dist/task.d.ts +1 -0
- package/dist/task.js +2 -0
- package/dist/types.d.ts +18 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +10 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +10 -0
- package/dist/watchers.d.ts +17 -13
- package/dist/watchers.js +26 -26
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MacpClient = exports.MacpStream = void 0;
|
|
37
|
+
exports.grpcStatusName = grpcStatusName;
|
|
37
38
|
/* eslint-disable @typescript-eslint/no-explicit-any --
|
|
38
39
|
* The gRPC service client returned by `protoLoader.loadSync()` is typed as
|
|
39
40
|
* `any` by `@grpc/proto-loader`; method signatures (Send, StreamSession,
|
|
@@ -98,6 +99,17 @@ class AsyncQueue {
|
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Map a gRPC `ServiceError.code` (numeric `grpc.status`) to its status name
|
|
104
|
+
* (e.g. `8` → `'RESOURCE_EXHAUSTED'`). Returns `undefined` for non-numeric or
|
|
105
|
+
* unknown codes. Used to populate {@link MacpTransportError.code}.
|
|
106
|
+
*/
|
|
107
|
+
function grpcStatusName(code) {
|
|
108
|
+
if (typeof code !== 'number')
|
|
109
|
+
return undefined;
|
|
110
|
+
const name = grpc.status[code];
|
|
111
|
+
return typeof name === 'string' ? name : undefined;
|
|
112
|
+
}
|
|
101
113
|
const TIMEOUT = Symbol('stream-read-timeout');
|
|
102
114
|
const STREAM_END = Symbol('stream-end');
|
|
103
115
|
class MacpStream {
|
|
@@ -120,7 +132,7 @@ class MacpStream {
|
|
|
120
132
|
}
|
|
121
133
|
});
|
|
122
134
|
call.on('error', (error) => {
|
|
123
|
-
this.queue.push(new errors_1.MacpTransportError(error.details || error.message));
|
|
135
|
+
this.queue.push(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
|
|
124
136
|
});
|
|
125
137
|
call.on('end', () => {
|
|
126
138
|
this.queue.push(STREAM_END);
|
|
@@ -142,9 +154,19 @@ class MacpStream {
|
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
/**
|
|
145
|
-
* RFC-MACP-0006
|
|
146
|
-
* + live broadcast. The runtime replays accepted envelopes
|
|
147
|
-
* `afterSequence
|
|
157
|
+
* RFC-MACP-0006 §3.2: Send a subscribe-only frame to receive session history
|
|
158
|
+
* + live broadcast. The runtime replays accepted envelopes strictly after
|
|
159
|
+
* `afterSequence`, then continues with live broadcast.
|
|
160
|
+
*
|
|
161
|
+
* `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
|
|
162
|
+
* `0` (default) replays from the very start; `k` replays envelopes with
|
|
163
|
+
* ordinal `> k`. Clients derive the ordinal by counting delivered envelopes
|
|
164
|
+
* (the Nth accepted envelope has ordinal N) — see
|
|
165
|
+
* `IncomingMessage.seq`, which is exactly this ordinal under the new contract.
|
|
166
|
+
* Ordinals are stable across log compaction and runtime restart. Resuming
|
|
167
|
+
* below a compacted base returns `FAILED_PRECONDITION` (inspectable via
|
|
168
|
+
* `MacpTransportError.code`) rather than silently skipping history (runtime
|
|
169
|
+
* ≥ 0.5.0; older runtimes compared inclusively against a raw log index).
|
|
148
170
|
*/
|
|
149
171
|
sendSubscribe(sessionId, afterSequence = 0) {
|
|
150
172
|
if (this.closed)
|
|
@@ -223,7 +245,7 @@ class MacpClient {
|
|
|
223
245
|
}
|
|
224
246
|
this.defaultDeadlineMs = options.defaultDeadlineMs;
|
|
225
247
|
this.clientName = options.clientName ?? 'macp-sdk-typescript';
|
|
226
|
-
this.clientVersion = options.clientVersion ?? '0.
|
|
248
|
+
this.clientVersion = options.clientVersion ?? '0.5.0';
|
|
227
249
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
228
250
|
const { protoDir: defaultProtoDir } = require('@multiagentcoordinationprotocol/proto');
|
|
229
251
|
const protoDir = options.protoDir ?? defaultProtoDir;
|
|
@@ -266,7 +288,7 @@ class MacpClient {
|
|
|
266
288
|
return new Promise((resolve, reject) => {
|
|
267
289
|
const callback = (error, response) => {
|
|
268
290
|
if (error)
|
|
269
|
-
reject(new errors_1.MacpTransportError(error.details || error.message));
|
|
291
|
+
reject(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
|
|
270
292
|
else
|
|
271
293
|
resolve(response);
|
|
272
294
|
};
|
|
@@ -382,7 +404,25 @@ class MacpClient {
|
|
|
382
404
|
async listRoots(deadlineMs) {
|
|
383
405
|
return this.unary('ListRoots', {}, undefined, deadlineMs);
|
|
384
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Register an extension-mode descriptor (runtime 0.5.0 guardrails,
|
|
409
|
+
* change-review A5):
|
|
410
|
+
* - the descriptor MUST declare `Commitment` among `terminalMessageTypes` —
|
|
411
|
+
* an ext mode without a terminal `Commitment` can never resolve, so the
|
|
412
|
+
* runtime rejects it. This method fails fast client-side to match.
|
|
413
|
+
* - a later {@link promoteMode} into the reserved `macp.mode.*` namespace is
|
|
414
|
+
* rejected by the runtime.
|
|
415
|
+
* - a SessionStart with an empty `mode_version` binds the registered
|
|
416
|
+
* descriptor's `modeVersion`; a subsequent Commitment must echo that bound
|
|
417
|
+
* version (echoing `""` no longer matches vacuously). `BaseSession`
|
|
418
|
+
* defaults `modeVersion` to `'1.0.0'`, so set the descriptor's version to
|
|
419
|
+
* match, or override the session's `modeVersion`.
|
|
420
|
+
*/
|
|
385
421
|
async registerExtMode(descriptor, options) {
|
|
422
|
+
if (!descriptor.terminalMessageTypes?.includes('Commitment')) {
|
|
423
|
+
throw new errors_1.MacpSessionError(`ext-mode descriptor '${descriptor.mode}' must declare 'Commitment' in terminalMessageTypes; ` +
|
|
424
|
+
'a mode without a terminal Commitment can never resolve and the runtime rejects it.');
|
|
425
|
+
}
|
|
386
426
|
const auth = this.requireAuth(options?.auth);
|
|
387
427
|
return this.unary('RegisterExtMode', { modeDescriptor: descriptor }, auth, options?.deadlineMs);
|
|
388
428
|
}
|
|
@@ -412,10 +452,42 @@ class MacpClient {
|
|
|
412
452
|
const res = await this.unary('ListPolicies', { mode: mode || '' }, auth, options?.deadlineMs);
|
|
413
453
|
return res.descriptors || [];
|
|
414
454
|
}
|
|
415
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
|
|
457
|
+
* lets the server choose the page size; the server MAY cap it. Callers MUST
|
|
458
|
+
* NOT assume the listing is complete unless `nextPageToken` is empty. Pass a
|
|
459
|
+
* returned non-empty `nextPageToken` back as `pageToken` to fetch the next
|
|
460
|
+
* page; a stale token yields `INVALID_ARGUMENT`.
|
|
461
|
+
*/
|
|
462
|
+
async listSessionsPage(options) {
|
|
416
463
|
const auth = this.requireAuth(options?.auth);
|
|
417
|
-
const res = await this.unary('ListSessions', {}, auth, options?.deadlineMs);
|
|
418
|
-
return res.sessions
|
|
464
|
+
const res = await this.unary('ListSessions', { pageSize: options?.pageSize ?? 0, pageToken: options?.pageToken ?? '' }, auth, options?.deadlineMs);
|
|
465
|
+
return { sessions: res.sessions ?? [], nextPageToken: res.nextPageToken ?? '' };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Enumerate ALL sessions, transparently walking pages until the runtime
|
|
469
|
+
* returns an empty `nextPageToken`. Preserves the "complete list" semantics
|
|
470
|
+
* this method has always documented, even against runtimes that cap page
|
|
471
|
+
* sizes (proto ≥ 0.1.6). For manual page control, use {@link listSessionsPage}.
|
|
472
|
+
*/
|
|
473
|
+
async listSessions(options) {
|
|
474
|
+
const all = [];
|
|
475
|
+
let pageToken = '';
|
|
476
|
+
// Bound the walk defensively so a misbehaving runtime that never returns an
|
|
477
|
+
// empty token cannot spin forever.
|
|
478
|
+
for (let page = 0; page < 100_000; page++) {
|
|
479
|
+
const res = await this.listSessionsPage({
|
|
480
|
+
pageSize: options?.pageSize,
|
|
481
|
+
pageToken,
|
|
482
|
+
auth: options?.auth,
|
|
483
|
+
deadlineMs: options?.deadlineMs,
|
|
484
|
+
});
|
|
485
|
+
all.push(...res.sessions);
|
|
486
|
+
if (!res.nextPageToken)
|
|
487
|
+
return all;
|
|
488
|
+
pageToken = res.nextPageToken;
|
|
489
|
+
}
|
|
490
|
+
return all;
|
|
419
491
|
}
|
|
420
492
|
watchSessions(auth) {
|
|
421
493
|
const metadata = this.metadata(auth);
|
|
@@ -425,10 +497,6 @@ class MacpClient {
|
|
|
425
497
|
const metadata = this.metadata(auth);
|
|
426
498
|
return metadata ? this.client.WatchPolicies({}, metadata) : this.client.WatchPolicies({});
|
|
427
499
|
}
|
|
428
|
-
/** @deprecated Use {@link watchPolicies}. Scheduled for removal in 0.4.0. */
|
|
429
|
-
_watchPolicies(auth) {
|
|
430
|
-
return this.watchPolicies(auth);
|
|
431
|
-
}
|
|
432
500
|
openStream(options) {
|
|
433
501
|
const auth = this.requireAuth(options?.auth);
|
|
434
502
|
const metadata = this.metadata(auth);
|
|
@@ -443,25 +511,15 @@ class MacpClient {
|
|
|
443
511
|
const metadata = this.metadata(auth);
|
|
444
512
|
return metadata ? this.client.WatchRoots({}, metadata) : this.client.WatchRoots({});
|
|
445
513
|
}
|
|
446
|
-
watchSignals(auth) {
|
|
447
|
-
const metadata = this.metadata(auth);
|
|
448
|
-
return metadata ? this.client.WatchSignals({}, metadata) : this.client.WatchSignals({});
|
|
449
|
-
}
|
|
450
514
|
/**
|
|
451
|
-
*
|
|
452
|
-
* {@link
|
|
453
|
-
*
|
|
515
|
+
* Subscribe to the ambient signal plane. Requires auth as of runtime 0.5.0 —
|
|
516
|
+
* routed through {@link requireAuth} so a missing credential fails fast with a
|
|
517
|
+
* clear client-side error instead of a stream `UNAUTHENTICATED`.
|
|
454
518
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
_watchRoots(auth) {
|
|
460
|
-
return this.watchRoots(auth);
|
|
461
|
-
}
|
|
462
|
-
/** @deprecated Use {@link watchSignals}. Scheduled for removal in 0.4.0. */
|
|
463
|
-
_watchSignals(auth) {
|
|
464
|
-
return this.watchSignals(auth);
|
|
519
|
+
watchSignals(auth) {
|
|
520
|
+
const selected = this.requireAuth(auth);
|
|
521
|
+
const metadata = (0, auth_1.metadataFromAuth)(selected);
|
|
522
|
+
return this.client.WatchSignals({}, metadata);
|
|
465
523
|
}
|
|
466
524
|
async sendSignal(options) {
|
|
467
525
|
(0, validation_1.validateSignalType)(options.signalType, options.data);
|
package/dist/decision.d.ts
CHANGED
package/dist/decision.js
CHANGED
|
@@ -40,6 +40,7 @@ class DecisionSession {
|
|
|
40
40
|
intent: input.intent,
|
|
41
41
|
participants: input.participants,
|
|
42
42
|
ttlMs: input.ttlMs,
|
|
43
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
43
44
|
modeVersion: this.modeVersion,
|
|
44
45
|
configurationVersion: this.configurationVersion,
|
|
45
46
|
});
|
|
@@ -47,6 +48,7 @@ class DecisionSession {
|
|
|
47
48
|
intent: input.intent,
|
|
48
49
|
participants: input.participants,
|
|
49
50
|
ttlMs: input.ttlMs,
|
|
51
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
50
52
|
contextId: input.contextId,
|
|
51
53
|
extensions: input.extensions,
|
|
52
54
|
roots: input.roots,
|
package/dist/envelope.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export declare function buildSessionStartPayload(input: {
|
|
|
7
7
|
intent: string;
|
|
8
8
|
participants: string[];
|
|
9
9
|
ttlMs: number;
|
|
10
|
+
/**
|
|
11
|
+
* Per-session max-suspend cap in ms (proto ≥ 0.1.5). `0`/absent selects the
|
|
12
|
+
* runtime default (7 days). Emitted as `0` when omitted, matching `ttlMs`'s
|
|
13
|
+
* "0 = default" convention.
|
|
14
|
+
*/
|
|
15
|
+
maxSuspendMs?: number;
|
|
10
16
|
modeVersion?: string;
|
|
11
17
|
configurationVersion?: string;
|
|
12
18
|
policyVersion?: string;
|
|
@@ -24,6 +30,20 @@ export declare function buildCommitmentRef(input: {
|
|
|
24
30
|
sessionId: string;
|
|
25
31
|
commitmentHash: string;
|
|
26
32
|
}): CommitmentRef;
|
|
33
|
+
/**
|
|
34
|
+
* Build a `CommitmentPayload`. Defaults `policyVersion` to
|
|
35
|
+
* {@link DEFAULT_POLICY_VERSION} (`'policy.default'`) when omitted.
|
|
36
|
+
*
|
|
37
|
+
* Policy-version echo (runtime ≥ 0.5.0): a Commitment with an **empty**
|
|
38
|
+
* `policyVersion` matches the session's bound policy — a session started with an
|
|
39
|
+
* empty policy_version no longer has to echo `'policy.default'`. A **non-empty**
|
|
40
|
+
* value must equal the session's resolved policy id exactly. The mode session
|
|
41
|
+
* helpers (`DecisionSession.commit`, etc.) echo the session-bound value, which
|
|
42
|
+
* is correct against both old and new runtimes; this standalone builder keeps
|
|
43
|
+
* the `'policy.default'` default for backward compatibility (older runtimes
|
|
44
|
+
* rejected an empty echo). Pass `policyVersion: ''` explicitly to opt into the
|
|
45
|
+
* new empty-matches-bound behavior — `''` is not coalesced by the default.
|
|
46
|
+
*/
|
|
27
47
|
export declare function buildCommitmentPayload(input: {
|
|
28
48
|
action: string;
|
|
29
49
|
authorityScope: string;
|
package/dist/envelope.js
CHANGED
|
@@ -36,6 +36,7 @@ function buildSessionStartPayload(input) {
|
|
|
36
36
|
configurationVersion: input.configurationVersion ?? constants_1.DEFAULT_CONFIGURATION_VERSION,
|
|
37
37
|
policyVersion: input.policyVersion ?? constants_1.DEFAULT_POLICY_VERSION,
|
|
38
38
|
ttlMs: input.ttlMs,
|
|
39
|
+
maxSuspendMs: input.maxSuspendMs ?? 0,
|
|
39
40
|
contextId: input.contextId ?? '',
|
|
40
41
|
extensions: input.extensions ?? {},
|
|
41
42
|
roots: input.roots ?? [],
|
|
@@ -56,6 +57,20 @@ function inferOutcomePositive(action) {
|
|
|
56
57
|
function buildCommitmentRef(input) {
|
|
57
58
|
return { sessionId: input.sessionId, commitmentHash: input.commitmentHash };
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a `CommitmentPayload`. Defaults `policyVersion` to
|
|
62
|
+
* {@link DEFAULT_POLICY_VERSION} (`'policy.default'`) when omitted.
|
|
63
|
+
*
|
|
64
|
+
* Policy-version echo (runtime ≥ 0.5.0): a Commitment with an **empty**
|
|
65
|
+
* `policyVersion` matches the session's bound policy — a session started with an
|
|
66
|
+
* empty policy_version no longer has to echo `'policy.default'`. A **non-empty**
|
|
67
|
+
* value must equal the session's resolved policy id exactly. The mode session
|
|
68
|
+
* helpers (`DecisionSession.commit`, etc.) echo the session-bound value, which
|
|
69
|
+
* is correct against both old and new runtimes; this standalone builder keeps
|
|
70
|
+
* the `'policy.default'` default for backward compatibility (older runtimes
|
|
71
|
+
* rejected an empty echo). Pass `policyVersion: ''` explicitly to opt into the
|
|
72
|
+
* new empty-matches-bound behavior — `''` is not coalesced by the default.
|
|
73
|
+
*/
|
|
59
74
|
function buildCommitmentPayload(input) {
|
|
60
75
|
const payload = {
|
|
61
76
|
commitmentId: input.commitmentId ?? newCommitmentId(),
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,7 +3,16 @@ export declare class MacpSdkError extends Error {
|
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
5
|
export declare class MacpTransportError extends MacpSdkError {
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* gRPC status name (e.g. `RESOURCE_EXHAUSTED`, `FAILED_PRECONDITION`,
|
|
8
|
+
* `UNAUTHENTICATED`) when the underlying transport failure carried one.
|
|
9
|
+
* Lets consumers distinguish, for example, watch-stream consumer lag
|
|
10
|
+
* (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
|
|
11
|
+
* → do not reconnect), or a passive-subscribe resume below a compacted base
|
|
12
|
+
* (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
|
|
13
|
+
*/
|
|
14
|
+
readonly code?: string;
|
|
15
|
+
constructor(message: string, code?: string);
|
|
7
16
|
}
|
|
8
17
|
/**
|
|
9
18
|
* Structured NACK record. Mirrors python-sdk's `AckFailure` dataclass so
|
package/dist/errors.js
CHANGED
|
@@ -9,9 +9,19 @@ class MacpSdkError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
exports.MacpSdkError = MacpSdkError;
|
|
11
11
|
class MacpTransportError extends MacpSdkError {
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* gRPC status name (e.g. `RESOURCE_EXHAUSTED`, `FAILED_PRECONDITION`,
|
|
14
|
+
* `UNAUTHENTICATED`) when the underlying transport failure carried one.
|
|
15
|
+
* Lets consumers distinguish, for example, watch-stream consumer lag
|
|
16
|
+
* (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
|
|
17
|
+
* → do not reconnect), or a passive-subscribe resume below a compacted base
|
|
18
|
+
* (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
|
|
19
|
+
*/
|
|
20
|
+
code;
|
|
21
|
+
constructor(message, code) {
|
|
13
22
|
super(message);
|
|
14
23
|
this.name = 'MacpTransportError';
|
|
24
|
+
this.code = code;
|
|
15
25
|
}
|
|
16
26
|
}
|
|
17
27
|
exports.MacpTransportError = MacpTransportError;
|
package/dist/handoff.d.ts
CHANGED
package/dist/handoff.js
CHANGED
|
@@ -40,6 +40,7 @@ class HandoffSession {
|
|
|
40
40
|
intent: input.intent,
|
|
41
41
|
participants: input.participants,
|
|
42
42
|
ttlMs: input.ttlMs,
|
|
43
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
43
44
|
modeVersion: this.modeVersion,
|
|
44
45
|
configurationVersion: this.configurationVersion,
|
|
45
46
|
});
|
|
@@ -47,6 +48,7 @@ class HandoffSession {
|
|
|
47
48
|
intent: input.intent,
|
|
48
49
|
participants: input.participants,
|
|
49
50
|
ttlMs: input.ttlMs,
|
|
51
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
50
52
|
contextId: input.contextId,
|
|
51
53
|
extensions: input.extensions,
|
|
52
54
|
roots: input.roots,
|
|
@@ -89,12 +91,18 @@ class HandoffSession {
|
|
|
89
91
|
}
|
|
90
92
|
async acceptHandoff(input) {
|
|
91
93
|
(0, validation_1.validateRequiredField)('handoffId', input.handoffId);
|
|
94
|
+
// `implicit` is a runtime-emitted-only flag (RFC-MACP-0010 §5.1); the
|
|
95
|
+
// runtime rejects client-submitted accepts with implicit=true. Strip it so
|
|
96
|
+
// a caller can never produce a rejected envelope, regardless of what they
|
|
97
|
+
// pass — belt and braces with the JSDoc "read-only" contract on the type.
|
|
98
|
+
const rest = { ...input };
|
|
99
|
+
delete rest.implicit;
|
|
92
100
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
93
101
|
mode: constants_1.MODE_HANDOFF,
|
|
94
102
|
messageType: 'HandoffAccept',
|
|
95
103
|
sessionId: this.sessionId,
|
|
96
104
|
sender: this.senderFor(input.sender, input.auth),
|
|
97
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffAccept',
|
|
105
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffAccept', rest),
|
|
98
106
|
});
|
|
99
107
|
return this.sendAndTrack(envelope, input.auth);
|
|
100
108
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.agent = exports.Auth = void 0;
|
|
39
|
+
exports.agent = exports.VERSION = exports.Auth = void 0;
|
|
40
40
|
// Auth: export only the public constructors and types. The helpers
|
|
41
41
|
// `validateAuth`, `authSender`, `assertSenderMatchesIdentity`, and
|
|
42
42
|
// `metadataFromAuth` are SDK-internal — importable from the `./auth`
|
|
@@ -61,5 +61,7 @@ __exportStar(require("./quorum"), exports);
|
|
|
61
61
|
__exportStar(require("./task"), exports);
|
|
62
62
|
__exportStar(require("./types"), exports);
|
|
63
63
|
__exportStar(require("./validation"), exports);
|
|
64
|
+
var version_1 = require("./version");
|
|
65
|
+
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_1.VERSION; } });
|
|
64
66
|
__exportStar(require("./watchers"), exports);
|
|
65
67
|
exports.agent = __importStar(require("./agent"));
|
package/dist/policy.d.ts
CHANGED
|
@@ -15,7 +15,18 @@ export interface CommitmentRules {
|
|
|
15
15
|
}
|
|
16
16
|
export interface VotingRules {
|
|
17
17
|
algorithm?: 'none' | 'majority' | 'supermajority' | 'unanimous' | 'weighted' | 'plurality';
|
|
18
|
+
/**
|
|
19
|
+
* Vote-share fraction on a **0–1 scale** (e.g. `0.5` = simple majority). This
|
|
20
|
+
* is a DIFFERENT field from the quorum `percentage` below — do not confuse
|
|
21
|
+
* the scales.
|
|
22
|
+
*/
|
|
18
23
|
threshold?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Participation quorum. For `type: 'percentage'`, `value` is an **integer
|
|
26
|
+
* 0–100** (the runtime evaluates it on the same 0–100 scale as the quorum
|
|
27
|
+
* mode's threshold), NOT a 0–1 fraction. For `type: 'count'`, an absolute
|
|
28
|
+
* participant count.
|
|
29
|
+
*/
|
|
19
30
|
quorum?: {
|
|
20
31
|
type: 'count' | 'percentage';
|
|
21
32
|
value: number;
|
|
@@ -39,6 +50,15 @@ export interface EvaluationRules {
|
|
|
39
50
|
}
|
|
40
51
|
export interface QuorumThreshold {
|
|
41
52
|
type: 'n_of_m' | 'percentage' | 'weighted';
|
|
53
|
+
/**
|
|
54
|
+
* The approval bar (RFC-MACP-0012 §4.2) — how many APPROVE commitments the
|
|
55
|
+
* request needs, NOT a participation quorum. Scale depends on `type`:
|
|
56
|
+
* - `n_of_m` / `weighted`: an absolute count (or weight sum).
|
|
57
|
+
* - `percentage`: an **integer 0–100**; the runtime computes the bar as
|
|
58
|
+
* `ceil(value / 100 × participants)`. So `75` means "≥ 75% of participants
|
|
59
|
+
* must approve", NOT `0.75`. `buildQuorumPolicy` validates this range and
|
|
60
|
+
* throws on a non-integer or out-of-[0,100] value.
|
|
61
|
+
*/
|
|
42
62
|
value: number;
|
|
43
63
|
}
|
|
44
64
|
export interface AbstentionRules {
|
package/dist/policy.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.buildQuorumPolicy = buildQuorumPolicy;
|
|
|
5
5
|
exports.buildProposalPolicy = buildProposalPolicy;
|
|
6
6
|
exports.buildTaskPolicy = buildTaskPolicy;
|
|
7
7
|
exports.buildHandoffPolicy = buildHandoffPolicy;
|
|
8
|
+
const errors_1 = require("./errors");
|
|
8
9
|
// ── Builder helpers ─────────────────────────────────────────────────
|
|
9
10
|
function serializeCommitment(commitment) {
|
|
10
11
|
return {
|
|
@@ -49,6 +50,14 @@ function buildDecisionPolicy(policyId, description, rules) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
function buildQuorumPolicy(policyId, description, rules) {
|
|
53
|
+
if (rules.threshold?.type === 'percentage') {
|
|
54
|
+
const v = rules.threshold.value;
|
|
55
|
+
if (!Number.isInteger(v) || v < 0 || v > 100) {
|
|
56
|
+
throw new errors_1.MacpSessionError(`quorum percentage threshold must be an integer in [0, 100] (e.g. 75 for 75%), got ${v}. ` +
|
|
57
|
+
'The runtime computes the approval bar as ceil(value/100 × participants); a fractional value ' +
|
|
58
|
+
'like 0.75 would round to a ~1% bar.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
const rulesJson = {
|
|
53
62
|
threshold: {
|
|
54
63
|
type: rules.threshold?.type ?? 'n_of_m',
|
|
@@ -10,6 +10,12 @@ export interface HandoffRecord {
|
|
|
10
10
|
contextContentType?: string;
|
|
11
11
|
acceptedBy?: string;
|
|
12
12
|
declinedBy?: string;
|
|
13
|
+
/**
|
|
14
|
+
* `true` when the accept that resolved this handoff was a runtime-emitted
|
|
15
|
+
* synthetic implicit accept (RFC-MACP-0010 §5.1, proto ≥ 0.1.6). `false` for
|
|
16
|
+
* an explicit target accept. Only meaningful once `status === 'accepted'`.
|
|
17
|
+
*/
|
|
18
|
+
implicit?: boolean;
|
|
13
19
|
}
|
|
14
20
|
export declare class HandoffProjection {
|
|
15
21
|
readonly handoffs: Map<string, HandoffRecord>;
|
|
@@ -22,6 +28,12 @@ export declare class HandoffProjection {
|
|
|
22
28
|
getHandoff(handoffId: string): HandoffRecord | undefined;
|
|
23
29
|
isAccepted(handoffId: string): boolean;
|
|
24
30
|
isDeclined(handoffId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `true` when the handoff was resolved by a runtime-emitted synthetic implicit
|
|
33
|
+
* accept (RFC-MACP-0010 §5.1) rather than an explicit target accept. Returns
|
|
34
|
+
* `false` for explicitly-accepted or not-yet-accepted handoffs.
|
|
35
|
+
*/
|
|
36
|
+
isImplicitlyAccepted(handoffId: string): boolean;
|
|
25
37
|
pendingHandoffs(): HandoffRecord[];
|
|
26
38
|
hasAcceptedOffer(handoffId?: string): boolean;
|
|
27
39
|
activeOffer(): HandoffRecord | undefined;
|
|
@@ -47,6 +47,10 @@ class HandoffProjection {
|
|
|
47
47
|
if (handoff) {
|
|
48
48
|
handoff.status = 'accepted';
|
|
49
49
|
handoff.acceptedBy = record.acceptedBy;
|
|
50
|
+
// proto3 bool defaults are materialized to `false` on decode
|
|
51
|
+
// (proto-registry), so this is always a real boolean once proto 0.1.6
|
|
52
|
+
// is loaded — `true` marks a runtime synthetic implicit accept.
|
|
53
|
+
handoff.implicit = record.implicit ?? false;
|
|
50
54
|
}
|
|
51
55
|
this.phase = 'Accepted';
|
|
52
56
|
break;
|
|
@@ -89,6 +93,15 @@ class HandoffProjection {
|
|
|
89
93
|
isDeclined(handoffId) {
|
|
90
94
|
return this.handoffs.get(handoffId)?.status === 'declined';
|
|
91
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* `true` when the handoff was resolved by a runtime-emitted synthetic implicit
|
|
98
|
+
* accept (RFC-MACP-0010 §5.1) rather than an explicit target accept. Returns
|
|
99
|
+
* `false` for explicitly-accepted or not-yet-accepted handoffs.
|
|
100
|
+
*/
|
|
101
|
+
isImplicitlyAccepted(handoffId) {
|
|
102
|
+
const handoff = this.handoffs.get(handoffId);
|
|
103
|
+
return handoff?.status === 'accepted' && handoff.implicit === true;
|
|
104
|
+
}
|
|
92
105
|
pendingHandoffs() {
|
|
93
106
|
return [...this.handoffs.values()].filter((h) => h.status === 'offered' || h.status === 'context_sent');
|
|
94
107
|
}
|
package/dist/proposal.d.ts
CHANGED
package/dist/proposal.js
CHANGED
|
@@ -40,6 +40,7 @@ class ProposalSession {
|
|
|
40
40
|
intent: input.intent,
|
|
41
41
|
participants: input.participants,
|
|
42
42
|
ttlMs: input.ttlMs,
|
|
43
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
43
44
|
modeVersion: this.modeVersion,
|
|
44
45
|
configurationVersion: this.configurationVersion,
|
|
45
46
|
});
|
|
@@ -47,6 +48,7 @@ class ProposalSession {
|
|
|
47
48
|
intent: input.intent,
|
|
48
49
|
participants: input.participants,
|
|
49
50
|
ttlMs: input.ttlMs,
|
|
51
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
50
52
|
contextId: input.contextId,
|
|
51
53
|
extensions: input.extensions,
|
|
52
54
|
roots: input.roots,
|
package/dist/proto-registry.d.ts
CHANGED
|
@@ -7,6 +7,16 @@ export declare class ProtoRegistry {
|
|
|
7
7
|
decodeMessage(typeName: string, payload: Buffer): Record<string, unknown>;
|
|
8
8
|
encodeKnownPayload(mode: string, messageType: string, value: Record<string, unknown>): Buffer;
|
|
9
9
|
decodeKnownPayload(mode: string, messageType: string, payload: Buffer): Record<string, unknown> | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
|
|
12
|
+
* formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
|
|
13
|
+
* pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
|
|
14
|
+
* tried first — mirroring the runtime's permanent decode order — and the two
|
|
15
|
+
* encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
|
|
16
|
+
* tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
|
|
17
|
+
* versa. Both normalize to `{ value: string }`.
|
|
18
|
+
*/
|
|
19
|
+
private decodeMultiRoundContribute;
|
|
10
20
|
private lookupType;
|
|
11
21
|
private tryDecodeUtf8;
|
|
12
22
|
}
|
package/dist/proto-registry.js
CHANGED
|
@@ -46,6 +46,9 @@ const CORE_MAP = {
|
|
|
46
46
|
Signal: 'macp.v1.SignalPayload',
|
|
47
47
|
Progress: 'macp.v1.ProgressPayload',
|
|
48
48
|
};
|
|
49
|
+
// The one mapping whose historical payloads may be legacy JSON. Runtime replays
|
|
50
|
+
// pre-proto histories byte-identically, so decode must accept both encodings.
|
|
51
|
+
const MULTI_ROUND_CONTRIBUTE = 'macp.modes.multi_round.v1.ContributePayload';
|
|
49
52
|
const MODE_MAP = {
|
|
50
53
|
[constants_1.MODE_DECISION]: {
|
|
51
54
|
Proposal: 'macp.modes.decision.v1.ProposalPayload',
|
|
@@ -81,7 +84,10 @@ const MODE_MAP = {
|
|
|
81
84
|
Abstain: 'macp.modes.quorum.v1.AbstainPayload',
|
|
82
85
|
},
|
|
83
86
|
[constants_1.MODE_MULTI_ROUND]: {
|
|
84
|
-
|
|
87
|
+
// Canonical protobuf as of proto 0.1.4 / runtime 0.5.0. Encode is always
|
|
88
|
+
// protobuf; decode tries legacy JSON (`{"value":"..."}`) first, then
|
|
89
|
+
// protobuf — see `MULTI_ROUND_CONTRIBUTE` handling in decodeKnownPayload.
|
|
90
|
+
Contribute: MULTI_ROUND_CONTRIBUTE,
|
|
85
91
|
},
|
|
86
92
|
};
|
|
87
93
|
const PROTO_FILES = [
|
|
@@ -93,6 +99,7 @@ const PROTO_FILES = [
|
|
|
93
99
|
'macp/modes/task/v1/task.proto',
|
|
94
100
|
'macp/modes/handoff/v1/handoff.proto',
|
|
95
101
|
'macp/modes/quorum/v1/quorum.proto',
|
|
102
|
+
'macp/modes/multi_round/v1/multi_round.proto',
|
|
96
103
|
];
|
|
97
104
|
class ProtoRegistry {
|
|
98
105
|
protoDir;
|
|
@@ -155,8 +162,34 @@ class ProtoRegistry {
|
|
|
155
162
|
return this.tryDecodeUtf8(payload);
|
|
156
163
|
if (typeName === '__json__')
|
|
157
164
|
return this.tryDecodeUtf8(payload);
|
|
165
|
+
if (typeName === MULTI_ROUND_CONTRIBUTE)
|
|
166
|
+
return this.decodeMultiRoundContribute(payload);
|
|
158
167
|
return this.decodeMessage(typeName, payload);
|
|
159
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
|
|
171
|
+
* formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
|
|
172
|
+
* pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
|
|
173
|
+
* tried first — mirroring the runtime's permanent decode order — and the two
|
|
174
|
+
* encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
|
|
175
|
+
* tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
|
|
176
|
+
* versa. Both normalize to `{ value: string }`.
|
|
177
|
+
*/
|
|
178
|
+
decodeMultiRoundContribute(payload) {
|
|
179
|
+
if (!payload.length)
|
|
180
|
+
return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
|
|
181
|
+
if (payload[0] === 0x7b) {
|
|
182
|
+
// Leading `{` — legacy JSON. Parse and normalize to { value }.
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(payload.toString('utf8'));
|
|
185
|
+
return { value: typeof parsed.value === 'string' ? parsed.value : String(parsed.value ?? '') };
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Not valid JSON despite the leading brace — fall through to protobuf.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
|
|
192
|
+
}
|
|
160
193
|
lookupType(typeName) {
|
|
161
194
|
const lookedUp = this.root.lookupType(typeName);
|
|
162
195
|
if (!(lookedUp instanceof protobufjs_1.default.Type))
|
package/dist/quorum.d.ts
CHANGED
package/dist/quorum.js
CHANGED
|
@@ -40,6 +40,7 @@ class QuorumSession {
|
|
|
40
40
|
intent: input.intent,
|
|
41
41
|
participants: input.participants,
|
|
42
42
|
ttlMs: input.ttlMs,
|
|
43
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
43
44
|
modeVersion: this.modeVersion,
|
|
44
45
|
configurationVersion: this.configurationVersion,
|
|
45
46
|
});
|
|
@@ -47,6 +48,7 @@ class QuorumSession {
|
|
|
47
48
|
intent: input.intent,
|
|
48
49
|
participants: input.participants,
|
|
49
50
|
ttlMs: input.ttlMs,
|
|
51
|
+
maxSuspendMs: input.maxSuspendMs,
|
|
50
52
|
contextId: input.contextId,
|
|
51
53
|
extensions: input.extensions,
|
|
52
54
|
roots: input.roots,
|
package/dist/retry.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export interface RetryPolicy {
|
|
|
5
5
|
maxRetries: number;
|
|
6
6
|
backoffBase: number;
|
|
7
7
|
backoffMax: number;
|
|
8
|
+
/**
|
|
9
|
+
* Retryable **ack** (NACK) codes — MACP application-level error codes on a
|
|
10
|
+
* `Send` response (e.g. `RATE_LIMITED`, `INTERNAL_ERROR`). This is distinct
|
|
11
|
+
* from a gRPC **stream** status such as `RESOURCE_EXHAUSTED` (watch-stream
|
|
12
|
+
* consumer lag), which is not an ack code and is not handled here — reconnect
|
|
13
|
+
* the stream instead (see the watcher classes' reconnect JSDoc).
|
|
14
|
+
*/
|
|
8
15
|
retryableCodes: Set<string>;
|
|
9
16
|
}
|
|
10
17
|
export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|