macp-sdk-typescript 0.9.0 → 0.10.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 +13 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +10 -0
- package/dist/projections/task.d.ts +20 -0
- package/dist/projections/task.js +65 -19
- 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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.10.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.9.0...v0.10.0) (2026-09-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* `client.sendProgress()` now throws `MacpSessionError` when exactly one of `sessionId`/`mode` is supplied. Such calls used to succeed against a runtime that had the symmetric gap; as of macp-runtime PR #137 they are rejected with `INVALID_ENVELOPE`. Pass both fields for a session-scoped Progress, or neither for an ambient one.
|
|
13
|
+
* **projections:** `TaskProjection` assignee semantics changed twice. (1) The first-accept-wins guard is now per-session, not per-`task_id`: on a transcript with two `TaskRequest`s, only the first `TaskAccept` assigns — the second task keeps `assignee === undefined` and `status === 'requested'`, where it previously got its own assignee. A `TaskAccept` the guard discards also no longer sets `phase = 'InProgress'`. (2) `TaskAccept` is no longer sticky forever: a `TaskReject` from the active assignee clears `assignee` back to `undefined`, so code reading `getTask(id).assignee` after a reject now sees `undefined` (or the reassigned participant) rather than the participant who rejected.
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **projections:** scope the Task assignee slot to the session and free it on reject ([#75](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/75)) ([f7841a9](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/f7841a96d120b3fc55effdfabf63a2d72f431081)), closes [#59](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/59) [#60](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/60) [#70](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/70) [#71](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/71)
|
|
18
|
+
* reject mixed ambient/session-scoped Progress envelopes ([#76](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/76)) ([a9e00e3](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/a9e00e370b7a4bf08d09190e8b75a12d221319f8))
|
|
19
|
+
|
|
7
20
|
## [0.9.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.8.0...v0.9.0) (2026-09-01)
|
|
8
21
|
|
|
9
22
|
|
package/dist/client.d.ts
CHANGED
|
@@ -262,6 +262,15 @@ export declare class MacpClient {
|
|
|
262
262
|
auth?: AuthConfig;
|
|
263
263
|
deadlineMs?: number;
|
|
264
264
|
}): Promise<Ack>;
|
|
265
|
+
/**
|
|
266
|
+
* Send a `Progress` message.
|
|
267
|
+
*
|
|
268
|
+
* Per RFC-MACP-0001 §6, `Progress` is legal in exactly two shapes: *ambient*
|
|
269
|
+
* (`sessionId` and `mode` both omitted/empty) or *session-scoped* (both
|
|
270
|
+
* non-empty). Supplying exactly one of the two throws `MacpSessionError` —
|
|
271
|
+
* the runtime rejects that mixed envelope with `INVALID_ENVELOPE`, so this
|
|
272
|
+
* fails fast with a message naming the mismatched field.
|
|
273
|
+
*/
|
|
265
274
|
sendProgress(options: {
|
|
266
275
|
sessionId?: string;
|
|
267
276
|
mode?: string;
|
package/dist/client.js
CHANGED
|
@@ -545,7 +545,17 @@ class MacpClient {
|
|
|
545
545
|
});
|
|
546
546
|
return this.send(envelope, { auth, deadlineMs: options.deadlineMs });
|
|
547
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Send a `Progress` message.
|
|
550
|
+
*
|
|
551
|
+
* Per RFC-MACP-0001 §6, `Progress` is legal in exactly two shapes: *ambient*
|
|
552
|
+
* (`sessionId` and `mode` both omitted/empty) or *session-scoped* (both
|
|
553
|
+
* non-empty). Supplying exactly one of the two throws `MacpSessionError` —
|
|
554
|
+
* the runtime rejects that mixed envelope with `INVALID_ENVELOPE`, so this
|
|
555
|
+
* fails fast with a message naming the mismatched field.
|
|
556
|
+
*/
|
|
548
557
|
async sendProgress(options) {
|
|
558
|
+
(0, validation_1.validateProgressScope)(options.sessionId ?? '', options.mode ?? '');
|
|
549
559
|
const auth = this.requireAuth(options.auth);
|
|
550
560
|
(0, auth_1.assertSenderMatchesIdentity)(auth, options.sender);
|
|
551
561
|
const progressPayload = (0, envelope_1.buildProgressPayload)({
|
|
@@ -61,6 +61,26 @@ export declare class TaskProjection {
|
|
|
61
61
|
* only as a one-line pointer.
|
|
62
62
|
*/
|
|
63
63
|
private readonly seenMessageIds;
|
|
64
|
+
/**
|
|
65
|
+
* The **session-level** active assignment: who currently holds the session's
|
|
66
|
+
* single assignee slot, and which `task_id` they took it on.
|
|
67
|
+
*
|
|
68
|
+
* RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
|
|
69
|
+
* the Session in base v1." The constraint is scoped to the *Session*, not to
|
|
70
|
+
* a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
|
|
71
|
+
* `TaskRequest`, so in a conforming transcript the two scopes coincide, but
|
|
72
|
+
* an unfiltered transcript carrying two `TaskRequest`s must not be able to
|
|
73
|
+
* hand out two active assignees. That is why this is one field on the
|
|
74
|
+
* projection rather than a per-`TaskRecord` flag (issue #71).
|
|
75
|
+
*
|
|
76
|
+
* `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
|
|
77
|
+
* mirror the reference runtime: `macp-runtime`
|
|
78
|
+
* `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
|
|
79
|
+
* Some(env.sender)` after rejecting any payload whose `assignee` disagrees
|
|
80
|
+
* with the sender (`:205-207`). `taskId` is retained so a reject can clear
|
|
81
|
+
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
82
|
+
*/
|
|
83
|
+
private activeAssignment?;
|
|
64
84
|
/**
|
|
65
85
|
* Apply one envelope to this projection's in-process state.
|
|
66
86
|
*
|
package/dist/projections/task.js
CHANGED
|
@@ -31,6 +31,26 @@ class TaskProjection {
|
|
|
31
31
|
* only as a one-line pointer.
|
|
32
32
|
*/
|
|
33
33
|
seenMessageIds = new Set();
|
|
34
|
+
/**
|
|
35
|
+
* The **session-level** active assignment: who currently holds the session's
|
|
36
|
+
* single assignee slot, and which `task_id` they took it on.
|
|
37
|
+
*
|
|
38
|
+
* RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
|
|
39
|
+
* the Session in base v1." The constraint is scoped to the *Session*, not to
|
|
40
|
+
* a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
|
|
41
|
+
* `TaskRequest`, so in a conforming transcript the two scopes coincide, but
|
|
42
|
+
* an unfiltered transcript carrying two `TaskRequest`s must not be able to
|
|
43
|
+
* hand out two active assignees. That is why this is one field on the
|
|
44
|
+
* projection rather than a per-`TaskRecord` flag (issue #71).
|
|
45
|
+
*
|
|
46
|
+
* `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
|
|
47
|
+
* mirror the reference runtime: `macp-runtime`
|
|
48
|
+
* `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
|
|
49
|
+
* Some(env.sender)` after rejecting any payload whose `assignee` disagrees
|
|
50
|
+
* with the sender (`:205-207`). `taskId` is retained so a reject can clear
|
|
51
|
+
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
52
|
+
*/
|
|
53
|
+
activeAssignment;
|
|
34
54
|
/**
|
|
35
55
|
* Apply one envelope to this projection's in-process state.
|
|
36
56
|
*
|
|
@@ -81,46 +101,72 @@ class TaskProjection {
|
|
|
81
101
|
case 'TaskAccept': {
|
|
82
102
|
const record = payload;
|
|
83
103
|
const task = this.tasks.get(record.taskId);
|
|
84
|
-
// RFC-MACP-0009 §5 rules 3/3a (`:69-
|
|
104
|
+
// RFC-MACP-0009 §5 rules 3/3a (`:69-70`): "Only one assignee may
|
|
85
105
|
// become active for the Session in base v1. The first accepted
|
|
86
106
|
// `TaskAccept` from any eligible participant designates that
|
|
87
107
|
// participant as the active assignee. Subsequent `TaskAccept`
|
|
88
108
|
// messages for the same session MUST be rejected if an active
|
|
89
109
|
// assignee is already designated." A conforming runtime rejects the
|
|
90
|
-
// second `TaskAccept` before it reaches accepted history
|
|
91
|
-
//
|
|
92
|
-
//
|
|
110
|
+
// second `TaskAccept` before it reaches accepted history
|
|
111
|
+
// (`macp-runtime` `crates/macp-modes/src/mode/task.rs:184-186`); this
|
|
112
|
+
// guard makes the projection first-accept-wins too, so a rogue second
|
|
113
|
+
// one (e.g. an unfiltered transcript) cannot silently reassign.
|
|
93
114
|
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// `
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
115
|
+
// The guard is keyed on the SESSION slot (`activeAssignment`), not on
|
|
116
|
+
// `task.assignee`, because rule 3's scope is the Session (issue #71).
|
|
117
|
+
// Two `TaskRequest`s in one transcript therefore share one assignee
|
|
118
|
+
// slot, matching the runtime, whose `TaskState` holds a single
|
|
119
|
+
// `active_assignee` (`task.rs:70`) alongside a single `task`.
|
|
120
|
+
//
|
|
121
|
+
// Rule 3c's policy-gated reassignment (`allow_reassignment_on_reject`,
|
|
122
|
+
// RFC-MACP-0012 `:135`) is reachable here via the `TaskReject` case
|
|
123
|
+
// below, which frees the slot — see its comment for why that needs no
|
|
124
|
+
// policy input.
|
|
102
125
|
//
|
|
103
126
|
// An anomaly would be recorded when this guard discards a second
|
|
104
127
|
// `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
|
|
105
128
|
// deliberately frozen pending cross-SDK agreement with
|
|
106
129
|
// macp-sdk-python.
|
|
107
|
-
if (task &&
|
|
130
|
+
if (task && this.activeAssignment === undefined) {
|
|
108
131
|
task.assignee = record.assignee;
|
|
109
132
|
task.status = 'accepted';
|
|
133
|
+
this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
|
|
134
|
+
this.phase = 'InProgress';
|
|
110
135
|
}
|
|
111
|
-
this.phase = 'InProgress';
|
|
112
136
|
break;
|
|
113
137
|
}
|
|
114
138
|
case 'TaskReject': {
|
|
115
139
|
const record = payload;
|
|
116
140
|
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
141
|
if (task)
|
|
123
142
|
task.status = 'rejected';
|
|
143
|
+
// RFC-MACP-0009 §5 rule 3c (`:72`): "When policy sets
|
|
144
|
+
// `allow_reassignment_on_reject: true` and the active assignee sends
|
|
145
|
+
// `TaskReject`, the session returns to the pre-assignment state. Other
|
|
146
|
+
// eligible participants MAY then send `TaskAccept` for the same
|
|
147
|
+
// `task_id`." Mirrors `macp-runtime`
|
|
148
|
+
// `crates/macp-modes/src/mode/task.rs:257-260`, which clears
|
|
149
|
+
// `active_assignee` only when the ACTIVE assignee is the rejecter —
|
|
150
|
+
// a reject from anyone else never frees the slot (`task.rs:241-244`
|
|
151
|
+
// rejects it as `InvalidPayload`).
|
|
152
|
+
//
|
|
153
|
+
// Deliberately NOT gated on the session policy, and it does not need
|
|
154
|
+
// to be. The runtime gates the reassignment twice — it denies the
|
|
155
|
+
// active assignee's `TaskReject` (`task.rs:223-239`) and the follow-up
|
|
156
|
+
// `TaskAccept` (`task.rs:187-204`) with `PolicyDenied` when
|
|
157
|
+
// `allow_reassignment_on_reject` is false — and a projection is a view
|
|
158
|
+
// of history the runtime ALREADY ACCEPTED (see this method's
|
|
159
|
+
// accepted-only input contract). A reassignment the runtime denied
|
|
160
|
+
// never reaches the transcript, so tracking the reject unconditionally
|
|
161
|
+
// cannot diverge from the runtime on any real transcript, and it needs
|
|
162
|
+
// no new API surface (threading a `PolicyDefinition` into the
|
|
163
|
+
// constructor would). Issue #70, option 2.
|
|
164
|
+
if (this.activeAssignment?.sender === envelope.sender) {
|
|
165
|
+
const held = this.tasks.get(this.activeAssignment.taskId);
|
|
166
|
+
if (held)
|
|
167
|
+
held.assignee = undefined;
|
|
168
|
+
this.activeAssignment = undefined;
|
|
169
|
+
}
|
|
124
170
|
break;
|
|
125
171
|
}
|
|
126
172
|
case 'TaskUpdate': {
|
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