macp-sdk-typescript 0.9.0 → 0.11.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 +107 -0
- package/README.md +1 -1
- package/dist/client.d.ts +9 -0
- package/dist/client.js +10 -0
- package/dist/policy.d.ts +54 -7
- package/dist/policy.js +143 -19
- package/dist/projections/base.js +24 -5
- package/dist/projections/decision.d.ts +5 -49
- package/dist/projections/decision.js +7 -82
- package/dist/projections/handoff.d.ts +5 -48
- package/dist/projections/handoff.js +5 -77
- package/dist/projections/proposal.d.ts +5 -48
- package/dist/projections/proposal.js +5 -77
- package/dist/projections/quorum.d.ts +5 -49
- package/dist/projections/quorum.js +7 -82
- package/dist/projections/task.d.ts +21 -44
- package/dist/projections/task.js +66 -92
- package/dist/proto-registry.d.ts +19 -5
- package/dist/proto-registry.js +24 -16
- package/dist/validation.d.ts +20 -0
- package/dist/validation.js +32 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BaseProjection } from './base';
|
|
2
2
|
import type { Envelope } from '../types';
|
|
3
3
|
import type { ProtoRegistry } from '../proto-registry';
|
|
4
4
|
export interface TaskRecord {
|
|
@@ -33,58 +33,35 @@ export interface TaskFailureRecord {
|
|
|
33
33
|
retryable: boolean;
|
|
34
34
|
sender: string;
|
|
35
35
|
}
|
|
36
|
-
export declare class TaskProjection {
|
|
36
|
+
export declare class TaskProjection extends BaseProjection {
|
|
37
|
+
protected readonly mode = "macp.mode.task.v1";
|
|
37
38
|
readonly tasks: Map<string, TaskRecord>;
|
|
38
39
|
readonly updates: TaskUpdateRecord[];
|
|
39
40
|
readonly completions: TaskCompletionRecord[];
|
|
40
41
|
readonly failures: TaskFailureRecord[];
|
|
41
|
-
/**
|
|
42
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
43
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
44
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
45
|
-
* only as a one-line pointer.
|
|
46
|
-
*/
|
|
47
|
-
readonly transcript: Envelope[];
|
|
48
|
-
/**
|
|
49
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
50
|
-
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
51
|
-
* for the canonical description; duplicated here only as a one-line
|
|
52
|
-
* pointer. No built-in detection populates this for Task mode.
|
|
53
|
-
*/
|
|
54
|
-
readonly anomalies: ProjectionAnomaly[];
|
|
55
42
|
phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
|
|
56
|
-
commitment?: Record<string, unknown>;
|
|
57
43
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
61
|
-
* only as a one-line pointer.
|
|
62
|
-
*/
|
|
63
|
-
private readonly seenMessageIds;
|
|
64
|
-
/**
|
|
65
|
-
* Apply one envelope to this projection's in-process state.
|
|
44
|
+
* The **session-level** active assignment: who currently holds the session's
|
|
45
|
+
* single assignee slot, and which `task_id` they took it on.
|
|
66
46
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
47
|
+
* RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
|
|
48
|
+
* the Session in base v1." The constraint is scoped to the *Session*, not to
|
|
49
|
+
* a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
|
|
50
|
+
* `TaskRequest`, so in a conforming transcript the two scopes coincide, but
|
|
51
|
+
* an unfiltered transcript carrying two `TaskRequest`s must not be able to
|
|
52
|
+
* hand out two active assignees. That is why this is one field on the
|
|
53
|
+
* projection rather than a per-`TaskRecord` flag (issue #71).
|
|
73
54
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
82
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
83
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
55
|
+
* `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
|
|
56
|
+
* mirror the reference runtime: `macp-runtime`
|
|
57
|
+
* `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
|
|
58
|
+
* Some(env.sender)` after rejecting any payload whose `assignee` disagrees
|
|
59
|
+
* with the sender (`:205-207`). `taskId` is retained so a reject can clear
|
|
60
|
+
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
84
61
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
62
|
+
private activeAssignment?;
|
|
63
|
+
/** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
64
|
+
protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
88
65
|
getTask(taskId: string): TaskRecord | undefined;
|
|
89
66
|
isComplete(taskId: string): boolean;
|
|
90
67
|
isFailed(taskId: string): boolean;
|
package/dist/projections/task.js
CHANGED
|
@@ -2,65 +2,36 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
-
const
|
|
6
|
-
class TaskProjection {
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
class TaskProjection extends base_1.BaseProjection {
|
|
7
|
+
mode = constants_1.MODE_TASK;
|
|
7
8
|
tasks = new Map();
|
|
8
9
|
updates = [];
|
|
9
10
|
completions = [];
|
|
10
11
|
failures = [];
|
|
11
|
-
/**
|
|
12
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
13
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
14
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
15
|
-
* only as a one-line pointer.
|
|
16
|
-
*/
|
|
17
|
-
transcript = [];
|
|
18
|
-
/**
|
|
19
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
20
|
-
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
21
|
-
* for the canonical description; duplicated here only as a one-line
|
|
22
|
-
* pointer. No built-in detection populates this for Task mode.
|
|
23
|
-
*/
|
|
24
|
-
anomalies = [];
|
|
25
12
|
phase = 'Pending';
|
|
26
|
-
commitment;
|
|
27
|
-
/**
|
|
28
|
-
* `message_id`s already applied to this projection. See
|
|
29
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
30
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
31
|
-
* only as a one-line pointer.
|
|
32
|
-
*/
|
|
33
|
-
seenMessageIds = new Set();
|
|
34
13
|
/**
|
|
35
|
-
*
|
|
14
|
+
* The **session-level** active assignment: who currently holds the session's
|
|
15
|
+
* single assignee slot, and which `task_id` they took it on.
|
|
36
16
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
17
|
+
* RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
|
|
18
|
+
* the Session in base v1." The constraint is scoped to the *Session*, not to
|
|
19
|
+
* a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
|
|
20
|
+
* `TaskRequest`, so in a conforming transcript the two scopes coincide, but
|
|
21
|
+
* an unfiltered transcript carrying two `TaskRequest`s must not be able to
|
|
22
|
+
* hand out two active assignees. That is why this is one field on the
|
|
23
|
+
* projection rather than a per-`TaskRecord` flag (issue #71).
|
|
43
24
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
25
|
+
* `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
|
|
26
|
+
* mirror the reference runtime: `macp-runtime`
|
|
27
|
+
* `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
|
|
28
|
+
* Some(env.sender)` after rejecting any payload whose `assignee` disagrees
|
|
29
|
+
* with the sender (`:205-207`). `taskId` is retained so a reject can clear
|
|
30
|
+
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
48
31
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (envelope.messageId) {
|
|
53
|
-
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
54
|
-
logging_1.logger.debug('projection redelivery ignored', {
|
|
55
|
-
messageId: envelope.messageId,
|
|
56
|
-
mode: envelope.mode,
|
|
57
|
-
messageType: envelope.messageType,
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
this.seenMessageIds.add(envelope.messageId);
|
|
62
|
-
}
|
|
63
|
-
this.transcript.push(envelope);
|
|
32
|
+
activeAssignment;
|
|
33
|
+
/** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
34
|
+
applyMode(envelope, protoRegistry) {
|
|
64
35
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
65
36
|
switch (envelope.messageType) {
|
|
66
37
|
case 'TaskRequest': {
|
|
@@ -81,46 +52,72 @@ class TaskProjection {
|
|
|
81
52
|
case 'TaskAccept': {
|
|
82
53
|
const record = payload;
|
|
83
54
|
const task = this.tasks.get(record.taskId);
|
|
84
|
-
// RFC-MACP-0009 §5 rules 3/3a (`:69-
|
|
55
|
+
// RFC-MACP-0009 §5 rules 3/3a (`:69-70`): "Only one assignee may
|
|
85
56
|
// become active for the Session in base v1. The first accepted
|
|
86
57
|
// `TaskAccept` from any eligible participant designates that
|
|
87
58
|
// participant as the active assignee. Subsequent `TaskAccept`
|
|
88
59
|
// messages for the same session MUST be rejected if an active
|
|
89
60
|
// assignee is already designated." A conforming runtime rejects the
|
|
90
|
-
// second `TaskAccept` before it reaches accepted history
|
|
91
|
-
//
|
|
92
|
-
//
|
|
61
|
+
// second `TaskAccept` before it reaches accepted history
|
|
62
|
+
// (`macp-runtime` `crates/macp-modes/src/mode/task.rs:184-186`); this
|
|
63
|
+
// guard makes the projection first-accept-wins too, so a rogue second
|
|
64
|
+
// one (e.g. an unfiltered transcript) cannot silently reassign.
|
|
65
|
+
//
|
|
66
|
+
// The guard is keyed on the SESSION slot (`activeAssignment`), not on
|
|
67
|
+
// `task.assignee`, because rule 3's scope is the Session (issue #71).
|
|
68
|
+
// Two `TaskRequest`s in one transcript therefore share one assignee
|
|
69
|
+
// slot, matching the runtime, whose `TaskState` holds a single
|
|
70
|
+
// `active_assignee` (`task.rs:70`) alongside a single `task`.
|
|
93
71
|
//
|
|
94
|
-
// Rule 3c's policy-gated reassignment
|
|
95
|
-
// RFC-MACP-0012 `:135`) is
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// is irrevocable unless policy explicitly permits reassignment").
|
|
99
|
-
// Plumbing policy into this projection needs its own decision (see
|
|
100
|
-
// plans/cross-repo/macp-sdk-typescript-conformance-and-transport.md,
|
|
101
|
-
// Phase 3 open question 4) before that path can be implemented.
|
|
72
|
+
// Rule 3c's policy-gated reassignment (`allow_reassignment_on_reject`,
|
|
73
|
+
// RFC-MACP-0012 `:135`) is reachable here via the `TaskReject` case
|
|
74
|
+
// below, which frees the slot — see its comment for why that needs no
|
|
75
|
+
// policy input.
|
|
102
76
|
//
|
|
103
77
|
// An anomaly would be recorded when this guard discards a second
|
|
104
78
|
// `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
|
|
105
79
|
// deliberately frozen pending cross-SDK agreement with
|
|
106
80
|
// macp-sdk-python.
|
|
107
|
-
if (task &&
|
|
81
|
+
if (task && this.activeAssignment === undefined) {
|
|
108
82
|
task.assignee = record.assignee;
|
|
109
83
|
task.status = 'accepted';
|
|
84
|
+
this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
|
|
85
|
+
this.phase = 'InProgress';
|
|
110
86
|
}
|
|
111
|
-
this.phase = 'InProgress';
|
|
112
87
|
break;
|
|
113
88
|
}
|
|
114
89
|
case 'TaskReject': {
|
|
115
90
|
const record = payload;
|
|
116
91
|
const task = this.tasks.get(record.taskId);
|
|
117
|
-
// Deliberately does not clear `task.assignee`: rule 3c's
|
|
118
|
-
// policy-gated reassignment path is not modelled here (see the
|
|
119
|
-
// `TaskAccept` case above), so `assignee` is treated as sticky once
|
|
120
|
-
// set, per rule 3b's default ("irrevocable unless policy explicitly
|
|
121
|
-
// permits reassignment").
|
|
122
92
|
if (task)
|
|
123
93
|
task.status = 'rejected';
|
|
94
|
+
// RFC-MACP-0009 §5 rule 3c (`:72`): "When policy sets
|
|
95
|
+
// `allow_reassignment_on_reject: true` and the active assignee sends
|
|
96
|
+
// `TaskReject`, the session returns to the pre-assignment state. Other
|
|
97
|
+
// eligible participants MAY then send `TaskAccept` for the same
|
|
98
|
+
// `task_id`." Mirrors `macp-runtime`
|
|
99
|
+
// `crates/macp-modes/src/mode/task.rs:257-260`, which clears
|
|
100
|
+
// `active_assignee` only when the ACTIVE assignee is the rejecter —
|
|
101
|
+
// a reject from anyone else never frees the slot (`task.rs:241-244`
|
|
102
|
+
// rejects it as `InvalidPayload`).
|
|
103
|
+
//
|
|
104
|
+
// Deliberately NOT gated on the session policy, and it does not need
|
|
105
|
+
// to be. The runtime gates the reassignment twice — it denies the
|
|
106
|
+
// active assignee's `TaskReject` (`task.rs:223-239`) and the follow-up
|
|
107
|
+
// `TaskAccept` (`task.rs:187-204`) with `PolicyDenied` when
|
|
108
|
+
// `allow_reassignment_on_reject` is false — and a projection is a view
|
|
109
|
+
// of history the runtime ALREADY ACCEPTED (see this method's
|
|
110
|
+
// accepted-only input contract). A reassignment the runtime denied
|
|
111
|
+
// never reaches the transcript, so tracking the reject unconditionally
|
|
112
|
+
// cannot diverge from the runtime on any real transcript, and it needs
|
|
113
|
+
// no new API surface (threading a `PolicyDefinition` into the
|
|
114
|
+
// constructor would). Issue #70, option 2.
|
|
115
|
+
if (this.activeAssignment?.sender === envelope.sender) {
|
|
116
|
+
const held = this.tasks.get(this.activeAssignment.taskId);
|
|
117
|
+
if (held)
|
|
118
|
+
held.assignee = undefined;
|
|
119
|
+
this.activeAssignment = undefined;
|
|
120
|
+
}
|
|
124
121
|
break;
|
|
125
122
|
}
|
|
126
123
|
case 'TaskUpdate': {
|
|
@@ -153,33 +150,10 @@ class TaskProjection {
|
|
|
153
150
|
this.phase = 'Failed';
|
|
154
151
|
break;
|
|
155
152
|
}
|
|
156
|
-
case 'Commitment': {
|
|
157
|
-
this.commitment = payload;
|
|
158
|
-
this.phase = 'Committed';
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
153
|
default:
|
|
162
154
|
break;
|
|
163
155
|
}
|
|
164
156
|
}
|
|
165
|
-
/**
|
|
166
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
167
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
168
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
169
|
-
*/
|
|
170
|
-
get hasAnomalies() {
|
|
171
|
-
return this.anomalies.length > 0;
|
|
172
|
-
}
|
|
173
|
-
get isCommitted() {
|
|
174
|
-
return this.commitment !== undefined;
|
|
175
|
-
}
|
|
176
|
-
get isPositiveOutcome() {
|
|
177
|
-
if (!this.commitment)
|
|
178
|
-
return undefined;
|
|
179
|
-
const val = this.commitment.outcomePositive ??
|
|
180
|
-
this.commitment.outcome_positive;
|
|
181
|
-
return val !== undefined ? Boolean(val) : true;
|
|
182
|
-
}
|
|
183
157
|
getTask(taskId) {
|
|
184
158
|
return this.tasks.get(taskId);
|
|
185
159
|
}
|
package/dist/proto-registry.d.ts
CHANGED
|
@@ -10,11 +10,25 @@ export declare class ProtoRegistry {
|
|
|
10
10
|
/**
|
|
11
11
|
* Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
|
|
12
12
|
* formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
|
|
13
|
-
* pre-proto histories) and canonical protobuf (`ContributePayload`).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* pre-proto histories) and canonical protobuf (`ContributePayload`). Both
|
|
14
|
+
* normalize to `{ value: string }`.
|
|
15
|
+
*
|
|
16
|
+
* Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
|
|
17
|
+
* attempted first, and only a `JSON.parse` failure falls through to
|
|
18
|
+
* protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
|
|
19
|
+
* not generalize — e.g. legacy JSON with leading whitespace has neither byte
|
|
20
|
+
* first, and used to reach `decodeMessage` and throw. Parity with
|
|
21
|
+
* `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
|
|
22
|
+
* worked this way. Safe because a canonical proto payload's bytes are
|
|
23
|
+
* essentially never also syntactically valid JSON.
|
|
24
|
+
*
|
|
25
|
+
* An empty payload falls through the same path: `JSON.parse('')` throws, so
|
|
26
|
+
* it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
|
|
27
|
+
* defaults) rather than a decode error. This SDK's decode layer is
|
|
28
|
+
* observational, not an acceptance gate — whether an empty `Contribute`
|
|
29
|
+
* should be rejected is a runtime-acceptance question (tracked cross-repo,
|
|
30
|
+
* `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
|
|
31
|
+
* currently `macp-runtime`-only), not something this method decides.
|
|
18
32
|
*/
|
|
19
33
|
private decodeMultiRoundContribute;
|
|
20
34
|
private lookupType;
|
package/dist/proto-registry.js
CHANGED
|
@@ -169,26 +169,34 @@ class ProtoRegistry {
|
|
|
169
169
|
/**
|
|
170
170
|
* Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
|
|
171
171
|
* formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
|
|
172
|
-
* pre-proto histories) and canonical protobuf (`ContributePayload`).
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
172
|
+
* pre-proto histories) and canonical protobuf (`ContributePayload`). Both
|
|
173
|
+
* normalize to `{ value: string }`.
|
|
174
|
+
*
|
|
175
|
+
* Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
|
|
176
|
+
* attempted first, and only a `JSON.parse` failure falls through to
|
|
177
|
+
* protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
|
|
178
|
+
* not generalize — e.g. legacy JSON with leading whitespace has neither byte
|
|
179
|
+
* first, and used to reach `decodeMessage` and throw. Parity with
|
|
180
|
+
* `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
|
|
181
|
+
* worked this way. Safe because a canonical proto payload's bytes are
|
|
182
|
+
* essentially never also syntactically valid JSON.
|
|
183
|
+
*
|
|
184
|
+
* An empty payload falls through the same path: `JSON.parse('')` throws, so
|
|
185
|
+
* it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
|
|
186
|
+
* defaults) rather than a decode error. This SDK's decode layer is
|
|
187
|
+
* observational, not an acceptance gate — whether an empty `Contribute`
|
|
188
|
+
* should be rejected is a runtime-acceptance question (tracked cross-repo,
|
|
189
|
+
* `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
|
|
190
|
+
* currently `macp-runtime`-only), not something this method decides.
|
|
177
191
|
*/
|
|
178
192
|
decodeMultiRoundContribute(payload) {
|
|
179
|
-
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(payload.toString('utf8'));
|
|
195
|
+
return { value: typeof parsed.value === 'string' ? parsed.value : String(parsed.value ?? '') };
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
180
198
|
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
199
|
}
|
|
191
|
-
return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
|
|
192
200
|
}
|
|
193
201
|
lookupType(typeName) {
|
|
194
202
|
const lookedUp = this.root.lookupType(typeName);
|
package/dist/validation.d.ts
CHANGED
|
@@ -5,6 +5,26 @@ export declare function validateConfidence(value: number): void;
|
|
|
5
5
|
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
|
+
/**
|
|
9
|
+
* Validate the `Progress` scope pairing (RFC-MACP-0001 §6).
|
|
10
|
+
*
|
|
11
|
+
* `Progress` is legal in exactly two shapes — ambient (`sessionId` and `mode`
|
|
12
|
+
* both empty) or session-scoped (both non-empty). An envelope with exactly one
|
|
13
|
+
* of the two empty is a mixed shape that the runtime rejects with
|
|
14
|
+
* `INVALID_ENVELOPE`; raising here names the mismatched field instead.
|
|
15
|
+
*
|
|
16
|
+
* Unlike Signals, `Progress` is *not* required to be ambient — this is a
|
|
17
|
+
* tri-state rule, so neither field may be inferred from the other.
|
|
18
|
+
*
|
|
19
|
+
* The emptiness test deliberately mirrors `macp-runtime`'s
|
|
20
|
+
* `validate_envelope_shape` **exactly**, including its asymmetry: `sessionId`
|
|
21
|
+
* is compared raw while `mode` is trimmed first. Mirroring rather than
|
|
22
|
+
* normalising is the only choice under which the SDK neither accepts a shape
|
|
23
|
+
* the runtime rejects nor rejects one it accepts. Concretely, a
|
|
24
|
+
* whitespace-only `mode` alongside an empty `sessionId` is ambient to the
|
|
25
|
+
* runtime, so it stays ambient here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateProgressScope(sessionId: string, mode: string): void;
|
|
8
28
|
export declare function validateTtlMs(ttlMs: number): void;
|
|
9
29
|
export declare function validateMaxSuspendMs(maxSuspendMs: number): void;
|
|
10
30
|
export declare function validateParticipants(participants: string[]): void;
|
package/dist/validation.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.validateConfidence = validateConfidence;
|
|
|
7
7
|
exports.validateSeverity = validateSeverity;
|
|
8
8
|
exports.validateParticipantCount = validateParticipantCount;
|
|
9
9
|
exports.validateSignalType = validateSignalType;
|
|
10
|
+
exports.validateProgressScope = validateProgressScope;
|
|
10
11
|
exports.validateTtlMs = validateTtlMs;
|
|
11
12
|
exports.validateMaxSuspendMs = validateMaxSuspendMs;
|
|
12
13
|
exports.validateParticipants = validateParticipants;
|
|
@@ -61,6 +62,37 @@ function validateSignalType(signalType, data) {
|
|
|
61
62
|
throw new errors_1.MacpSessionError('signalType must be non-empty when data is present');
|
|
62
63
|
}
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate the `Progress` scope pairing (RFC-MACP-0001 §6).
|
|
67
|
+
*
|
|
68
|
+
* `Progress` is legal in exactly two shapes — ambient (`sessionId` and `mode`
|
|
69
|
+
* both empty) or session-scoped (both non-empty). An envelope with exactly one
|
|
70
|
+
* of the two empty is a mixed shape that the runtime rejects with
|
|
71
|
+
* `INVALID_ENVELOPE`; raising here names the mismatched field instead.
|
|
72
|
+
*
|
|
73
|
+
* Unlike Signals, `Progress` is *not* required to be ambient — this is a
|
|
74
|
+
* tri-state rule, so neither field may be inferred from the other.
|
|
75
|
+
*
|
|
76
|
+
* The emptiness test deliberately mirrors `macp-runtime`'s
|
|
77
|
+
* `validate_envelope_shape` **exactly**, including its asymmetry: `sessionId`
|
|
78
|
+
* is compared raw while `mode` is trimmed first. Mirroring rather than
|
|
79
|
+
* normalising is the only choice under which the SDK neither accepts a shape
|
|
80
|
+
* the runtime rejects nor rejects one it accepts. Concretely, a
|
|
81
|
+
* whitespace-only `mode` alongside an empty `sessionId` is ambient to the
|
|
82
|
+
* runtime, so it stays ambient here.
|
|
83
|
+
*/
|
|
84
|
+
function validateProgressScope(sessionId, mode) {
|
|
85
|
+
const sessionIdEmpty = sessionId === '';
|
|
86
|
+
const modeEmpty = mode.trim() === '';
|
|
87
|
+
if (sessionIdEmpty === modeEmpty)
|
|
88
|
+
return;
|
|
89
|
+
const detail = sessionIdEmpty
|
|
90
|
+
? `mode is ${JSON.stringify(mode)} but sessionId is empty`
|
|
91
|
+
: `sessionId is ${JSON.stringify(sessionId)} but mode is empty`;
|
|
92
|
+
throw new errors_1.MacpSessionError(`Progress must be either ambient (sessionId and mode both empty) or session-scoped ` +
|
|
93
|
+
`(both non-empty), but ${detail} (RFC-MACP-0001 §6). ` +
|
|
94
|
+
'Pass both fields for a session-scoped Progress, or neither for an ambient one.');
|
|
95
|
+
}
|
|
64
96
|
const MAX_TTL_MS = 86_400_000; // 24 hours
|
|
65
97
|
function validateTtlMs(ttlMs) {
|
|
66
98
|
if (!Number.isFinite(ttlMs) || ttlMs < 1 || ttlMs > MAX_TTL_MS) {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -55,18 +55,18 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@grpc/grpc-js": "^1.14.3",
|
|
57
57
|
"@grpc/proto-loader": "^0.8.0",
|
|
58
|
-
"@multiagentcoordinationprotocol/proto": "^0.1.
|
|
58
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.10",
|
|
59
59
|
"protobufjs": "^8.6.5"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^26.4.0",
|
|
63
63
|
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
64
64
|
"@typescript-eslint/parser": "^8.68.0",
|
|
65
|
-
"@vitest/coverage-v8": "^
|
|
65
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
66
66
|
"eslint": "^10.9.1",
|
|
67
67
|
"prettier": "^3.0.0",
|
|
68
68
|
"tsx": "^4.21.0",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
|
-
"vitest": "^
|
|
70
|
+
"vitest": "^5.0.0"
|
|
71
71
|
}
|
|
72
72
|
}
|