macp-sdk-typescript 0.4.0 → 0.5.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 +77 -0
- package/README.md +4 -3
- 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/policy.d.ts +35 -0
- package/dist/policy.js +18 -2
- 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 +48 -2
- 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/watchers.d.ts +17 -13
- package/dist/watchers.js +26 -26
- package/package.json +4 -4
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/policy.d.ts
CHANGED
|
@@ -4,10 +4,29 @@ export interface CommitmentRules {
|
|
|
4
4
|
designatedRoles?: string[];
|
|
5
5
|
/** Decision-specific: require quorum before commit. Ignored for other modes. */
|
|
6
6
|
requireVoteQuorum?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* RFC-MACP-0012 schema_version 2, Decision mode only: when `true`, a
|
|
9
|
+
* reject-majority resolves the session with a committed *negative* outcome
|
|
10
|
+
* (`outcome_positive = false`) instead of denying commitment. Emitted only by
|
|
11
|
+
* `buildDecisionPolicy`, so the still-v1 quorum/proposal/task/handoff
|
|
12
|
+
* commitment schemas are unaffected. Default `false` preserves v1 behavior.
|
|
13
|
+
*/
|
|
14
|
+
allowDeclineOverApproval?: boolean;
|
|
7
15
|
}
|
|
8
16
|
export interface VotingRules {
|
|
9
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
|
+
*/
|
|
10
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
|
+
*/
|
|
11
30
|
quorum?: {
|
|
12
31
|
type: 'count' | 'percentage';
|
|
13
32
|
value: number;
|
|
@@ -17,6 +36,13 @@ export interface VotingRules {
|
|
|
17
36
|
export interface ObjectionHandlingRules {
|
|
18
37
|
criticalSeverityVetoes?: boolean;
|
|
19
38
|
vetoThreshold?: number;
|
|
39
|
+
/**
|
|
40
|
+
* RFC-MACP-0012 schema_version 2: action taken when a critical objection would
|
|
41
|
+
* block commitment. `deny` rejects the commitment (legacy default),
|
|
42
|
+
* `finalize_decline` resolves the session as a negative outcome, `hold` leaves
|
|
43
|
+
* the session open. Default `deny` preserves v1 behavior.
|
|
44
|
+
*/
|
|
45
|
+
criticalObjectionAction?: 'deny' | 'finalize_decline' | 'hold';
|
|
20
46
|
}
|
|
21
47
|
export interface EvaluationRules {
|
|
22
48
|
minimumConfidence?: number;
|
|
@@ -24,6 +50,15 @@ export interface EvaluationRules {
|
|
|
24
50
|
}
|
|
25
51
|
export interface QuorumThreshold {
|
|
26
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
|
+
*/
|
|
27
62
|
value: number;
|
|
28
63
|
}
|
|
29
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 {
|
|
@@ -16,6 +17,12 @@ function serializeCommitment(commitment) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
function buildDecisionPolicy(policyId, description, rules) {
|
|
20
|
+
// Decision-only: extend the shared v1 commitment rules with the schema_version 2
|
|
21
|
+
// decline-over-approval switch, appended after the shared keys so it does not
|
|
22
|
+
// leak into the still-v1 quorum/proposal/task/handoff commitment blocks
|
|
23
|
+
// (parity with python-sdk `_commitment_dict` + `build_decision_policy`).
|
|
24
|
+
const commitmentSection = serializeCommitment(rules.commitment);
|
|
25
|
+
commitmentSection.allow_decline_over_approval = rules.commitment?.allowDeclineOverApproval ?? false;
|
|
19
26
|
const rulesJson = {
|
|
20
27
|
voting: {
|
|
21
28
|
algorithm: rules.voting?.algorithm ?? 'none',
|
|
@@ -26,22 +33,31 @@ function buildDecisionPolicy(policyId, description, rules) {
|
|
|
26
33
|
objection_handling: {
|
|
27
34
|
critical_severity_vetoes: rules.objectionHandling?.criticalSeverityVetoes ?? false,
|
|
28
35
|
veto_threshold: rules.objectionHandling?.vetoThreshold ?? 1,
|
|
36
|
+
critical_objection_action: rules.objectionHandling?.criticalObjectionAction ?? 'deny',
|
|
29
37
|
},
|
|
30
38
|
evaluation: {
|
|
31
39
|
minimum_confidence: rules.evaluation?.minimumConfidence ?? 0,
|
|
32
40
|
required_before_voting: rules.evaluation?.requiredBeforeVoting ?? false,
|
|
33
41
|
},
|
|
34
|
-
commitment:
|
|
42
|
+
commitment: commitmentSection,
|
|
35
43
|
};
|
|
36
44
|
return {
|
|
37
45
|
policyId,
|
|
38
46
|
mode: 'macp.mode.decision.v1',
|
|
39
47
|
description,
|
|
40
48
|
rules: JSON.stringify(rulesJson),
|
|
41
|
-
schemaVersion:
|
|
49
|
+
schemaVersion: 2,
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
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
|
+
}
|
|
45
61
|
const rulesJson = {
|
|
46
62
|
threshold: {
|
|
47
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;
|
|
@@ -121,12 +128,25 @@ class ProtoRegistry {
|
|
|
121
128
|
decodeMessage(typeName, payload) {
|
|
122
129
|
const type = this.lookupType(typeName);
|
|
123
130
|
const decoded = type.decode(payload);
|
|
124
|
-
|
|
131
|
+
const obj = type.toObject(decoded, {
|
|
125
132
|
longs: String,
|
|
126
133
|
enums: String,
|
|
127
134
|
bytes: Buffer,
|
|
128
135
|
defaults: false,
|
|
129
136
|
});
|
|
137
|
+
// proto3 omits default-valued scalars on the wire; protobufjs (>=8.6) drops
|
|
138
|
+
// them again on decode with `defaults:false`. For a `bool` that means an
|
|
139
|
+
// explicit `false` — e.g. `Commitment.outcome_positive` on a negative
|
|
140
|
+
// (decline) outcome — decodes as *absent*, indistinguishable from unset and
|
|
141
|
+
// silently mis-read as positive. Materialize proto3 bool defaults so the
|
|
142
|
+
// read-model always sees the real boolean, mirroring Python protobuf where a
|
|
143
|
+
// schema bool field is always present.
|
|
144
|
+
for (const field of type.fieldsArray) {
|
|
145
|
+
if (field.type === 'bool' && !field.repeated && obj[field.name] === undefined) {
|
|
146
|
+
obj[field.name] = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return obj;
|
|
130
150
|
}
|
|
131
151
|
encodeKnownPayload(mode, messageType, value) {
|
|
132
152
|
const typeName = this.getKnownTypeName(mode, messageType);
|
|
@@ -142,8 +162,34 @@ class ProtoRegistry {
|
|
|
142
162
|
return this.tryDecodeUtf8(payload);
|
|
143
163
|
if (typeName === '__json__')
|
|
144
164
|
return this.tryDecodeUtf8(payload);
|
|
165
|
+
if (typeName === MULTI_ROUND_CONTRIBUTE)
|
|
166
|
+
return this.decodeMultiRoundContribute(payload);
|
|
145
167
|
return this.decodeMessage(typeName, payload);
|
|
146
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
|
+
}
|
|
147
193
|
lookupType(typeName) {
|
|
148
194
|
const lookedUp = this.root.lookupType(typeName);
|
|
149
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;
|
package/dist/task.d.ts
CHANGED
package/dist/task.js
CHANGED
|
@@ -40,6 +40,7 @@ class TaskSession {
|
|
|
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 TaskSession {
|
|
|
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/types.d.ts
CHANGED
|
@@ -106,6 +106,15 @@ export interface SessionStartPayload {
|
|
|
106
106
|
configurationVersion: string;
|
|
107
107
|
policyVersion?: string;
|
|
108
108
|
ttlMs: number;
|
|
109
|
+
/**
|
|
110
|
+
* Maximum cumulative suspended duration (ms) before a SUSPENDED session
|
|
111
|
+
* transitions to EXPIRED (RFC-MACP-0001 §7.5, proto ≥ 0.1.5). Bound at
|
|
112
|
+
* session start like {@link ttlMs}: `0` (or absent) selects the runtime's
|
|
113
|
+
* configured default (7 days). The runtime records the resolved cap so
|
|
114
|
+
* replay is deterministic. Negative values are rejected at SessionStart.
|
|
115
|
+
* Decoded as a string (int64) but accepted as `number` on input.
|
|
116
|
+
*/
|
|
117
|
+
maxSuspendMs?: number | string;
|
|
109
118
|
contextId?: string;
|
|
110
119
|
extensions?: Record<string, Buffer>;
|
|
111
120
|
roots?: Root[];
|
|
@@ -251,6 +260,15 @@ export interface HandoffAcceptPayload {
|
|
|
251
260
|
handoffId: string;
|
|
252
261
|
acceptedBy: string;
|
|
253
262
|
reason?: string;
|
|
263
|
+
/**
|
|
264
|
+
* Runtime-emitted synthetic accepts ONLY (RFC-MACP-0010 §5.1): `true` on the
|
|
265
|
+
* accept the runtime produces when a handoff's `implicit_accept_timeout_ms`
|
|
266
|
+
* elapses (`message_id` = `implicit-accept:<handoff_id>`, proto ≥ 0.1.6).
|
|
267
|
+
* Read-only — clients MUST NOT submit an accept with `implicit = true`; the
|
|
268
|
+
* runtime rejects it. `HandoffSession.acceptHandoff` strips this field before
|
|
269
|
+
* encoding so a caller can never produce a rejected envelope.
|
|
270
|
+
*/
|
|
271
|
+
implicit?: boolean;
|
|
254
272
|
}
|
|
255
273
|
export interface HandoffDeclinePayload {
|
|
256
274
|
handoffId: string;
|
package/dist/validation.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ export declare function validateSeverity(value: string): string;
|
|
|
6
6
|
export declare function validateParticipantCount(count: number): void;
|
|
7
7
|
export declare function validateSignalType(signalType: string, data?: Buffer | Uint8Array): void;
|
|
8
8
|
export declare function validateTtlMs(ttlMs: number): void;
|
|
9
|
+
export declare function validateMaxSuspendMs(maxSuspendMs: number): void;
|
|
9
10
|
export declare function validateParticipants(participants: string[]): void;
|
|
10
11
|
export declare function validateRequiredField(fieldName: string, value: string): void;
|
|
11
12
|
export declare function validateSessionStart(input: {
|
|
12
13
|
intent: string;
|
|
13
14
|
participants: string[];
|
|
14
15
|
ttlMs: number;
|
|
16
|
+
maxSuspendMs?: number;
|
|
15
17
|
modeVersion: string;
|
|
16
18
|
configurationVersion: string;
|
|
17
19
|
}): void;
|
package/dist/validation.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.validateSeverity = validateSeverity;
|
|
|
8
8
|
exports.validateParticipantCount = validateParticipantCount;
|
|
9
9
|
exports.validateSignalType = validateSignalType;
|
|
10
10
|
exports.validateTtlMs = validateTtlMs;
|
|
11
|
+
exports.validateMaxSuspendMs = validateMaxSuspendMs;
|
|
11
12
|
exports.validateParticipants = validateParticipants;
|
|
12
13
|
exports.validateRequiredField = validateRequiredField;
|
|
13
14
|
exports.validateSessionStart = validateSessionStart;
|
|
@@ -65,6 +66,13 @@ function validateTtlMs(ttlMs) {
|
|
|
65
66
|
throw new errors_1.MacpSessionError(`ttl_ms must be in [1, ${MAX_TTL_MS}], got ${ttlMs}`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
function validateMaxSuspendMs(maxSuspendMs) {
|
|
70
|
+
// 0 = "use runtime default"; only negatives are rejected (matches the
|
|
71
|
+
// runtime, which rejects negative max_suspend_ms at SessionStart).
|
|
72
|
+
if (!Number.isFinite(maxSuspendMs) || maxSuspendMs < 0) {
|
|
73
|
+
throw new errors_1.MacpSessionError(`max_suspend_ms must be >= 0 (0 = runtime default), got ${maxSuspendMs}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
68
76
|
function validateParticipants(participants) {
|
|
69
77
|
if (!participants.length) {
|
|
70
78
|
throw new errors_1.MacpSessionError('participants must be non-empty');
|
|
@@ -87,6 +95,8 @@ function validateSessionStart(input) {
|
|
|
87
95
|
validateRequiredField('intent', input.intent);
|
|
88
96
|
validateParticipants(input.participants);
|
|
89
97
|
validateTtlMs(input.ttlMs);
|
|
98
|
+
if (input.maxSuspendMs !== undefined)
|
|
99
|
+
validateMaxSuspendMs(input.maxSuspendMs);
|
|
90
100
|
validateRequiredField('modeVersion', input.modeVersion);
|
|
91
101
|
validateRequiredField('configurationVersion', input.configurationVersion);
|
|
92
102
|
}
|
package/dist/watchers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthConfig } from './auth';
|
|
2
|
-
import type
|
|
2
|
+
import { type MacpClient } from './client';
|
|
3
3
|
import type { Envelope, PolicyDescriptor, RegistryChanged, RootsChanged, SessionLifecycleEvent, SessionLifecycleEventType } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* The lifecycle event types after which a session emits no further events.
|
|
@@ -43,6 +43,16 @@ export declare class RootsWatcher {
|
|
|
43
43
|
watch(handler: (change: RootsChanged) => void | Promise<void>): Promise<void>;
|
|
44
44
|
nextChange(): Promise<RootsChanged>;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Watches the ambient signal plane. **Requires auth** as of runtime 0.5.0: if
|
|
48
|
+
* no credential is configured on the watcher or the client, iterating
|
|
49
|
+
* {@link SignalWatcher.signals} throws immediately (via `client.watchSignals`).
|
|
50
|
+
*
|
|
51
|
+
* Consumer lag terminates the stream with a coded `MacpTransportError`
|
|
52
|
+
* (`code === 'RESOURCE_EXHAUSTED'`); the correct response is to reconnect.
|
|
53
|
+
* A missing/invalid credential surfaces as `code === 'UNAUTHENTICATED'` — fix
|
|
54
|
+
* auth, do not reconnect.
|
|
55
|
+
*/
|
|
46
56
|
export declare class SignalWatcher {
|
|
47
57
|
private readonly client;
|
|
48
58
|
private readonly auth?;
|
|
@@ -67,26 +77,20 @@ export declare class PolicyWatcher {
|
|
|
67
77
|
watch(handler: (change: PolicyChange) => void | Promise<void>): Promise<void>;
|
|
68
78
|
nextChange(): Promise<PolicyChange>;
|
|
69
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Watches session lifecycle events (`WatchSessions`). Consumer lag terminates
|
|
82
|
+
* the stream with a coded `MacpTransportError` (`code === 'RESOURCE_EXHAUSTED'`);
|
|
83
|
+
* the correct response is to reconnect and re-sync current state via
|
|
84
|
+
* {@link MacpClient.listSessions}, since events emitted during the gap are lost.
|
|
85
|
+
*/
|
|
70
86
|
export declare class SessionLifecycleWatcher {
|
|
71
87
|
private readonly client;
|
|
72
88
|
private readonly auth?;
|
|
73
|
-
private deprecatedEventsWarned;
|
|
74
|
-
private deprecatedNextEventWarned;
|
|
75
89
|
constructor(client: MacpClient, options?: {
|
|
76
90
|
auth?: AuthConfig;
|
|
77
91
|
});
|
|
78
92
|
changes(signal?: AbortSignal): AsyncGenerator<SessionLifecycleEvent, void, void>;
|
|
79
93
|
watch(handler: (event: SessionLifecycleEvent) => void | Promise<void>): Promise<void>;
|
|
80
94
|
nextChange(): Promise<SessionLifecycleEvent>;
|
|
81
|
-
/**
|
|
82
|
-
* @deprecated Use {@link changes} — renamed for parity with python-sdk's
|
|
83
|
-
* `SessionLifecycleWatcher.changes()` and the uniform `changes()` name
|
|
84
|
-
* used by all other watchers in both SDKs. Will be removed in 0.5.0.
|
|
85
|
-
*/
|
|
86
|
-
events(signal?: AbortSignal): AsyncGenerator<SessionLifecycleEvent, void, void>;
|
|
87
|
-
/**
|
|
88
|
-
* @deprecated Use {@link nextChange}. Will be removed in 0.5.0.
|
|
89
|
-
*/
|
|
90
|
-
nextEvent(): Promise<SessionLifecycleEvent>;
|
|
91
95
|
}
|
|
92
96
|
export {};
|