macp-sdk-typescript 0.10.0 → 0.12.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 +465 -0
- package/README.md +2 -2
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -1
- package/dist/agent/participant.d.ts +47 -4
- package/dist/agent/participant.js +88 -10
- package/dist/agent/strategies.js +23 -7
- package/dist/agent/transports.d.ts +19 -1
- package/dist/agent/transports.js +108 -66
- package/dist/auth.d.ts +15 -4
- package/dist/auth.js +23 -7
- package/dist/base-session.js +13 -3
- package/dist/client.js +58 -15
- package/dist/commitment-hash.d.ts +33 -3
- package/dist/commitment-hash.js +39 -5
- package/dist/decision.d.ts +1 -0
- package/dist/decision.js +11 -9
- package/dist/envelope.js +9 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.js +19 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/policy.d.ts +54 -7
- package/dist/policy.js +143 -19
- package/dist/projections/base.d.ts +71 -14
- package/dist/projections/base.js +106 -16
- package/dist/projections/decision.d.ts +5 -49
- package/dist/projections/decision.js +30 -85
- package/dist/projections/handoff.d.ts +5 -48
- package/dist/projections/handoff.js +44 -93
- package/dist/projections/proposal.d.ts +5 -48
- package/dist/projections/proposal.js +21 -79
- package/dist/projections/quorum.d.ts +5 -49
- package/dist/projections/quorum.js +20 -82
- package/dist/projections/task.d.ts +31 -51
- package/dist/projections/task.js +49 -90
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -2
- package/dist/proto-registry.d.ts +85 -5
- package/dist/proto-registry.js +101 -15
- package/dist/quorum.d.ts +1 -0
- package/dist/quorum.js +4 -4
- package/dist/task.d.ts +1 -0
- package/dist/task.js +24 -11
- package/dist/types.d.ts +14 -7
- package/dist/validation.d.ts +14 -0
- package/dist/validation.js +39 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
|
@@ -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 ApprovalRequestRecord {
|
|
@@ -14,57 +14,13 @@ export interface BallotRecord {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
sender: string;
|
|
16
16
|
}
|
|
17
|
-
export declare class QuorumProjection {
|
|
17
|
+
export declare class QuorumProjection extends BaseProjection {
|
|
18
|
+
protected readonly mode = "macp.mode.quorum.v1";
|
|
18
19
|
readonly requests: Map<string, ApprovalRequestRecord>;
|
|
19
20
|
readonly ballots: Map<string, Map<string, BallotRecord>>;
|
|
20
|
-
/**
|
|
21
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
22
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
23
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
24
|
-
* only as a one-line pointer.
|
|
25
|
-
*/
|
|
26
|
-
readonly transcript: Envelope[];
|
|
27
|
-
/**
|
|
28
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
29
|
-
* transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
|
|
30
|
-
* from the same sender for the same `request_id`). See
|
|
31
|
-
* `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
|
|
32
|
-
* description; duplicated here only as a one-line pointer.
|
|
33
|
-
*/
|
|
34
|
-
readonly anomalies: ProjectionAnomaly[];
|
|
35
21
|
phase: 'Pending' | 'Voting' | 'Committed';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* `message_id`s already applied to this projection. See
|
|
39
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
40
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
41
|
-
* only as a one-line pointer.
|
|
42
|
-
*/
|
|
43
|
-
private readonly seenMessageIds;
|
|
44
|
-
/**
|
|
45
|
-
* Apply one envelope to this projection's in-process state.
|
|
46
|
-
*
|
|
47
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
48
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
49
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
50
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
51
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
52
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
53
|
-
*
|
|
54
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
55
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
56
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
57
|
-
* citation; duplicated here only as a one-line pointer.
|
|
58
|
-
*/
|
|
59
|
-
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
60
|
-
/**
|
|
61
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
62
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
63
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
64
|
-
*/
|
|
65
|
-
get hasAnomalies(): boolean;
|
|
66
|
-
get isCommitted(): boolean;
|
|
67
|
-
get isPositiveOutcome(): boolean | undefined;
|
|
22
|
+
/** Handle a Quorum-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
23
|
+
protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
68
24
|
private setBallot;
|
|
69
25
|
approvalCount(requestId: string): number;
|
|
70
26
|
rejectionCount(requestId: string): number;
|
|
@@ -2,64 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QuorumProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
-
const
|
|
6
|
-
class QuorumProjection {
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
class QuorumProjection extends base_1.BaseProjection {
|
|
7
|
+
mode = constants_1.MODE_QUORUM;
|
|
7
8
|
requests = new Map();
|
|
8
9
|
ballots = new Map();
|
|
9
|
-
/**
|
|
10
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
11
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
12
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
13
|
-
* only as a one-line pointer.
|
|
14
|
-
*/
|
|
15
|
-
transcript = [];
|
|
16
|
-
/**
|
|
17
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
18
|
-
* transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
|
|
19
|
-
* from the same sender for the same `request_id`). See
|
|
20
|
-
* `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
|
|
21
|
-
* description; duplicated here only as a one-line pointer.
|
|
22
|
-
*/
|
|
23
|
-
anomalies = [];
|
|
24
10
|
phase = 'Pending';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* `message_id`s already applied to this projection. See
|
|
28
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
29
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
30
|
-
* only as a one-line pointer.
|
|
31
|
-
*/
|
|
32
|
-
seenMessageIds = new Set();
|
|
33
|
-
/**
|
|
34
|
-
* Apply one envelope to this projection's in-process state.
|
|
35
|
-
*
|
|
36
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
37
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
38
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
39
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
40
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
41
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
42
|
-
*
|
|
43
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
44
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
45
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
46
|
-
* citation; duplicated here only as a one-line pointer.
|
|
47
|
-
*/
|
|
48
|
-
applyEnvelope(envelope, protoRegistry) {
|
|
49
|
-
if (envelope.mode !== constants_1.MODE_QUORUM)
|
|
50
|
-
return;
|
|
51
|
-
if (envelope.messageId) {
|
|
52
|
-
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
53
|
-
logging_1.logger.debug('projection redelivery ignored', {
|
|
54
|
-
messageId: envelope.messageId,
|
|
55
|
-
mode: envelope.mode,
|
|
56
|
-
messageType: envelope.messageType,
|
|
57
|
-
});
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
this.seenMessageIds.add(envelope.messageId);
|
|
61
|
-
}
|
|
62
|
-
this.transcript.push(envelope);
|
|
11
|
+
/** Handle a Quorum-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
12
|
+
applyMode(envelope, protoRegistry) {
|
|
63
13
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
64
14
|
switch (envelope.messageType) {
|
|
65
15
|
case 'ApprovalRequest': {
|
|
@@ -83,34 +33,24 @@ class QuorumProjection {
|
|
|
83
33
|
this.setBallot(envelope, record.requestId, 'abstain', record.reason);
|
|
84
34
|
break;
|
|
85
35
|
}
|
|
86
|
-
case 'Commitment': {
|
|
87
|
-
this.commitment = payload;
|
|
88
|
-
this.phase = 'Committed';
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
36
|
default:
|
|
92
37
|
break;
|
|
93
38
|
}
|
|
94
39
|
}
|
|
95
|
-
/**
|
|
96
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
97
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
98
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
99
|
-
*/
|
|
100
|
-
get hasAnomalies() {
|
|
101
|
-
return this.anomalies.length > 0;
|
|
102
|
-
}
|
|
103
|
-
get isCommitted() {
|
|
104
|
-
return this.commitment !== undefined;
|
|
105
|
-
}
|
|
106
|
-
get isPositiveOutcome() {
|
|
107
|
-
if (!this.commitment)
|
|
108
|
-
return undefined;
|
|
109
|
-
const val = this.commitment.outcomePositive ??
|
|
110
|
-
this.commitment.outcome_positive;
|
|
111
|
-
return val !== undefined ? Boolean(val) : true;
|
|
112
|
-
}
|
|
113
40
|
setBallot(envelope, requestId, vote, reason) {
|
|
41
|
+
// RFC-MACP-0011 §5 rule 3 (`rfcs/RFC-MACP-0011-quorum-mode.md:69`): a ballot
|
|
42
|
+
// MUST reference the Session's accepted request_id. `applyEnvelope`'s input
|
|
43
|
+
// contract is accepted-only (see accepted-only-contract.test.ts), not a
|
|
44
|
+
// guarantee every field inside an accepted envelope is itself valid, so an
|
|
45
|
+
// unfiltered/multi-session transcript could still carry a ballot for a
|
|
46
|
+
// request_id this projection never saw an ApprovalRequest for. Without this
|
|
47
|
+
// guard that would fabricate a `ballots` Map entry: votedSenders() and
|
|
48
|
+
// approvalCount()/rejectionCount()/abstentionCount() would report on a
|
|
49
|
+
// request that doesn't exist, and a second such ballot from the same sender
|
|
50
|
+
// would fire a spurious duplicate_ballot anomaly against it (issue #121,
|
|
51
|
+
// same bug shape as issue #119's proposal.ts/handoff.ts/decision.ts sites).
|
|
52
|
+
if (!this.requests.has(requestId))
|
|
53
|
+
return;
|
|
114
54
|
const senderMap = this.ballots.get(requestId) ?? new Map();
|
|
115
55
|
const kept = senderMap.get(envelope.sender);
|
|
116
56
|
if (kept !== undefined) {
|
|
@@ -129,7 +69,7 @@ class QuorumProjection {
|
|
|
129
69
|
// by RFC-0011 either; it is parity with RFC-MACP-0007 §5 item 3's
|
|
130
70
|
// explicit first-stands rule for `Vote`, plus macp-runtime's enforced
|
|
131
71
|
// first-wins behaviour.
|
|
132
|
-
|
|
72
|
+
this.recordAnomaly({
|
|
133
73
|
kind: 'duplicate_ballot',
|
|
134
74
|
mode: envelope.mode,
|
|
135
75
|
messageType: envelope.messageType,
|
|
@@ -137,9 +77,7 @@ class QuorumProjection {
|
|
|
137
77
|
sender: envelope.sender,
|
|
138
78
|
subjectId: requestId,
|
|
139
79
|
detail: `sender ${envelope.sender} already cast '${kept.vote}' on request ${requestId}; discarded '${vote}'`,
|
|
140
|
-
};
|
|
141
|
-
this.anomalies.push(anomaly);
|
|
142
|
-
logging_1.logger.warn('projection anomaly', anomaly);
|
|
80
|
+
});
|
|
143
81
|
return;
|
|
144
82
|
}
|
|
145
83
|
senderMap.set(envelope.sender, { requestId, vote, reason, sender: envelope.sender });
|
|
@@ -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 {
|
|
@@ -19,13 +19,21 @@ export interface TaskUpdateRecord {
|
|
|
19
19
|
message?: string;
|
|
20
20
|
sender: string;
|
|
21
21
|
}
|
|
22
|
-
export interface
|
|
22
|
+
export interface TaskCompleteRecord {
|
|
23
23
|
taskId: string;
|
|
24
24
|
assignee: string;
|
|
25
25
|
summary?: string;
|
|
26
26
|
sender: string;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated Use {@link TaskCompleteRecord}. Renamed for cross-SDK naming
|
|
30
|
+
* parity (multiagentcoordinationprotocol#135, issue #138) — the record now
|
|
31
|
+
* mirrors its triggering `TaskComplete` message type the way its sibling
|
|
32
|
+
* {@link TaskUpdateRecord} already mirrors `TaskUpdate`, and matches
|
|
33
|
+
* `macp-sdk-python`'s `TaskCompleteRecord`. Scheduled for removal in 0.13.0.
|
|
34
|
+
*/
|
|
35
|
+
export type TaskCompletionRecord = TaskCompleteRecord;
|
|
36
|
+
export interface TaskFailRecord {
|
|
29
37
|
taskId: string;
|
|
30
38
|
assignee: string;
|
|
31
39
|
errorCode?: string;
|
|
@@ -33,34 +41,21 @@ export interface TaskFailureRecord {
|
|
|
33
41
|
retryable: boolean;
|
|
34
42
|
sender: string;
|
|
35
43
|
}
|
|
36
|
-
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Use {@link TaskFailRecord}. Renamed for cross-SDK naming
|
|
46
|
+
* parity (multiagentcoordinationprotocol#135, issue #138) — the record now
|
|
47
|
+
* mirrors its triggering `TaskFail` message type the way its sibling
|
|
48
|
+
* {@link TaskUpdateRecord} already mirrors `TaskUpdate`, and matches
|
|
49
|
+
* `macp-sdk-python`'s `TaskFailRecord`. Scheduled for removal in 0.13.0.
|
|
50
|
+
*/
|
|
51
|
+
export type TaskFailureRecord = TaskFailRecord;
|
|
52
|
+
export declare class TaskProjection extends BaseProjection {
|
|
53
|
+
protected readonly mode = "macp.mode.task.v1";
|
|
37
54
|
readonly tasks: Map<string, TaskRecord>;
|
|
38
55
|
readonly updates: TaskUpdateRecord[];
|
|
39
|
-
readonly completions:
|
|
40
|
-
readonly failures:
|
|
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[];
|
|
56
|
+
readonly completions: TaskCompleteRecord[];
|
|
57
|
+
readonly failures: TaskFailRecord[];
|
|
55
58
|
phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
|
|
56
|
-
commitment?: Record<string, unknown>;
|
|
57
|
-
/**
|
|
58
|
-
* `message_id`s already applied to this projection. See
|
|
59
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
60
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
61
|
-
* only as a one-line pointer.
|
|
62
|
-
*/
|
|
63
|
-
private readonly seenMessageIds;
|
|
64
59
|
/**
|
|
65
60
|
* The **session-level** active assignment: who currently holds the session's
|
|
66
61
|
* single assignee slot, and which `task_id` they took it on.
|
|
@@ -81,31 +76,16 @@ export declare class TaskProjection {
|
|
|
81
76
|
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
82
77
|
*/
|
|
83
78
|
private activeAssignment?;
|
|
79
|
+
/** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
80
|
+
protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
81
|
+
getTask(taskId: string): TaskRecord | undefined;
|
|
82
|
+
isCompleted(taskId: string): boolean;
|
|
84
83
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
90
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
91
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
92
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
93
|
-
*
|
|
94
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
95
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
96
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
97
|
-
* citation; duplicated here only as a one-line pointer.
|
|
98
|
-
*/
|
|
99
|
-
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
100
|
-
/**
|
|
101
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
102
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
103
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
84
|
+
* @deprecated Use {@link isCompleted}. Renamed for cross-SDK naming parity
|
|
85
|
+
* (multiagentcoordinationprotocol#135, issue #138) — every other `is*`
|
|
86
|
+
* predicate in this SDK is past-participle, and `macp-sdk-python` has
|
|
87
|
+
* always spelled this `is_completed`. Scheduled for removal in 0.13.0.
|
|
104
88
|
*/
|
|
105
|
-
get hasAnomalies(): boolean;
|
|
106
|
-
get isCommitted(): boolean;
|
|
107
|
-
get isPositiveOutcome(): boolean | undefined;
|
|
108
|
-
getTask(taskId: string): TaskRecord | undefined;
|
|
109
89
|
isComplete(taskId: string): boolean;
|
|
110
90
|
isFailed(taskId: string): boolean;
|
|
111
91
|
isRetryable(taskId: string): boolean;
|
package/dist/projections/task.js
CHANGED
|
@@ -2,35 +2,14 @@
|
|
|
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
|
|
36
15
|
* single assignee slot, and which `task_id` they took it on.
|
|
@@ -51,36 +30,8 @@ class TaskProjection {
|
|
|
51
30
|
* the right `TaskRecord.assignee` even on a multi-task transcript.
|
|
52
31
|
*/
|
|
53
32
|
activeAssignment;
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
58
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
59
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
60
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
61
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
62
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
63
|
-
*
|
|
64
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
65
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
66
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
67
|
-
* citation; duplicated here only as a one-line pointer.
|
|
68
|
-
*/
|
|
69
|
-
applyEnvelope(envelope, protoRegistry) {
|
|
70
|
-
if (envelope.mode !== constants_1.MODE_TASK)
|
|
71
|
-
return;
|
|
72
|
-
if (envelope.messageId) {
|
|
73
|
-
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
74
|
-
logging_1.logger.debug('projection redelivery ignored', {
|
|
75
|
-
messageId: envelope.messageId,
|
|
76
|
-
mode: envelope.mode,
|
|
77
|
-
messageType: envelope.messageType,
|
|
78
|
-
});
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
this.seenMessageIds.add(envelope.messageId);
|
|
82
|
-
}
|
|
83
|
-
this.transcript.push(envelope);
|
|
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) {
|
|
84
35
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
85
36
|
switch (envelope.messageType) {
|
|
86
37
|
case 'TaskRequest': {
|
|
@@ -123,16 +74,37 @@ class TaskProjection {
|
|
|
123
74
|
// below, which frees the slot — see its comment for why that needs no
|
|
124
75
|
// policy input.
|
|
125
76
|
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
77
|
+
// A discarded second `TaskAccept` records a `duplicate_task_accept`
|
|
78
|
+
// anomaly (issue #126/#128) only when this projection actually has a
|
|
79
|
+
// `TaskRequest` on file for the losing `taskId` (`task` is defined) —
|
|
80
|
+
// an accept for a `taskId` this projection never saw a `TaskRequest`
|
|
81
|
+
// for stays a silent no-op below, since a projection that joined
|
|
82
|
+
// mid-session may never have observed the original request, and that
|
|
83
|
+
// is not caller misuse. Note this guard's `activeAssignment` slot is
|
|
84
|
+
// session-scoped (see the comment above), so the `taskId` that loses
|
|
85
|
+
// here need not be the same one the slot is already held for — it is
|
|
86
|
+
// simply a second, separately-requested task competing for the one
|
|
87
|
+
// slot the session allows.
|
|
88
|
+
if (task) {
|
|
89
|
+
if (this.activeAssignment === undefined) {
|
|
90
|
+
task.assignee = record.assignee;
|
|
91
|
+
task.status = 'accepted';
|
|
92
|
+
this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
|
|
93
|
+
this.phase = 'InProgress';
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.recordAnomaly({
|
|
97
|
+
kind: 'duplicate_task_accept',
|
|
98
|
+
mode: envelope.mode,
|
|
99
|
+
messageType: envelope.messageType,
|
|
100
|
+
messageId: envelope.messageId,
|
|
101
|
+
sender: envelope.sender,
|
|
102
|
+
subjectId: record.taskId,
|
|
103
|
+
detail: `sender ${envelope.sender} attempted TaskAccept for ${record.taskId}, but ${this.activeAssignment.sender} already holds the session's one assignee slot with task ${this.activeAssignment.taskId}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
135
106
|
}
|
|
107
|
+
// unknown taskId (task undefined): stays a silent no-op.
|
|
136
108
|
break;
|
|
137
109
|
}
|
|
138
110
|
case 'TaskReject': {
|
|
@@ -186,52 +158,39 @@ class TaskProjection {
|
|
|
186
158
|
if (task) {
|
|
187
159
|
task.status = 'completed';
|
|
188
160
|
task.progress = 1;
|
|
161
|
+
this.phase = 'Completed';
|
|
189
162
|
}
|
|
190
|
-
this.phase = 'Completed';
|
|
191
163
|
break;
|
|
192
164
|
}
|
|
193
165
|
case 'TaskFail': {
|
|
194
166
|
const record = payload;
|
|
195
167
|
this.failures.push({ ...record, retryable: record.retryable ?? false, sender: envelope.sender });
|
|
196
168
|
const task = this.tasks.get(record.taskId);
|
|
197
|
-
if (task)
|
|
169
|
+
if (task) {
|
|
198
170
|
task.status = 'failed';
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
case 'Commitment': {
|
|
203
|
-
this.commitment = payload;
|
|
204
|
-
this.phase = 'Committed';
|
|
171
|
+
this.phase = 'Failed';
|
|
172
|
+
}
|
|
205
173
|
break;
|
|
206
174
|
}
|
|
207
175
|
default:
|
|
208
176
|
break;
|
|
209
177
|
}
|
|
210
178
|
}
|
|
211
|
-
/**
|
|
212
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
213
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
214
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
215
|
-
*/
|
|
216
|
-
get hasAnomalies() {
|
|
217
|
-
return this.anomalies.length > 0;
|
|
218
|
-
}
|
|
219
|
-
get isCommitted() {
|
|
220
|
-
return this.commitment !== undefined;
|
|
221
|
-
}
|
|
222
|
-
get isPositiveOutcome() {
|
|
223
|
-
if (!this.commitment)
|
|
224
|
-
return undefined;
|
|
225
|
-
const val = this.commitment.outcomePositive ??
|
|
226
|
-
this.commitment.outcome_positive;
|
|
227
|
-
return val !== undefined ? Boolean(val) : true;
|
|
228
|
-
}
|
|
229
179
|
getTask(taskId) {
|
|
230
180
|
return this.tasks.get(taskId);
|
|
231
181
|
}
|
|
232
|
-
|
|
182
|
+
isCompleted(taskId) {
|
|
233
183
|
return this.tasks.get(taskId)?.status === 'completed';
|
|
234
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* @deprecated Use {@link isCompleted}. Renamed for cross-SDK naming parity
|
|
187
|
+
* (multiagentcoordinationprotocol#135, issue #138) — every other `is*`
|
|
188
|
+
* predicate in this SDK is past-participle, and `macp-sdk-python` has
|
|
189
|
+
* always spelled this `is_completed`. Scheduled for removal in 0.13.0.
|
|
190
|
+
*/
|
|
191
|
+
isComplete(taskId) {
|
|
192
|
+
return this.isCompleted(taskId);
|
|
193
|
+
}
|
|
235
194
|
isFailed(taskId) {
|
|
236
195
|
return this.tasks.get(taskId)?.status === 'failed';
|
|
237
196
|
}
|
package/dist/proposal.d.ts
CHANGED
package/dist/proposal.js
CHANGED
|
@@ -60,10 +60,10 @@ class ProposalSession {
|
|
|
60
60
|
mode: constants_1.MODE_PROPOSAL,
|
|
61
61
|
messageType: 'SessionStart',
|
|
62
62
|
sessionId: this.sessionId,
|
|
63
|
-
sender: this.senderFor(input.sender),
|
|
63
|
+
sender: this.senderFor(input.sender, input.auth),
|
|
64
64
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_PROPOSAL, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
|
|
65
65
|
});
|
|
66
|
-
return this.sendAndTrack(envelope, this.auth);
|
|
66
|
+
return this.sendAndTrack(envelope, input.auth ?? this.auth);
|
|
67
67
|
}
|
|
68
68
|
async propose(input) {
|
|
69
69
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
package/dist/proto-registry.d.ts
CHANGED
|
@@ -10,13 +10,93 @@ 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 }`, but only the protobuf path's `value` is always
|
|
15
|
+
* a `string` — the legacy-JSON path passes `parsed.value` through exactly
|
|
16
|
+
* as `JSON.parse` produced it (issue #124: no `String(...)` coercion, no
|
|
17
|
+
* `?? ''` fallback for an absent key), so `value` may be `undefined`,
|
|
18
|
+
* `null`, a number, a boolean, or an object/array for malformed-but-valid
|
|
19
|
+
* legacy JSON. This method's own contract is observational, not a gate —
|
|
20
|
+
* see the `contribute_acceptance` note below.
|
|
21
|
+
*
|
|
22
|
+
* Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
|
|
23
|
+
* attempted first, and only a `JSON.parse` failure falls through to
|
|
24
|
+
* protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
|
|
25
|
+
* not generalize — e.g. legacy JSON with leading whitespace has neither byte
|
|
26
|
+
* first, and used to reach `decodeMessage` and throw. Parity with
|
|
27
|
+
* `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
|
|
28
|
+
* worked this way.
|
|
29
|
+
*
|
|
30
|
+
* **A successful `JSON.parse` is not proof the bytes are legacy JSON**
|
|
31
|
+
* (issue #104 — this method's prior "essentially never also syntactically
|
|
32
|
+
* valid JSON" claim was false, not merely imprecise): the canonical proto
|
|
33
|
+
* tag byte for field 1 (`0x0A`) is itself insignificant JSON whitespace, so
|
|
34
|
+
* for specific value byte-lengths the length varint — or, when the varint
|
|
35
|
+
* is itself whitespace, the value's own leading byte — becomes the first
|
|
36
|
+
* significant character `JSON.parse` sees, and a genuine `ContributePayload`
|
|
37
|
+
* silently misreads as a JSON number/string/object. Verified directly
|
|
38
|
+
* (encode-then-decode sweep, value lengths 1-127, seven value shapes): every
|
|
39
|
+
* one of lengths 9, 10, 13, 32, 34, 45, 48, 49-57, and 123 corrupted before
|
|
40
|
+
* `isCanonicalProto` below, most to total silent data loss
|
|
41
|
+
* (`String(undefined ?? '') === ''`), zero corrupt after.
|
|
42
|
+
*
|
|
43
|
+
* `isCanonicalProto` resolves the ambiguity by tie-break, not by refusing
|
|
44
|
+
* to try JSON first: bytes that parse as JSON *and* are the exact canonical
|
|
45
|
+
* proto encoding are treated as proto. This has a narrow, priced cost on
|
|
46
|
+
* the reverse direction (legacy JSON misread as proto) that it introduces
|
|
47
|
+
* rather than merely fails to close — see `isCanonicalProto`'s own
|
|
48
|
+
* docblock.
|
|
49
|
+
*
|
|
50
|
+
* An empty payload falls through the same path: `JSON.parse('')` throws, so
|
|
51
|
+
* it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
|
|
52
|
+
* defaults) rather than a decode error. This SDK's decode layer is
|
|
53
|
+
* observational, not an acceptance gate — whether an empty `Contribute`
|
|
54
|
+
* should be rejected is a runtime-acceptance question (tracked cross-repo,
|
|
55
|
+
* `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
|
|
56
|
+
* currently `macp-runtime`-only), not something this method decides.
|
|
18
57
|
*/
|
|
19
58
|
private decodeMultiRoundContribute;
|
|
59
|
+
/**
|
|
60
|
+
* True iff `payload` is the exact canonical protobuf encoding of
|
|
61
|
+
* `typeName` — used to break the JSON/proto ambiguity in
|
|
62
|
+
* `decodeMultiRoundContribute` (issue #104): bytes that both parse as JSON
|
|
63
|
+
* *and* round-trip byte-identically through the proto message are treated
|
|
64
|
+
* as proto, not JSON.
|
|
65
|
+
*
|
|
66
|
+
* `macp-sdk-python`'s equivalent (`_is_canonical_proto`) needs an explicit
|
|
67
|
+
* `DiscardUnknownFields()` step before comparing, because Python's protobuf
|
|
68
|
+
* runtime preserves unknown fields verbatim through
|
|
69
|
+
* `ParseFromString`/`SerializeToString` by default — without it, a plain
|
|
70
|
+
* round-trip check misclassified several whitespace-prefixed legacy-JSON
|
|
71
|
+
* payloads as canonical proto. **`protobufjs` needs no equivalent step**:
|
|
72
|
+
* verified directly (a synthetic payload with a declared field-1 string
|
|
73
|
+
* plus an undeclared field 2) that `Type.decode()` silently drops fields
|
|
74
|
+
* not in the schema rather than preserving them, so `Type.encode()` can
|
|
75
|
+
* never reproduce bytes it never captured — a byte-exact round-trip here
|
|
76
|
+
* is already proof the payload is *exactly* the single-field encoding, no
|
|
77
|
+
* unknown fields possible. Confirmed this closes the same
|
|
78
|
+
* whitespace-prefixed false-positive class `DiscardUnknownFields` closes
|
|
79
|
+
* in Python (tab/space-prefixed legacy JSON at several lengths).
|
|
80
|
+
*
|
|
81
|
+
* One narrow, symmetric residual survives regardless — genuinely
|
|
82
|
+
* irreducible, not a gap this check merely fails to close: a payload whose
|
|
83
|
+
* first byte is a literal `0x0A` (`\n`) and whose remainder forms a
|
|
84
|
+
* complete, well-formed proto field-1 string with nothing left over has no
|
|
85
|
+
* unknown field for either implementation to expose a mismatch on, because
|
|
86
|
+
* it *is*, byte for byte, both a legal JSON reading and the canonical
|
|
87
|
+
* proto encoding of some (possibly nonsensical) string. Verified instances
|
|
88
|
+
* (this SDK's own `JSON.stringify`, which — unlike Python's `json.dumps`
|
|
89
|
+
* — omits the space after `:`, shifting the colliding lengths by one byte
|
|
90
|
+
* from `macp-sdk-python`'s pinned cases): `'\n' + JSON.stringify({value:
|
|
91
|
+
* 'z'.repeat(112)})`, and with one extra whitespace byte inserted after the
|
|
92
|
+
* `\n`, `'\r'` at value length 1 and `' '` at value length 20. A real
|
|
93
|
+
* legacy-JSON sender would need to deliberately prefix with a literal
|
|
94
|
+
* newline byte for this to matter — this SDK's own JSON producers never
|
|
95
|
+
* emit one — so the trade (fixing the ordinary, naturally-reachable
|
|
96
|
+
* forward-direction corruption above at the cost of this adversarial,
|
|
97
|
+
* unreachable-by-any-known-encoder reverse case) is the right one.
|
|
98
|
+
*/
|
|
99
|
+
private isCanonicalProto;
|
|
20
100
|
private lookupType;
|
|
21
101
|
private tryDecodeUtf8;
|
|
22
102
|
}
|