macp-sdk-typescript 0.16.1 → 0.17.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 +20 -0
- package/README.md +13 -0
- package/dist/policy.d.ts +69 -6
- package/dist/policy.js +209 -4
- package/dist/projections/handoff.d.ts +13 -0
- package/dist/projections/handoff.js +27 -0
- package/dist/projections/quorum.d.ts +32 -5
- package/dist/projections/quorum.js +40 -14
- package/dist/projections/task.d.ts +25 -0
- package/dist/projections/task.js +63 -4
- package/dist/types.d.ts +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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
|
+
## [0.17.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.16.1...v0.17.0) (2026-10-18)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* **policy:** an untyped (JS/JSON) caller passing an off-domain rule value used to get a descriptor that a pre-0.9 runtime registered. That call now throws MacpSessionError at build time.
|
|
13
|
+
* **task:** a TaskReject sent before any TaskAccept no longer sets TaskRecord.status to 'rejected'; the task stays 'requested' and remains in activeTasks(). TaskRejectRecord gains a required whileAssigned field.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **handoff:** expose declined targets and the rule 3a re-offer bar ([#181](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/181)) ([dc61e08](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/dc61e08a552a729c6df9aa0dc19c31abfacd3189))
|
|
18
|
+
* **policy:** refuse every off-domain rule value the 0.9 runtime enforces ([#183](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/183)) ([9afd316](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/9afd3167117dd99baadabb18f3199bc0140ae0ad))
|
|
19
|
+
* **policy:** x- extension and _/$ annotation rules keys; policyRuleExtensions() ([#184](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/184)) ([84b3a14](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/84b3a14c2a414ebb7ff1042f5999ca17d961eab4))
|
|
20
|
+
* **quorum:** effectiveQuorumThreshold and a policy-override bar for the readiness helpers ([#182](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/182)) ([63a00c4](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/63a00c459f676b93cba66c1a53924074c297c30f))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **task:** model a pre-assignment TaskReject as a personal decline (rule 2a) ([#179](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/179)) ([2d56c32](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/2d56c3290c9a77557fdbaa11ab8003498178b514))
|
|
26
|
+
|
|
7
27
|
## [0.16.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.16.0...v0.16.1) (2026-10-18)
|
|
8
28
|
|
|
9
29
|
|
package/README.md
CHANGED
|
@@ -467,6 +467,19 @@ npm run test:integration
|
|
|
467
467
|
docker rm -f macp-runtime-test
|
|
468
468
|
```
|
|
469
469
|
|
|
470
|
+
## Runtime Compatibility
|
|
471
|
+
|
|
472
|
+
This SDK requires **macp-runtime ≥ 0.9.0**. CI gates every PR on the
|
|
473
|
+
integration suite against `macp-runtime:0.9.0`, the same pin as
|
|
474
|
+
`macp-sdk-python`, and runs a non-blocking canary against `:latest`. Against
|
|
475
|
+
an older runtime, the 0.9.0 behaviors the SDK models will not match. Examples:
|
|
476
|
+
|
|
477
|
+
- the quorum policy-threshold override (`effectiveQuorumThreshold`);
|
|
478
|
+
- task rule 2a personal declines;
|
|
479
|
+
- the handoff rule 3a re-offer bar;
|
|
480
|
+
- policy rule-domain admission and `ruleExtensions`;
|
|
481
|
+
- `macp-error-code` statuses.
|
|
482
|
+
|
|
470
483
|
## Runtime Boundary
|
|
471
484
|
|
|
472
485
|
This SDK is a **client** for the MACP Rust runtime. The runtime handles session state, message ordering, deduplication, TTL enforcement, and mode-specific validation. The SDK provides typed helpers for building and sending envelopes, and local projections for tracking state client-side.
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PolicyDescriptor } from './types';
|
|
1
|
+
import type { InitializeResult, PolicyDescriptor } from './types';
|
|
2
2
|
export interface CommitmentRules {
|
|
3
3
|
authority?: 'initiator_only' | 'any_participant' | 'designated_role';
|
|
4
4
|
/**
|
|
@@ -128,7 +128,40 @@ export interface HandoffPolicyRulesInput {
|
|
|
128
128
|
acceptance?: HandoffAcceptanceRules;
|
|
129
129
|
commitment?: CommitmentRules;
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
/** `/properties/commitment/properties/authority` (all five modes). */
|
|
132
|
+
export declare const COMMITMENT_AUTHORITIES: ReadonlySet<NonNullable<CommitmentRules['authority']>>;
|
|
133
|
+
/** quorum-rules `/properties/abstention/properties/interpretation`. */
|
|
134
|
+
export declare const ABSTENTION_INTERPRETATIONS: ReadonlySet<NonNullable<AbstentionRules['interpretation']>>;
|
|
135
|
+
/** proposal-rules `/properties/acceptance/properties/criterion`. */
|
|
136
|
+
export declare const PROPOSAL_ACCEPTANCE_CRITERIA: ReadonlySet<NonNullable<ProposalAcceptanceRules['criterion']>>;
|
|
137
|
+
/** decision-rules `/properties/objection_handling/properties/critical_objection_action`. */
|
|
138
|
+
export declare const CRITICAL_OBJECTION_ACTIONS: ReadonlySet<NonNullable<ObjectionHandlingRules['criticalObjectionAction']>>;
|
|
139
|
+
/**
|
|
140
|
+
* Extra top-level `rules` keys every builder accepts (RFC-MACP-0012 §4),
|
|
141
|
+
* emitted after the standard sections in insertion order. Omitted or empty,
|
|
142
|
+
* the `rules` bytes are exactly what they were before these existed.
|
|
143
|
+
*/
|
|
144
|
+
export interface PolicyRuleExtras {
|
|
145
|
+
/**
|
|
146
|
+
* `x-` vendor-extension sections, e.g. `{ 'x-com.acme.audit': { level: 2 } }`.
|
|
147
|
+
* Keys must match `^x-[a-z0-9]+(\.[a-z0-9-]+)+$` and values must be objects.
|
|
148
|
+
* The runtime admits a key only if an embedder registered it (see
|
|
149
|
+
* `policyRuleExtensions()`; the stock server registers none), so the builder
|
|
150
|
+
* checks the key's shape, not whether this runtime will admit it.
|
|
151
|
+
*/
|
|
152
|
+
extensions?: Record<string, Record<string, unknown>>;
|
|
153
|
+
/** `_`- or `$`-prefixed annotation keys, any JSON value; the runtime ignores them. */
|
|
154
|
+
annotations?: Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The `x-` rule-extension keys a runtime advertises
|
|
158
|
+
* (`capabilities.policyRegistry.ruleExtensions`), or `[]` when it advertises
|
|
159
|
+
* none, including runtimes older than 0.9.0. Advisory: a descriptor carrying
|
|
160
|
+
* an unlisted key is refused at RegisterPolicy, but admission is what
|
|
161
|
+
* decides. Accepts the `initialize()` result or its `capabilities`.
|
|
162
|
+
*/
|
|
163
|
+
export declare function policyRuleExtensions(source: InitializeResult | InitializeResult['capabilities'] | null | undefined): string[];
|
|
164
|
+
export interface DecisionPolicyOptions extends PolicyRuleExtras {
|
|
132
165
|
/**
|
|
133
166
|
* RFC-MACP-0012 rule schema version the runtime evaluates this policy
|
|
134
167
|
* under (validated, never re-validated, at admission -- RFC-MACP-0012 §8
|
|
@@ -157,7 +190,37 @@ export interface DecisionPolicyOptions {
|
|
|
157
190
|
schemaVersion?: 1 | 2 | 3;
|
|
158
191
|
}
|
|
159
192
|
export declare function buildDecisionPolicy(policyId: string, description: string, rules: DecisionPolicyRulesInput, options?: DecisionPolicyOptions): PolicyDescriptor;
|
|
160
|
-
export declare function buildQuorumPolicy(policyId: string, description: string, rules: QuorumPolicyRulesInput): PolicyDescriptor;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
export declare function buildQuorumPolicy(policyId: string, description: string, rules: QuorumPolicyRulesInput, options?: PolicyRuleExtras): PolicyDescriptor;
|
|
194
|
+
/**
|
|
195
|
+
* The approval bar a Quorum session actually needs, or `null` when it is
|
|
196
|
+
* unsatisfiable.
|
|
197
|
+
*
|
|
198
|
+
* Since runtime 0.9.0 (S4, RFC-MACP-0011 §5 rule 6) a bound policy's
|
|
199
|
+
* `threshold` replaces the `ApprovalRequest.requiredApprovals` payload value
|
|
200
|
+
* *as given*, even above the participant count, in which case the threshold
|
|
201
|
+
* is unreachable and a negative Commitment is eligible at once. Pass the
|
|
202
|
+
* result as `{ effectiveThreshold }` to the `QuorumProjection` readiness
|
|
203
|
+
* helpers (`null` included — they treat it as unreachable).
|
|
204
|
+
*
|
|
205
|
+
* A port of the runtime's `QuorumThreshold::effective` and
|
|
206
|
+
* `QuorumMode::effective_threshold`, with the same results as
|
|
207
|
+
* macp-sdk-python's `effective_quorum_threshold`:
|
|
208
|
+
*
|
|
209
|
+
* - no policy, no `threshold`, `value <= 0`/NaN, a non-numeric or boolean
|
|
210
|
+
* `value`, a non-string `type`, or unparsable rules (the runtime's
|
|
211
|
+
* `unwrap_or_default` path): `requiredApprovals`, unchanged;
|
|
212
|
+
* - `n_of_m` (the default `type`, and the runtime's `count` alias):
|
|
213
|
+
* `max(1, ceil(value))`;
|
|
214
|
+
* - integer `percentage`: `max(1, ceil(value * n / 100))` in exact integer
|
|
215
|
+
* arithmetic (RFC-MACP-0012 §4.2 forbids float division); fractional
|
|
216
|
+
* `percentage`: `max(1, ceil(value * n / 100))`;
|
|
217
|
+
* - `percentage` with no participants, or an unknown `type`: `null`;
|
|
218
|
+
* - every result saturates at `2^32 - 1` (an infinite value included).
|
|
219
|
+
*
|
|
220
|
+
* `rules` may be a `PolicyDescriptor`, its JSON `rules` string/bytes, or the
|
|
221
|
+
* decoded object.
|
|
222
|
+
*/
|
|
223
|
+
export declare function effectiveQuorumThreshold(rules: PolicyDescriptor | string | Uint8Array | Record<string, unknown> | null | undefined, participantCount: number, requiredApprovals: number): number | null;
|
|
224
|
+
export declare function buildProposalPolicy(policyId: string, description: string, rules: ProposalPolicyRulesInput, options?: PolicyRuleExtras): PolicyDescriptor;
|
|
225
|
+
export declare function buildTaskPolicy(policyId: string, description: string, rules: TaskPolicyRulesInput, options?: PolicyRuleExtras): PolicyDescriptor;
|
|
226
|
+
export declare function buildHandoffPolicy(policyId: string, description: string, rules: HandoffPolicyRulesInput, options?: PolicyRuleExtras): PolicyDescriptor;
|
package/dist/policy.js
CHANGED
|
@@ -1,13 +1,111 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CRITICAL_OBJECTION_ACTIONS = exports.PROPOSAL_ACCEPTANCE_CRITERIA = exports.ABSTENTION_INTERPRETATIONS = exports.COMMITMENT_AUTHORITIES = void 0;
|
|
4
|
+
exports.policyRuleExtensions = policyRuleExtensions;
|
|
3
5
|
exports.buildDecisionPolicy = buildDecisionPolicy;
|
|
4
6
|
exports.buildQuorumPolicy = buildQuorumPolicy;
|
|
7
|
+
exports.effectiveQuorumThreshold = effectiveQuorumThreshold;
|
|
5
8
|
exports.buildProposalPolicy = buildProposalPolicy;
|
|
6
9
|
exports.buildTaskPolicy = buildTaskPolicy;
|
|
7
10
|
exports.buildHandoffPolicy = buildHandoffPolicy;
|
|
8
11
|
const errors_1 = require("./errors");
|
|
12
|
+
// ── Rule value domains ──────────────────────────────────────────────
|
|
13
|
+
// Runtime 0.9.0 validates every rule object against the canonical
|
|
14
|
+
// spec `schemas/json/policy/*-rules.schema.json` and refuses an off-domain
|
|
15
|
+
// value at RegisterPolicy with INVALID_POLICY_DEFINITION; the builders refuse
|
|
16
|
+
// it client-side first. Same sets as macp-sdk-python's `macp_sdk.policy`;
|
|
17
|
+
// tests/unit/policy-schema-parity.test.ts pins them against the schemas.
|
|
18
|
+
// Typed against the rule-input unions so a widened union without a matching
|
|
19
|
+
// set entry is a tsc error.
|
|
20
|
+
/** `/properties/commitment/properties/authority` (all five modes). */
|
|
21
|
+
exports.COMMITMENT_AUTHORITIES = new Set([
|
|
22
|
+
'initiator_only',
|
|
23
|
+
'any_participant',
|
|
24
|
+
'designated_role',
|
|
25
|
+
]);
|
|
26
|
+
/** quorum-rules `/properties/abstention/properties/interpretation`. */
|
|
27
|
+
exports.ABSTENTION_INTERPRETATIONS = new Set([
|
|
28
|
+
'neutral',
|
|
29
|
+
'implicit_reject',
|
|
30
|
+
'ignored',
|
|
31
|
+
]);
|
|
32
|
+
/** proposal-rules `/properties/acceptance/properties/criterion`. */
|
|
33
|
+
exports.PROPOSAL_ACCEPTANCE_CRITERIA = new Set([
|
|
34
|
+
'all_parties',
|
|
35
|
+
'counterparty',
|
|
36
|
+
'initiator',
|
|
37
|
+
]);
|
|
38
|
+
/** decision-rules `/properties/objection_handling/properties/critical_objection_action`. */
|
|
39
|
+
exports.CRITICAL_OBJECTION_ACTIONS = new Set(['deny', 'finalize_decline', 'hold']);
|
|
40
|
+
// decision-rules `/properties/voting/properties/quorum/properties/type`.
|
|
41
|
+
const VOTING_QUORUM_TYPES = new Set(['count', 'percentage']);
|
|
42
|
+
// RFC-MACP-0012 §4 key classes legal at the rules top level besides the
|
|
43
|
+
// standard sections: `x-` vendor extensions (object-valued) and `_`/`$`
|
|
44
|
+
// annotations (any value). Same patterns as the schemas' patternProperties.
|
|
45
|
+
const EXTENSION_KEY_RE = /^x-[a-z0-9]+(\.[a-z0-9-]+)+$/;
|
|
46
|
+
const ANNOTATION_KEY_RE = /^[_$]/;
|
|
47
|
+
// An own enumerable key even for `__proto__` (a legal annotation name),
|
|
48
|
+
// which plain assignment would turn into a prototype swap that
|
|
49
|
+
// JSON.stringify then drops.
|
|
50
|
+
function defineKey(rules, key, value) {
|
|
51
|
+
Object.defineProperty(rules, key, { value, enumerable: true, writable: true, configurable: true });
|
|
52
|
+
}
|
|
53
|
+
function addExtraKeys(rules, extras) {
|
|
54
|
+
for (const [key, value] of Object.entries(extras?.extensions ?? {})) {
|
|
55
|
+
if (Object.hasOwn(rules, key)) {
|
|
56
|
+
throw new errors_1.MacpSessionError(`extension key '${key}' collides with a standard rule section`);
|
|
57
|
+
}
|
|
58
|
+
if (!EXTENSION_KEY_RE.test(key)) {
|
|
59
|
+
throw new errors_1.MacpSessionError(`extension key '${key}' must match ${EXTENSION_KEY_RE.source} (e.g. 'x-com.acme.audit')`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
62
|
+
throw new errors_1.MacpSessionError(`extension '${key}' must be an object, got ${JSON.stringify(value)}`);
|
|
63
|
+
}
|
|
64
|
+
defineKey(rules, key, { ...value });
|
|
65
|
+
}
|
|
66
|
+
for (const [name, note] of Object.entries(extras?.annotations ?? {})) {
|
|
67
|
+
if (Object.hasOwn(rules, name)) {
|
|
68
|
+
throw new errors_1.MacpSessionError(`annotation key '${name}' collides with a standard rule section`);
|
|
69
|
+
}
|
|
70
|
+
if (!ANNOTATION_KEY_RE.test(name)) {
|
|
71
|
+
throw new errors_1.MacpSessionError(`annotation key '${name}' must start with '_' or '$'`);
|
|
72
|
+
}
|
|
73
|
+
defineKey(rules, name, note);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `x-` rule-extension keys a runtime advertises
|
|
78
|
+
* (`capabilities.policyRegistry.ruleExtensions`), or `[]` when it advertises
|
|
79
|
+
* none, including runtimes older than 0.9.0. Advisory: a descriptor carrying
|
|
80
|
+
* an unlisted key is refused at RegisterPolicy, but admission is what
|
|
81
|
+
* decides. Accepts the `initialize()` result or its `capabilities`.
|
|
82
|
+
*/
|
|
83
|
+
function policyRuleExtensions(source) {
|
|
84
|
+
const capabilities = source && 'selectedProtocolVersion' in source
|
|
85
|
+
? source.capabilities
|
|
86
|
+
: (source ?? undefined);
|
|
87
|
+
const registry = capabilities?.policyRegistry;
|
|
88
|
+
const keys = registry?.ruleExtensions;
|
|
89
|
+
return Array.isArray(keys) ? keys.filter((k) => typeof k === 'string') : [];
|
|
90
|
+
}
|
|
91
|
+
function checkEnum(fieldName, value, allowed) {
|
|
92
|
+
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
93
|
+
throw new errors_1.MacpSessionError(`${fieldName} must be one of ${[...allowed].sort().join(', ')}, got ${JSON.stringify(value)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Number.isInteger rejects booleans, NaN and ±Infinity, so unlike Python's
|
|
97
|
+
// `_check_min_int` no separate bool guard is needed.
|
|
98
|
+
function checkMinInt(fieldName, value, minimum) {
|
|
99
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
100
|
+
throw new errors_1.MacpSessionError(`${fieldName} must be an integer >= ${minimum}, got ${JSON.stringify(value)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function isFiniteNumber(value) {
|
|
104
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
105
|
+
}
|
|
9
106
|
// ── Builder helpers ─────────────────────────────────────────────────
|
|
10
107
|
function serializeCommitment(commitment) {
|
|
108
|
+
checkEnum('commitment.authority', commitment?.authority ?? 'initiator_only', exports.COMMITMENT_AUTHORITIES);
|
|
11
109
|
// `authority: 'designated_role'` with an empty (or unset) `designatedRoles`
|
|
12
110
|
// names no one, so no sender could ever satisfy it. All five rule schemas
|
|
13
111
|
// reject this combination at admission (spec PR #121) via a root-level
|
|
@@ -117,6 +215,21 @@ function buildDecisionPolicy(policyId, description, rules, options) {
|
|
|
117
215
|
}
|
|
118
216
|
}
|
|
119
217
|
}
|
|
218
|
+
// Value domains the runtime enforces since 0.9.0 (decision-rules.schema.json).
|
|
219
|
+
// Same order as macp-sdk-python's build_decision_policy.
|
|
220
|
+
const quorum = rules.voting?.quorum;
|
|
221
|
+
if (quorum !== undefined) {
|
|
222
|
+
checkEnum('voting.quorum_type', quorum.type, VOTING_QUORUM_TYPES);
|
|
223
|
+
if (!isFiniteNumber(quorum.value) || quorum.value < 0) {
|
|
224
|
+
throw new errors_1.MacpSessionError(`voting.quorum_value must be a finite number >= 0, got ${JSON.stringify(quorum.value)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
checkMinInt('objection_handling.veto_threshold', rules.objectionHandling?.vetoThreshold ?? 1, 1);
|
|
228
|
+
checkEnum('objection_handling.critical_objection_action', rules.objectionHandling?.criticalObjectionAction ?? 'deny', exports.CRITICAL_OBJECTION_ACTIONS);
|
|
229
|
+
const minimumConfidence = rules.evaluation?.minimumConfidence ?? 0;
|
|
230
|
+
if (!isFiniteNumber(minimumConfidence) || minimumConfidence < 0 || minimumConfidence > 1) {
|
|
231
|
+
throw new errors_1.MacpSessionError(`evaluation.minimum_confidence must be in [0.0, 1.0], got ${JSON.stringify(minimumConfidence)}`);
|
|
232
|
+
}
|
|
120
233
|
// Decision-only: extend the shared commitment rules with the two keys
|
|
121
234
|
// decision-rules.schema.json's commitment object allows and the other four
|
|
122
235
|
// modes' commitment schemas do not -- `require_vote_quorum` and the
|
|
@@ -144,6 +257,7 @@ function buildDecisionPolicy(policyId, description, rules, options) {
|
|
|
144
257
|
},
|
|
145
258
|
commitment: commitmentSection,
|
|
146
259
|
};
|
|
260
|
+
addExtraKeys(rulesJson, options);
|
|
147
261
|
return {
|
|
148
262
|
policyId,
|
|
149
263
|
mode: 'macp.mode.decision.v1',
|
|
@@ -152,7 +266,7 @@ function buildDecisionPolicy(policyId, description, rules, options) {
|
|
|
152
266
|
schemaVersion,
|
|
153
267
|
};
|
|
154
268
|
}
|
|
155
|
-
function buildQuorumPolicy(policyId, description, rules) {
|
|
269
|
+
function buildQuorumPolicy(policyId, description, rules, options) {
|
|
156
270
|
const threshold = rules.threshold;
|
|
157
271
|
if (threshold) {
|
|
158
272
|
// Match the canonical quorum-rules schema's constraints before the runtime
|
|
@@ -186,6 +300,7 @@ function buildQuorumPolicy(policyId, description, rules) {
|
|
|
186
300
|
'defined semantics and must not be used.');
|
|
187
301
|
}
|
|
188
302
|
}
|
|
303
|
+
checkEnum('abstention.interpretation', rules.abstention?.interpretation ?? 'neutral', exports.ABSTENTION_INTERPRETATIONS);
|
|
189
304
|
const rulesJson = {
|
|
190
305
|
threshold: {
|
|
191
306
|
type: rules.threshold?.type ?? 'n_of_m',
|
|
@@ -197,6 +312,7 @@ function buildQuorumPolicy(policyId, description, rules) {
|
|
|
197
312
|
},
|
|
198
313
|
commitment: serializeCommitment(rules.commitment),
|
|
199
314
|
};
|
|
315
|
+
addExtraKeys(rulesJson, options);
|
|
200
316
|
return {
|
|
201
317
|
policyId,
|
|
202
318
|
mode: 'macp.mode.quorum.v1',
|
|
@@ -205,7 +321,92 @@ function buildQuorumPolicy(policyId, description, rules) {
|
|
|
205
321
|
schemaVersion: 1,
|
|
206
322
|
};
|
|
207
323
|
}
|
|
208
|
-
|
|
324
|
+
const U32_MAX = 2 ** 32 - 1;
|
|
325
|
+
/**
|
|
326
|
+
* The approval bar a Quorum session actually needs, or `null` when it is
|
|
327
|
+
* unsatisfiable.
|
|
328
|
+
*
|
|
329
|
+
* Since runtime 0.9.0 (S4, RFC-MACP-0011 §5 rule 6) a bound policy's
|
|
330
|
+
* `threshold` replaces the `ApprovalRequest.requiredApprovals` payload value
|
|
331
|
+
* *as given*, even above the participant count, in which case the threshold
|
|
332
|
+
* is unreachable and a negative Commitment is eligible at once. Pass the
|
|
333
|
+
* result as `{ effectiveThreshold }` to the `QuorumProjection` readiness
|
|
334
|
+
* helpers (`null` included — they treat it as unreachable).
|
|
335
|
+
*
|
|
336
|
+
* A port of the runtime's `QuorumThreshold::effective` and
|
|
337
|
+
* `QuorumMode::effective_threshold`, with the same results as
|
|
338
|
+
* macp-sdk-python's `effective_quorum_threshold`:
|
|
339
|
+
*
|
|
340
|
+
* - no policy, no `threshold`, `value <= 0`/NaN, a non-numeric or boolean
|
|
341
|
+
* `value`, a non-string `type`, or unparsable rules (the runtime's
|
|
342
|
+
* `unwrap_or_default` path): `requiredApprovals`, unchanged;
|
|
343
|
+
* - `n_of_m` (the default `type`, and the runtime's `count` alias):
|
|
344
|
+
* `max(1, ceil(value))`;
|
|
345
|
+
* - integer `percentage`: `max(1, ceil(value * n / 100))` in exact integer
|
|
346
|
+
* arithmetic (RFC-MACP-0012 §4.2 forbids float division); fractional
|
|
347
|
+
* `percentage`: `max(1, ceil(value * n / 100))`;
|
|
348
|
+
* - `percentage` with no participants, or an unknown `type`: `null`;
|
|
349
|
+
* - every result saturates at `2^32 - 1` (an infinite value included).
|
|
350
|
+
*
|
|
351
|
+
* `rules` may be a `PolicyDescriptor`, its JSON `rules` string/bytes, or the
|
|
352
|
+
* decoded object.
|
|
353
|
+
*/
|
|
354
|
+
function effectiveQuorumThreshold(rules, participantCount, requiredApprovals) {
|
|
355
|
+
let parsed = rules;
|
|
356
|
+
if (isPolicyDescriptor(parsed))
|
|
357
|
+
parsed = parsed.rules;
|
|
358
|
+
if (typeof parsed === 'string' || parsed instanceof Uint8Array) {
|
|
359
|
+
const text = typeof parsed === 'string' ? parsed : Buffer.from(parsed).toString('utf8');
|
|
360
|
+
if (!text)
|
|
361
|
+
return requiredApprovals;
|
|
362
|
+
try {
|
|
363
|
+
parsed = JSON.parse(text);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return requiredApprovals;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!isPlainRecord(parsed))
|
|
370
|
+
return requiredApprovals;
|
|
371
|
+
const threshold = parsed.threshold;
|
|
372
|
+
if (!isPlainRecord(threshold))
|
|
373
|
+
return requiredApprovals;
|
|
374
|
+
const type = threshold.type === undefined ? 'n_of_m' : threshold.type;
|
|
375
|
+
const value = threshold.value === undefined ? 0 : threshold.value;
|
|
376
|
+
if (typeof type !== 'string' || typeof value !== 'number')
|
|
377
|
+
return requiredApprovals;
|
|
378
|
+
// NaN, 0 and negatives are all inert (the runtime's partial_cmp guard).
|
|
379
|
+
if (!(value > 0))
|
|
380
|
+
return requiredApprovals;
|
|
381
|
+
let raw;
|
|
382
|
+
if (type === 'percentage') {
|
|
383
|
+
if (participantCount === 0)
|
|
384
|
+
return null;
|
|
385
|
+
if (Number.isInteger(value)) {
|
|
386
|
+
const required = (BigInt(value) * BigInt(participantCount) + 99n) / 100n;
|
|
387
|
+
return Number(required > BigInt(U32_MAX) ? BigInt(U32_MAX) : required < 1n ? 1n : required);
|
|
388
|
+
}
|
|
389
|
+
raw = (value * participantCount) / 100;
|
|
390
|
+
}
|
|
391
|
+
else if (type === 'n_of_m' || type === 'count') {
|
|
392
|
+
raw = value;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
if (raw === Infinity)
|
|
398
|
+
return U32_MAX;
|
|
399
|
+
return Math.min(Math.max(1, Math.ceil(raw)), U32_MAX);
|
|
400
|
+
}
|
|
401
|
+
function isPlainRecord(value) {
|
|
402
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Uint8Array);
|
|
403
|
+
}
|
|
404
|
+
function isPolicyDescriptor(value) {
|
|
405
|
+
return isPlainRecord(value) && typeof value.policyId === 'string' && typeof value.rules === 'string';
|
|
406
|
+
}
|
|
407
|
+
function buildProposalPolicy(policyId, description, rules, options) {
|
|
408
|
+
checkEnum('acceptance.criterion', rules.acceptance?.criterion ?? 'all_parties', exports.PROPOSAL_ACCEPTANCE_CRITERIA);
|
|
409
|
+
checkMinInt('counter_proposal.max_rounds', rules.counterProposal?.maxRounds ?? 0, 0);
|
|
209
410
|
const rulesJson = {
|
|
210
411
|
acceptance: {
|
|
211
412
|
criterion: rules.acceptance?.criterion ?? 'all_parties',
|
|
@@ -218,6 +419,7 @@ function buildProposalPolicy(policyId, description, rules) {
|
|
|
218
419
|
},
|
|
219
420
|
commitment: serializeCommitment(rules.commitment),
|
|
220
421
|
};
|
|
422
|
+
addExtraKeys(rulesJson, options);
|
|
221
423
|
return {
|
|
222
424
|
policyId,
|
|
223
425
|
mode: 'macp.mode.proposal.v1',
|
|
@@ -226,7 +428,7 @@ function buildProposalPolicy(policyId, description, rules) {
|
|
|
226
428
|
schemaVersion: 1,
|
|
227
429
|
};
|
|
228
430
|
}
|
|
229
|
-
function buildTaskPolicy(policyId, description, rules) {
|
|
431
|
+
function buildTaskPolicy(policyId, description, rules, options) {
|
|
230
432
|
const rulesJson = {
|
|
231
433
|
assignment: {
|
|
232
434
|
allow_reassignment_on_reject: rules.assignment?.allowReassignmentOnReject ?? false,
|
|
@@ -236,6 +438,7 @@ function buildTaskPolicy(policyId, description, rules) {
|
|
|
236
438
|
},
|
|
237
439
|
commitment: serializeCommitment(rules.commitment),
|
|
238
440
|
};
|
|
441
|
+
addExtraKeys(rulesJson, options);
|
|
239
442
|
return {
|
|
240
443
|
policyId,
|
|
241
444
|
mode: 'macp.mode.task.v1',
|
|
@@ -244,13 +447,15 @@ function buildTaskPolicy(policyId, description, rules) {
|
|
|
244
447
|
schemaVersion: 1,
|
|
245
448
|
};
|
|
246
449
|
}
|
|
247
|
-
function buildHandoffPolicy(policyId, description, rules) {
|
|
450
|
+
function buildHandoffPolicy(policyId, description, rules, options) {
|
|
451
|
+
checkMinInt('acceptance.implicit_accept_timeout_ms', rules.acceptance?.implicitAcceptTimeoutMs ?? 0, 0);
|
|
248
452
|
const rulesJson = {
|
|
249
453
|
acceptance: {
|
|
250
454
|
implicit_accept_timeout_ms: rules.acceptance?.implicitAcceptTimeoutMs ?? 0,
|
|
251
455
|
},
|
|
252
456
|
commitment: serializeCommitment(rules.commitment),
|
|
253
457
|
};
|
|
458
|
+
addExtraKeys(rulesJson, options);
|
|
254
459
|
return {
|
|
255
460
|
policyId,
|
|
256
461
|
mode: 'macp.mode.handoff.v1',
|
|
@@ -36,4 +36,17 @@ export declare class HandoffProjection extends BaseProjection {
|
|
|
36
36
|
pendingHandoffs(): HandoffRecord[];
|
|
37
37
|
hasAcceptedOffer(handoffId?: string): boolean;
|
|
38
38
|
activeOffer(): HandoffRecord | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Targets that declined an offer this session, in offer order, de-duplicated.
|
|
41
|
+
* Keyed on the offer's `targetParticipant` (as the runtime is), not on the
|
|
42
|
+
* payload-supplied `declinedBy`, which may be empty.
|
|
43
|
+
*/
|
|
44
|
+
declinedTargets(): string[];
|
|
45
|
+
/**
|
|
46
|
+
* True if a new `HandoffOffer` to `target` would be admissible: `target` has
|
|
47
|
+
* not declined earlier in the session (rule 3a), no offer is outstanding,
|
|
48
|
+
* and no offer has been accepted. Does not check that `target` is a declared
|
|
49
|
+
* participant other than the sender.
|
|
50
|
+
*/
|
|
51
|
+
canOfferTo(target: string): boolean;
|
|
39
52
|
}
|
|
@@ -162,5 +162,32 @@ class HandoffProjection extends base_1.BaseProjection {
|
|
|
162
162
|
}
|
|
163
163
|
return undefined;
|
|
164
164
|
}
|
|
165
|
+
// Advisory (RFC-MACP-0010 §5 rule 3a, runtime ≥ 0.9.0): modelled from what
|
|
166
|
+
// this projection has seen. A projection that joined mid-session may miss
|
|
167
|
+
// earlier offers, and sessions started on a runtime < 0.9.0 do not enforce
|
|
168
|
+
// rule 3a. Nothing refuses client-side — `HandoffSession.offer()` still
|
|
169
|
+
// sends, and the runtime's `INVALID_ENVELOPE` NACK is authoritative. Same
|
|
170
|
+
// names and semantics as macp-sdk-python's `HandoffProjection`.
|
|
171
|
+
/**
|
|
172
|
+
* Targets that declined an offer this session, in offer order, de-duplicated.
|
|
173
|
+
* Keyed on the offer's `targetParticipant` (as the runtime is), not on the
|
|
174
|
+
* payload-supplied `declinedBy`, which may be empty.
|
|
175
|
+
*/
|
|
176
|
+
declinedTargets() {
|
|
177
|
+
return [
|
|
178
|
+
...new Set([...this.handoffs.values()].filter((h) => h.status === 'declined').map((h) => h.targetParticipant)),
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* True if a new `HandoffOffer` to `target` would be admissible: `target` has
|
|
183
|
+
* not declined earlier in the session (rule 3a), no offer is outstanding,
|
|
184
|
+
* and no offer has been accepted. Does not check that `target` is a declared
|
|
185
|
+
* participant other than the sender.
|
|
186
|
+
*/
|
|
187
|
+
canOfferTo(target) {
|
|
188
|
+
if (this.declinedTargets().includes(target))
|
|
189
|
+
return false;
|
|
190
|
+
return ![...this.handoffs.values()].some((h) => h.status === 'offered' || h.status === 'context_sent' || h.status === 'accepted');
|
|
191
|
+
}
|
|
165
192
|
}
|
|
166
193
|
exports.HandoffProjection = HandoffProjection;
|
|
@@ -14,6 +14,15 @@ export interface BallotRecord {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
sender: string;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Optional policy override for the `QuorumProjection` readiness helpers.
|
|
19
|
+
* `effectiveThreshold` is `effectiveQuorumThreshold()`'s result: a number
|
|
20
|
+
* replaces the payload's `requiredApprovals`, `null` means unsatisfiable
|
|
21
|
+
* (treated as an infinite bar), and omitting it keeps the payload value.
|
|
22
|
+
*/
|
|
23
|
+
export interface QuorumThresholdOptions {
|
|
24
|
+
effectiveThreshold?: number | null;
|
|
25
|
+
}
|
|
17
26
|
export declare class QuorumProjection extends BaseProjection {
|
|
18
27
|
protected readonly mode = "macp.mode.quorum.v1";
|
|
19
28
|
readonly requests: Map<string, ApprovalRequestRecord>;
|
|
@@ -25,11 +34,29 @@ export declare class QuorumProjection extends BaseProjection {
|
|
|
25
34
|
approvalCount(requestId: string): number;
|
|
26
35
|
rejectionCount(requestId: string): number;
|
|
27
36
|
abstentionCount(requestId: string): number;
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The approval bar for `requestId`: the policy override when one is passed,
|
|
39
|
+
* else the payload's `requiredApprovals`. `null` (an unsatisfiable override,
|
|
40
|
+
* see `effectiveQuorumThreshold`) is an infinite bar. `undefined` for an
|
|
41
|
+
* unknown request.
|
|
42
|
+
*/
|
|
43
|
+
private required;
|
|
44
|
+
/**
|
|
45
|
+
* True once the approval count reaches the bar. Pass `{ effectiveThreshold }`
|
|
46
|
+
* (from `effectiveQuorumThreshold`) when a bound policy's `threshold`
|
|
47
|
+
* overrides `requiredApprovals` (runtime ≥ 0.9.0, RFC-MACP-0011 §5 rule 6).
|
|
48
|
+
*/
|
|
49
|
+
hasQuorum(requestId: string, options?: QuorumThresholdOptions): boolean;
|
|
50
|
+
/** The approval bar, or 0 if the request is unknown. `Infinity` for an unsatisfiable override. */
|
|
51
|
+
threshold(requestId: string, options?: QuorumThresholdOptions): number;
|
|
30
52
|
votedSenders(requestId: string): string[];
|
|
31
|
-
remainingVotesNeeded(requestId: string): number;
|
|
32
|
-
commitmentReady(requestId: string): boolean;
|
|
33
|
-
|
|
53
|
+
remainingVotesNeeded(requestId: string, options?: QuorumThresholdOptions): number;
|
|
54
|
+
commitmentReady(requestId: string, options?: QuorumThresholdOptions): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* True if the remaining possible approvals cannot reach the bar. Under a
|
|
57
|
+
* policy `threshold` override above the participant count (runtime ≥ 0.9.0)
|
|
58
|
+
* this is true before any ballot — pass `{ effectiveThreshold }` to see it.
|
|
59
|
+
*/
|
|
60
|
+
isThresholdUnreachable(requestId: string, totalEligible: number, options?: QuorumThresholdOptions): boolean;
|
|
34
61
|
private countVotes;
|
|
35
62
|
}
|
|
@@ -95,34 +95,60 @@ class QuorumProjection extends base_1.BaseProjection {
|
|
|
95
95
|
abstentionCount(requestId) {
|
|
96
96
|
return this.countVotes(requestId, 'abstain');
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The approval bar for `requestId`: the policy override when one is passed,
|
|
100
|
+
* else the payload's `requiredApprovals`. `null` (an unsatisfiable override,
|
|
101
|
+
* see `effectiveQuorumThreshold`) is an infinite bar. `undefined` for an
|
|
102
|
+
* unknown request.
|
|
103
|
+
*/
|
|
104
|
+
required(requestId, options) {
|
|
99
105
|
const req = this.requests.get(requestId);
|
|
100
106
|
if (!req)
|
|
107
|
+
return undefined;
|
|
108
|
+
const override = options?.effectiveThreshold;
|
|
109
|
+
if (override === undefined)
|
|
110
|
+
return req.requiredApprovals;
|
|
111
|
+
return override === null ? Infinity : override;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* True once the approval count reaches the bar. Pass `{ effectiveThreshold }`
|
|
115
|
+
* (from `effectiveQuorumThreshold`) when a bound policy's `threshold`
|
|
116
|
+
* overrides `requiredApprovals` (runtime ≥ 0.9.0, RFC-MACP-0011 §5 rule 6).
|
|
117
|
+
*/
|
|
118
|
+
hasQuorum(requestId, options) {
|
|
119
|
+
const required = this.required(requestId, options);
|
|
120
|
+
if (required === undefined)
|
|
101
121
|
return false;
|
|
102
|
-
return this.approvalCount(requestId) >=
|
|
122
|
+
return this.approvalCount(requestId) >= required;
|
|
103
123
|
}
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
/** The approval bar, or 0 if the request is unknown. `Infinity` for an unsatisfiable override. */
|
|
125
|
+
threshold(requestId, options) {
|
|
126
|
+
return this.required(requestId, options) ?? 0;
|
|
106
127
|
}
|
|
107
128
|
votedSenders(requestId) {
|
|
108
129
|
const senderMap = this.ballots.get(requestId);
|
|
109
130
|
return senderMap ? [...senderMap.keys()] : [];
|
|
110
131
|
}
|
|
111
|
-
remainingVotesNeeded(requestId) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
132
|
+
remainingVotesNeeded(requestId, options) {
|
|
133
|
+
const required = this.required(requestId, options);
|
|
134
|
+
if (required === undefined)
|
|
114
135
|
return 0;
|
|
115
|
-
return Math.max(0,
|
|
136
|
+
return Math.max(0, required - this.approvalCount(requestId));
|
|
116
137
|
}
|
|
117
|
-
commitmentReady(requestId) {
|
|
118
|
-
return this.hasQuorum(requestId) && this.phase !== 'Committed';
|
|
138
|
+
commitmentReady(requestId, options) {
|
|
139
|
+
return this.hasQuorum(requestId, options) && this.phase !== 'Committed';
|
|
119
140
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
/**
|
|
142
|
+
* True if the remaining possible approvals cannot reach the bar. Under a
|
|
143
|
+
* policy `threshold` override above the participant count (runtime ≥ 0.9.0)
|
|
144
|
+
* this is true before any ballot — pass `{ effectiveThreshold }` to see it.
|
|
145
|
+
*/
|
|
146
|
+
isThresholdUnreachable(requestId, totalEligible, options) {
|
|
147
|
+
const required = this.required(requestId, options);
|
|
148
|
+
if (required === undefined)
|
|
123
149
|
return false;
|
|
124
150
|
const remaining = totalEligible - this.votedSenders(requestId).length;
|
|
125
|
-
return this.approvalCount(requestId) + remaining <
|
|
151
|
+
return this.approvalCount(requestId) + remaining < required;
|
|
126
152
|
}
|
|
127
153
|
countVotes(requestId, vote) {
|
|
128
154
|
const senderMap = this.ballots.get(requestId);
|
|
@@ -38,6 +38,13 @@ export interface TaskRejectRecord {
|
|
|
38
38
|
assignee: string;
|
|
39
39
|
reason?: string;
|
|
40
40
|
sender: string;
|
|
41
|
+
/**
|
|
42
|
+
* `true` when the rejecter held the session's active assignment (RFC-MACP-0009
|
|
43
|
+
* §5 rule 3c: the assignee gave the task back). `false` for a pre-assignment
|
|
44
|
+
* personal decline (rule 2a, runtime ≥ 0.9.0), which leaves the task open to
|
|
45
|
+
* the other participants. Mirrors the runtime's `TaskRejectRecord.while_assigned`.
|
|
46
|
+
*/
|
|
47
|
+
whileAssigned: boolean;
|
|
41
48
|
}
|
|
42
49
|
export declare class TaskProjection extends BaseProjection {
|
|
43
50
|
protected readonly mode = "macp.mode.task.v1";
|
|
@@ -77,4 +84,22 @@ export declare class TaskProjection extends BaseProjection {
|
|
|
77
84
|
activeTasks(): TaskRecord[];
|
|
78
85
|
isAccepted(taskId: string): boolean;
|
|
79
86
|
latestProgress(): number | undefined;
|
|
87
|
+
/** True if `participant` has sent a `TaskReject` (either kind). */
|
|
88
|
+
hasDeclined(participant: string): boolean;
|
|
89
|
+
/** Participants that sent a `TaskReject`, in arrival order, de-duplicated. */
|
|
90
|
+
declinedParticipants(): string[];
|
|
91
|
+
/**
|
|
92
|
+
* True if `participant` may still send `TaskAccept`: the session's assignee
|
|
93
|
+
* slot is free and `participant` has not declined (rule 2a). Does not
|
|
94
|
+
* evaluate the bound policy's `allow_reassignment_on_reject`, which the
|
|
95
|
+
* runtime also requires after an active assignee gave the task back.
|
|
96
|
+
*/
|
|
97
|
+
canAccept(participant: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* True if a negative Commitment is eligible for `taskId`: a terminal report
|
|
100
|
+
* (`TaskComplete`/`TaskFail`) exists, or the named `requestedAssignee`
|
|
101
|
+
* declined before accepting and no one holds the slot (rule 2a), leaving no
|
|
102
|
+
* eligible assignee.
|
|
103
|
+
*/
|
|
104
|
+
negativeCommitEligible(taskId: string): boolean;
|
|
80
105
|
}
|
package/dist/projections/task.js
CHANGED
|
@@ -119,9 +119,17 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
119
119
|
// `p.assignee or envelope.sender` does: TaskComplete/TaskFail below
|
|
120
120
|
// spread the payload's `assignee` as-is with no such fallback, and
|
|
121
121
|
// intra-SDK consistency wins over replicating Python's nuance here.
|
|
122
|
-
|
|
122
|
+
//
|
|
123
|
+
// `whileAssigned` is computed before the slot is freed (runtime
|
|
124
|
+
// `task.rs`, same order). Only the active assignee's reject (rule 3c)
|
|
125
|
+
// marks the task 'rejected'; a pre-assignment reject is a personal
|
|
126
|
+
// decline (RFC-MACP-0009 §5 rule 2a, runtime ≥ 0.9.0) that leaves the
|
|
127
|
+
// task 'requested' for everyone else.
|
|
128
|
+
const slot = this.activeAssignment;
|
|
129
|
+
const whileAssigned = slot?.sender === envelope.sender;
|
|
130
|
+
this.rejections.push({ ...record, sender: envelope.sender, whileAssigned });
|
|
123
131
|
const task = this.tasks.get(record.taskId);
|
|
124
|
-
if (task)
|
|
132
|
+
if (task && whileAssigned)
|
|
125
133
|
task.status = 'rejected';
|
|
126
134
|
// RFC-MACP-0009 §5 rule 3c (`:72`): "When policy sets
|
|
127
135
|
// `allow_reassignment_on_reject: true` and the active assignee sends
|
|
@@ -144,8 +152,8 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
144
152
|
// cannot diverge from the runtime on any real transcript, and it needs
|
|
145
153
|
// no new API surface (threading a `PolicyDefinition` into the
|
|
146
154
|
// constructor would). Issue #70, option 2.
|
|
147
|
-
if (
|
|
148
|
-
const held = this.tasks.get(
|
|
155
|
+
if (slot && whileAssigned) {
|
|
156
|
+
const held = this.tasks.get(slot.taskId);
|
|
149
157
|
if (held)
|
|
150
158
|
held.assignee = undefined;
|
|
151
159
|
this.activeAssignment = undefined;
|
|
@@ -213,5 +221,56 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
213
221
|
latestProgress() {
|
|
214
222
|
return this.updates[this.updates.length - 1]?.progress;
|
|
215
223
|
}
|
|
224
|
+
// Advisory: the helpers below model the runtime's rev-4 rules (RFC-MACP-0009
|
|
225
|
+
// §5 rule 2a, runtime ≥ 0.9.0) from what this projection has seen. A
|
|
226
|
+
// projection that joined mid-session may be partial, and sessions started on
|
|
227
|
+
// a runtime < 0.9.0 keep the earlier rule (any prior reject gates every later
|
|
228
|
+
// accept). None of them refuse anything: `TaskSession.acceptTask()` still
|
|
229
|
+
// sends, and the runtime's NACK stays authoritative. Same names and semantics
|
|
230
|
+
// as macp-sdk-python's `TaskProjection`.
|
|
231
|
+
/** True if `participant` has sent a `TaskReject` (either kind). */
|
|
232
|
+
hasDeclined(participant) {
|
|
233
|
+
return this.rejections.some((r) => rejecterOf(r) === participant);
|
|
234
|
+
}
|
|
235
|
+
/** Participants that sent a `TaskReject`, in arrival order, de-duplicated. */
|
|
236
|
+
declinedParticipants() {
|
|
237
|
+
return [...new Set(this.rejections.map(rejecterOf))];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* True if `participant` may still send `TaskAccept`: the session's assignee
|
|
241
|
+
* slot is free and `participant` has not declined (rule 2a). Does not
|
|
242
|
+
* evaluate the bound policy's `allow_reassignment_on_reject`, which the
|
|
243
|
+
* runtime also requires after an active assignee gave the task back.
|
|
244
|
+
*/
|
|
245
|
+
canAccept(participant) {
|
|
246
|
+
return this.activeAssignment === undefined && !this.hasDeclined(participant);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* True if a negative Commitment is eligible for `taskId`: a terminal report
|
|
250
|
+
* (`TaskComplete`/`TaskFail`) exists, or the named `requestedAssignee`
|
|
251
|
+
* declined before accepting and no one holds the slot (rule 2a), leaving no
|
|
252
|
+
* eligible assignee.
|
|
253
|
+
*/
|
|
254
|
+
negativeCommitEligible(taskId) {
|
|
255
|
+
const task = this.tasks.get(taskId);
|
|
256
|
+
if (!task)
|
|
257
|
+
return false;
|
|
258
|
+
if (this.completions.some((c) => c.taskId === taskId) || this.failures.some((f) => f.taskId === taskId)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const requested = task.requestedAssignee;
|
|
262
|
+
return (!!requested &&
|
|
263
|
+
this.activeAssignment === undefined &&
|
|
264
|
+
this.rejections.some((r) => rejecterOf(r) === requested && !r.whileAssigned));
|
|
265
|
+
}
|
|
216
266
|
}
|
|
217
267
|
exports.TaskProjection = TaskProjection;
|
|
268
|
+
/**
|
|
269
|
+
* Who a rejection is about: the payload's `assignee`, falling back to the
|
|
270
|
+
* envelope sender when the payload leaves it empty (Python's
|
|
271
|
+
* `p.assignee or envelope.sender`). Applied here, not to the stored record, so
|
|
272
|
+
* `TaskRejectRecord.assignee` keeps its as-sent value like TaskComplete/TaskFail.
|
|
273
|
+
*/
|
|
274
|
+
function rejecterOf(r) {
|
|
275
|
+
return r.assignee || r.sender;
|
|
276
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -86,6 +86,21 @@ export interface AgentManifest {
|
|
|
86
86
|
metadata?: Record<string, string>;
|
|
87
87
|
transportEndpoints?: TransportEndpoint[];
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* `capabilities.policyRegistry` (`macp.v1.PolicyRegistryCapability`) as
|
|
91
|
+
* `InitializeResult.capabilities` carries it.
|
|
92
|
+
*/
|
|
93
|
+
export interface PolicyRegistryCapability {
|
|
94
|
+
registerPolicy?: boolean;
|
|
95
|
+
listPolicies?: boolean;
|
|
96
|
+
listChanged?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Vendor-extension keys (RFC-MACP-0012 §4) this runtime implements, by full
|
|
99
|
+
* key name. Any other `x-` key is refused at RegisterPolicy. Advisory:
|
|
100
|
+
* admission, not this list, is authoritative. Runtime ≥ 0.9.0.
|
|
101
|
+
*/
|
|
102
|
+
ruleExtensions?: string[];
|
|
103
|
+
}
|
|
89
104
|
export interface InitializeResult {
|
|
90
105
|
selectedProtocolVersion: string;
|
|
91
106
|
runtimeInfo?: {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED