@axorum/mcp 0.0.0-stage → 1.0.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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Turning what the ledger said into what the model reads.
3
+ *
4
+ * # The one idea in this package
5
+ *
6
+ * **A refusal is a result, not an error.**
7
+ *
8
+ * When the policy in force forbids an action, the intent still reaches the commit point,
9
+ * is still judged, and the refusal is still *recorded*. The ledger answers `200`. The
10
+ * client resolves. And this module renders it as an ordinary tool result with `isError`
11
+ * **unset** — because that is what it is. The model reads "Refused", learns why, and
12
+ * adapts: it proposes something else, or it tells its human that the policy will not allow
13
+ * this and a human has to decide. That loop is the entire product. It is why it is safe to
14
+ * hand a language model money-moving tools at all.
15
+ *
16
+ * Setting `isError` on a refusal would break that loop in a specific and bad way. `isError`
17
+ * is the channel that means *the tool malfunctioned* — and a well-built agent's response to
18
+ * a malfunction is to retry. So a refusal dressed as an error invites the model to retry
19
+ * the exact thing the policy just forbade, in a loop, and to report to its human that
20
+ * Axorum is broken rather than that the spend was denied. The refusal is the most valuable
21
+ * thing this ledger produces. It goes in the `content`, in plain language, where the model
22
+ * will read it.
23
+ *
24
+ * {@link renderError} is for the cases where the ledger genuinely never got to answer: the
25
+ * process is misconfigured, the wire broke, the pin would not hold. Those are malfunctions,
26
+ * they set `isError`, and they carry the machine-readable `code` and the load-bearing
27
+ * extras a caller needs to act.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ import { type AgentOutcome, type CurrencyWarning, type UsageBalanceReport } from "@axorum/client";
32
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
+ /**
34
+ * The headline sentence for a verdict — the first thing the model reads, and often the
35
+ * only thing it needs.
36
+ *
37
+ * Every line leads with whether money moved, because that is the question. The five
38
+ * verdicts are not five shades of the same thing:
39
+ *
40
+ * - `Permitted` — allowed, and posted.
41
+ * - `ObligatedFulfilled` — allowed, posted, and it discharged a duty.
42
+ * - `ObligatedPending` — allowed, posted, and it *created* a duty that is now owed.
43
+ * - `ForbiddenRecorded` — forbidden, **but posted anyway**, and the violation is on the
44
+ * ledger. This is the "record, don't block" policy stance — a real one, and the reason
45
+ * `posted` is not simply the inverse of "forbidden".
46
+ * - `ForbiddenRejected` — forbidden, and **nothing posted**. The recorded refusal.
47
+ */
48
+ export declare function verdictLine(outcome: AgentOutcome): string;
49
+ /**
50
+ * The full result for a submitted intent: the verdict line, then the structured outcome.
51
+ *
52
+ * Two content blocks, deliberately. The first is prose a model acts on without parsing
53
+ * anything; the second is the record — verdict, policy, transaction id, obligations,
54
+ * provenance — for when it needs a detail or wants to quote one to a human. Recorded
55
+ * provenance preserves the optional evaluation-semantics stamp; archived and
56
+ * pre-versioning records omit it. `isError` is **never** set here: every value of
57
+ * `AgentOutcome`, refusal included, is the ledger answering the question it was asked.
58
+ */
59
+ export declare function renderOutcome(outcome: AgentOutcome): CallToolResult;
60
+ /**
61
+ * Whether a stored transaction record says the entries posted — or `undefined` if it does
62
+ * not say in a shape this server recognises.
63
+ *
64
+ * The record is **contractually opaque** in this version of the wire contract: its shape is
65
+ * not frozen, and this server must not pretend to understand it. But one thing *is*
66
+ * guaranteed — a committed transaction's record carries a `posted` boolean, and a recorded
67
+ * refusal appears with `posted: false`. That single fact is the one a model reading a
68
+ * record back actually needs, so it is worth digging for.
69
+ *
70
+ * So this looks in the two places it can honestly look — the top level, and one level in
71
+ * under an externally tagged key (the record arrives today as `{"Transaction": {…}}`) — and
72
+ * gives up rather than guessing if it finds neither. A caller that gets `undefined` still
73
+ * gets the whole record; it just does not get a headline about it.
74
+ */
75
+ export declare function postedFlag(record: unknown): boolean | undefined;
76
+ /**
77
+ * The headline for a transaction read back off the ledger.
78
+ *
79
+ * A refusal found here is the audit trail doing its job — the ledger judged an intent,
80
+ * forbade it, and *remembered* doing so. Saying that plainly matters, because a model that
81
+ * reads back a record and sees only a wall of JSON may well conclude the lookup failed.
82
+ */
83
+ export declare function describeRecord(transaction: string, record: unknown): string;
84
+ /**
85
+ * What an agent reads about a declared currency the policy in force refuses some action in:
86
+ * which actions, that it cannot change it, and the remedy to hand a human.
87
+ */
88
+ export declare function currencyWarningSentence(currency: string, warning: CurrencyWarning): string;
89
+ /**
90
+ * The headline for a usage balance read back off the ledger.
91
+ *
92
+ * **Leads with the service's own sentence.** `report.text` is the balance narrated in
93
+ * compliance English with exact figures — the ledger's own words, not a rendering this
94
+ * server assembles from the counters. Restating it from the raw numbers would be a second
95
+ * source of truth for a compliance figure, and a chance to disagree with the first.
96
+ *
97
+ * What this adds is the part the narration cannot know: what the model should *do*. A
98
+ * balance's headroom is exactly what bounds its next lawful move, so it is worth saying
99
+ * plainly rather than leaving a model to derive it by subtracting two figures out of the
100
+ * JSON.
101
+ *
102
+ * # Two things this prose must not get wrong
103
+ *
104
+ * **A headroom bounds a POSITIVE move, and only a positive one.** `UsageBalance::admits`
105
+ * checks the *post-state*, so a **negative** delta — a credit memo, a metering fix — is
106
+ * admitted whenever the chain still holds afterwards and no counter would go below zero.
107
+ * From a fully reconciled `1000/1000/1000`, `bill_usage: -500` is lawful and posts. Telling
108
+ * a model that a reconciled balance refuses every move until fresh consumption arrives
109
+ * would be false, and would talk it out of a correction the ledger would have accepted —
110
+ * a correction this server's own input schema advertises.
111
+ *
112
+ * **The headrooms are bounds against the balance as it STANDS, and they move together
113
+ * inside one intent.** The moves of a single intent are judged against their *combined*
114
+ * post-state, so a meter move in the same intent raises what may be billed in it: at
115
+ * `10000/6500/4000`, `meter_usage: +3500` with `bill_usage: +6000` is admitted, even though
116
+ * the standing bill headroom is only `2500`. Stating the standing figure as a hard per-intent
117
+ * ceiling would understate the lawful bound and make the model split one commit into two.
118
+ */
119
+ export declare function describeUsage(report: UsageBalanceReport): string;
120
+ /** A plain successful result: a line of prose, then the structured payload. */
121
+ export declare function renderResult(summary: string, payload: unknown): CallToolResult;
122
+ /** A successful result that is prose only — nothing structured worth showing. */
123
+ export declare function renderText(summary: string): CallToolResult;
124
+ /**
125
+ * What the model was doing when the call failed.
126
+ *
127
+ * It changes what the failure *means*, so it cannot be inferred from the error: a read that
128
+ * fails wrote nothing because it never intended to, and telling a model that "the ledger did
129
+ * not judge your intent" when it merely asked for a balance invites it to worry about a
130
+ * posting that was never in flight. Every tool says which it is.
131
+ *
132
+ * A submission is `"submit"` when this server minted its transaction id, and `"resubmit"`
133
+ * when the model supplied one. The difference is what a refusal can prove. `submit_intent`
134
+ * sends a minted id once (re-sent only to re-pin, after a `409` that recorded nothing), so an
135
+ * answer that recorded nothing proves the id holds nothing. A supplied id may have been sent
136
+ * by an earlier call that never heard its answer, and that call may still commit: the same
137
+ * answer then speaks for this attempt only, never for the id.
138
+ */
139
+ export type Operation = "submit" | "resubmit" | "read";
140
+ /**
141
+ * A genuine failure: the ledger never got to answer, or this process is misconfigured.
142
+ *
143
+ * `isError: true`, which tells the model the *tool* malfunctioned rather than that the
144
+ * *spend* was denied — see the module docs on why that distinction is load-bearing.
145
+ *
146
+ * The content carries the machine-readable `code` and the **load-bearing extras**. Those
147
+ * extras are not decoration: without `active` no caller can re-pin, and without
148
+ * `primary_api_addr` none can say which replica the follower believed was primary. `@axorum/client` is hand-written
149
+ * precisely so they survive, and this server would be throwing that away if it rendered a
150
+ * bare message.
151
+ *
152
+ * `operation` is **required** rather than defaulted: a default would silently render a
153
+ * failed read in submit-shaped prose, which is exactly the bug it exists to prevent.
154
+ */
155
+ export declare function renderError(error: unknown, operation: Operation): CallToolResult;
156
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAUL,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAKzE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmDzD;AA2BD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,cAAc,CAOnE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAa/D;AAOD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAa3E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAO1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAsChE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,CAO9E;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAE1D;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;AAkDvD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,cAAc,CAuBhF"}
package/dist/render.js ADDED
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Turning what the ledger said into what the model reads.
3
+ *
4
+ * # The one idea in this package
5
+ *
6
+ * **A refusal is a result, not an error.**
7
+ *
8
+ * When the policy in force forbids an action, the intent still reaches the commit point,
9
+ * is still judged, and the refusal is still *recorded*. The ledger answers `200`. The
10
+ * client resolves. And this module renders it as an ordinary tool result with `isError`
11
+ * **unset** — because that is what it is. The model reads "Refused", learns why, and
12
+ * adapts: it proposes something else, or it tells its human that the policy will not allow
13
+ * this and a human has to decide. That loop is the entire product. It is why it is safe to
14
+ * hand a language model money-moving tools at all.
15
+ *
16
+ * Setting `isError` on a refusal would break that loop in a specific and bad way. `isError`
17
+ * is the channel that means *the tool malfunctioned* — and a well-built agent's response to
18
+ * a malfunction is to retry. So a refusal dressed as an error invites the model to retry
19
+ * the exact thing the policy just forbade, in a loop, and to report to its human that
20
+ * Axorum is broken rather than that the spend was denied. The refusal is the most valuable
21
+ * thing this ledger produces. It goes in the `content`, in plain language, where the model
22
+ * will read it.
23
+ *
24
+ * {@link renderError} is for the cases where the ledger genuinely never got to answer: the
25
+ * process is misconfigured, the wire broke, the pin would not hold. Those are malfunctions,
26
+ * they set `isError`, and they carry the machine-readable `code` and the load-bearing
27
+ * extras a caller needs to act.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ import { ApiError, AttestationError, AxorumError, CommitUnavailableError, NotPrimaryError, StalePolicyError, TransactionReusedError, UnknownAgentError, refusalOf, } from "@axorum/client";
32
+ import { ConfigurationError } from "./config.js";
33
+ import { toJsonText } from "./json.js";
34
+ /**
35
+ * The headline sentence for a verdict — the first thing the model reads, and often the
36
+ * only thing it needs.
37
+ *
38
+ * Every line leads with whether money moved, because that is the question. The five
39
+ * verdicts are not five shades of the same thing:
40
+ *
41
+ * - `Permitted` — allowed, and posted.
42
+ * - `ObligatedFulfilled` — allowed, posted, and it discharged a duty.
43
+ * - `ObligatedPending` — allowed, posted, and it *created* a duty that is now owed.
44
+ * - `ForbiddenRecorded` — forbidden, **but posted anyway**, and the violation is on the
45
+ * ledger. This is the "record, don't block" policy stance — a real one, and the reason
46
+ * `posted` is not simply the inverse of "forbidden".
47
+ * - `ForbiddenRejected` — forbidden, and **nothing posted**. The recorded refusal.
48
+ */
49
+ export function verdictLine(outcome) {
50
+ switch (outcome.verdict) {
51
+ case "Permitted":
52
+ return "Posted. Verdict: Permitted — the policy in force allows this action, and the entries were posted to the ledger.";
53
+ case "ObligatedFulfilled":
54
+ return "Posted. Verdict: ObligatedFulfilled — the entries were posted, and this action discharged an explicitly selected duty the actor was authorized to perform.";
55
+ case "ObligatedPending":
56
+ return ("Posted. Verdict: ObligatedPending — the entries were posted, and this action leaves " +
57
+ `${describeDuties(outcome.obligations_pending.length)} open. ` +
58
+ "Something now has to happen to discharge them; check get_obligations.");
59
+ case "ForbiddenRecorded":
60
+ return ("Posted, and recorded as a violation. Verdict: ForbiddenRecorded — the policy in force " +
61
+ "forbids this action, but the policy's stance is to record rather than block, so the " +
62
+ "entries WERE posted and the violation is on the ledger. Tell your human: this went " +
63
+ "through, and it went through against policy.");
64
+ case "ForbiddenRejected": {
65
+ // The server's reason (contract 5.0.0) names the rule that fired, states the exact
66
+ // figures its guard compared (with the headroom, the largest amount that would pass),
67
+ // and says what to do next; a server older than that sends none, so fall back to the
68
+ // general sentence. The structured conditions and figures ride in the second content
69
+ // block, every amount an exact decimal string (see toJsonText), so a model that wants
70
+ // to retry at the headroom reads the number rather than parsing prose.
71
+ const refusal = refusalOf(outcome);
72
+ const why = refusal
73
+ ? `Why (${refusal.code}): ${refusal.explanation}`
74
+ : "The policy in force forbids this action.";
75
+ return (`Refused — nothing posted. ${why} The refusal itself is now recorded on the ledger. ` +
76
+ "This is a verdict, not a failure: the system worked exactly as intended. Do NOT " +
77
+ "retry this unchanged — it will be refused again. Either propose an action the policy " +
78
+ "allows, or tell your human why it was refused and that a human has to change the " +
79
+ "policy to permit it.");
80
+ }
81
+ default:
82
+ // The verdict set is closed in this version of the contract, but a newer service may
83
+ // widen it. Report the verdict verbatim rather than guessing at its meaning — and
84
+ // lead with `posted`, which is the fact that matters either way.
85
+ return outcome.posted
86
+ ? `Posted. Verdict: ${String(outcome.verdict)} — a verdict this server does not have prose for. The entries WERE posted.`
87
+ : `Nothing posted. Verdict: ${String(outcome.verdict)} — a verdict this server does not have prose for. The entries were NOT posted.`;
88
+ }
89
+ }
90
+ /**
91
+ * The sentence that leads a replayed answer (contract 5.0.0): the transaction was already
92
+ * on the ledger, so this is its original outcome and nothing new was written. Without it
93
+ * a model would read "Posted." and believe it had just spent the money a second time.
94
+ */
95
+ function replayPrefix(outcome) {
96
+ if (outcome.replayed !== true) {
97
+ return "";
98
+ }
99
+ const replay = `REPLAY: transaction ${outcome.transaction} was already on the ledger, so this is its ` +
100
+ "original outcome and NOTHING new was written. ";
101
+ return outcome.content_verified === false
102
+ ? replay +
103
+ "The ledger could not compare this request with the one it stored (the id was " +
104
+ "committed before 1.0.0), so if you changed the request, that change was NOT " +
105
+ "applied: submit it under a new transaction id. "
106
+ : replay;
107
+ }
108
+ /** `1 obligation` / `N obligations`, so the sentence above reads correctly either way. */
109
+ function describeDuties(count) {
110
+ return count === 1 ? "1 obligation" : `${count} obligations`;
111
+ }
112
+ /**
113
+ * The full result for a submitted intent: the verdict line, then the structured outcome.
114
+ *
115
+ * Two content blocks, deliberately. The first is prose a model acts on without parsing
116
+ * anything; the second is the record — verdict, policy, transaction id, obligations,
117
+ * provenance — for when it needs a detail or wants to quote one to a human. Recorded
118
+ * provenance preserves the optional evaluation-semantics stamp; archived and
119
+ * pre-versioning records omit it. `isError` is **never** set here: every value of
120
+ * `AgentOutcome`, refusal included, is the ledger answering the question it was asked.
121
+ */
122
+ export function renderOutcome(outcome) {
123
+ return {
124
+ content: [
125
+ { type: "text", text: replayPrefix(outcome) + verdictLine(outcome) },
126
+ { type: "text", text: toJsonText(outcome) },
127
+ ],
128
+ };
129
+ }
130
+ /**
131
+ * Whether a stored transaction record says the entries posted — or `undefined` if it does
132
+ * not say in a shape this server recognises.
133
+ *
134
+ * The record is **contractually opaque** in this version of the wire contract: its shape is
135
+ * not frozen, and this server must not pretend to understand it. But one thing *is*
136
+ * guaranteed — a committed transaction's record carries a `posted` boolean, and a recorded
137
+ * refusal appears with `posted: false`. That single fact is the one a model reading a
138
+ * record back actually needs, so it is worth digging for.
139
+ *
140
+ * So this looks in the two places it can honestly look — the top level, and one level in
141
+ * under an externally tagged key (the record arrives today as `{"Transaction": {…}}`) — and
142
+ * gives up rather than guessing if it finds neither. A caller that gets `undefined` still
143
+ * gets the whole record; it just does not get a headline about it.
144
+ */
145
+ export function postedFlag(record) {
146
+ if (!isObject(record)) {
147
+ return undefined;
148
+ }
149
+ if (typeof record["posted"] === "boolean") {
150
+ return record["posted"];
151
+ }
152
+ for (const value of Object.values(record)) {
153
+ if (isObject(value) && typeof value["posted"] === "boolean") {
154
+ return value["posted"];
155
+ }
156
+ }
157
+ return undefined;
158
+ }
159
+ /** Whether a value is a non-null, non-array object. */
160
+ function isObject(value) {
161
+ return typeof value === "object" && value !== null && !Array.isArray(value);
162
+ }
163
+ /**
164
+ * The headline for a transaction read back off the ledger.
165
+ *
166
+ * A refusal found here is the audit trail doing its job — the ledger judged an intent,
167
+ * forbade it, and *remembered* doing so. Saying that plainly matters, because a model that
168
+ * reads back a record and sees only a wall of JSON may well conclude the lookup failed.
169
+ */
170
+ export function describeRecord(transaction, record) {
171
+ switch (postedFlag(record)) {
172
+ case true:
173
+ return `Transaction '${transaction}' is on the ledger, and it POSTED — the entries were written.`;
174
+ case false:
175
+ return (`Transaction '${transaction}' is on the ledger, and it was REFUSED — nothing posted. ` +
176
+ "The refusal itself is the record: the ledger judged this intent, the policy forbade it, " +
177
+ "and the ledger remembered doing so. This lookup succeeded; it is telling you the answer was no.");
178
+ default:
179
+ return `The stored ledger record for transaction '${transaction}'.`;
180
+ }
181
+ }
182
+ /**
183
+ * What an agent reads about a declared currency the policy in force refuses some action in:
184
+ * which actions, that it cannot change it, and the remedy to hand a human.
185
+ */
186
+ export function currencyWarningSentence(currency, warning) {
187
+ const actions = warning.actions.map((action) => `'${action}'`).join(", ");
188
+ return (`The policy in force refuses ${actions} in ${currency}, whatever the intent carries. ` +
189
+ "You cannot change that: use a currency with no warning, or hand this remedy to a human " +
190
+ `operator: ${warning.remedy}.`);
191
+ }
192
+ /**
193
+ * The headline for a usage balance read back off the ledger.
194
+ *
195
+ * **Leads with the service's own sentence.** `report.text` is the balance narrated in
196
+ * compliance English with exact figures — the ledger's own words, not a rendering this
197
+ * server assembles from the counters. Restating it from the raw numbers would be a second
198
+ * source of truth for a compliance figure, and a chance to disagree with the first.
199
+ *
200
+ * What this adds is the part the narration cannot know: what the model should *do*. A
201
+ * balance's headroom is exactly what bounds its next lawful move, so it is worth saying
202
+ * plainly rather than leaving a model to derive it by subtracting two figures out of the
203
+ * JSON.
204
+ *
205
+ * # Two things this prose must not get wrong
206
+ *
207
+ * **A headroom bounds a POSITIVE move, and only a positive one.** `UsageBalance::admits`
208
+ * checks the *post-state*, so a **negative** delta — a credit memo, a metering fix — is
209
+ * admitted whenever the chain still holds afterwards and no counter would go below zero.
210
+ * From a fully reconciled `1000/1000/1000`, `bill_usage: -500` is lawful and posts. Telling
211
+ * a model that a reconciled balance refuses every move until fresh consumption arrives
212
+ * would be false, and would talk it out of a correction the ledger would have accepted —
213
+ * a correction this server's own input schema advertises.
214
+ *
215
+ * **The headrooms are bounds against the balance as it STANDS, and they move together
216
+ * inside one intent.** The moves of a single intent are judged against their *combined*
217
+ * post-state, so a meter move in the same intent raises what may be billed in it: at
218
+ * `10000/6500/4000`, `meter_usage: +3500` with `bill_usage: +6000` is admitted, even though
219
+ * the standing bill headroom is only `2500`. Stating the standing figure as a hard per-intent
220
+ * ceiling would understate the lawful bound and make the model split one commit into two.
221
+ */
222
+ export function describeUsage(report) {
223
+ // A balance that has never been moved is `reconciled` too — vacuously, since 0 = 0 = 0.
224
+ // It is a different situation from a reconciled balance with real figures behind it, and
225
+ // the advice is different, so it is answered first. The "nothing consumed yet" statement
226
+ // is now carried by `report.text` itself — the ledger narrates the empty balance as unused —
227
+ // so this layer no longer restates it and adds only the actionable guidance.
228
+ if (report.consumed === 0n && report.metered === 0n && report.billed === 0n) {
229
+ return (`${report.text} Record consumption with \`consume_usage\` before you meter or bill ` +
230
+ "anything: there is nothing yet to measure or to charge for, so a positive meter or " +
231
+ "bill move against it would be refused.");
232
+ }
233
+ if (report.reconciled) {
234
+ return (`${report.text} This balance is fully reconciled: everything consumed has been metered, ` +
235
+ "and everything metered has been billed. There is no headroom for a POSITIVE move: " +
236
+ "metering or billing anything FURTHER would break the chain unless fresh consumption is " +
237
+ "recorded first. A NEGATIVE correction is a different matter and is still lawful — a " +
238
+ "credit memo or a metering fix may unwind up to " +
239
+ `${report.billed.toString()} minor units of billing — because the ledger checks the state ` +
240
+ "an intent would LEAVE BEHIND, not the state it found. It is admitted as long as no " +
241
+ "counter would go below zero and the chain still holds afterwards.");
242
+ }
243
+ return (`${report.text} The chain has NOT reconciled. Against the balance as it stands you may ` +
244
+ `meter up to ${report.unmetered.toString()} more minor units of consumption already ` +
245
+ `recorded, and bill up to ${report.unbilled.toString()} more minor units of usage already ` +
246
+ "metered. Those two bounds move together inside a single intent: its moves are judged " +
247
+ "against their combined post-state, so metering more in the SAME intent raises what you " +
248
+ "may bill in it. What is never allowed is breaking the chain — metering more than has " +
249
+ "been consumed is phantom usage, and billing more than has been metered is billing with " +
250
+ "nothing to back it. A negative correction is lawful here too, so long as no counter " +
251
+ "would go below zero.");
252
+ }
253
+ /** A plain successful result: a line of prose, then the structured payload. */
254
+ export function renderResult(summary, payload) {
255
+ return {
256
+ content: [
257
+ { type: "text", text: summary },
258
+ { type: "text", text: toJsonText(payload) },
259
+ ],
260
+ };
261
+ }
262
+ /** A successful result that is prose only — nothing structured worth showing. */
263
+ export function renderText(summary) {
264
+ return { content: [{ type: "text", text: summary }] };
265
+ }
266
+ /** Whether `operation` is a submission, whoever minted its transaction id. Pure. */
267
+ function submits(operation) {
268
+ return operation !== "read";
269
+ }
270
+ /**
271
+ * What the ledger did not do, when it got the call but refused it. For a resubmission,
272
+ * only what this attempt did not do: see {@link Operation}.
273
+ */
274
+ function nothingHappened(operation) {
275
+ switch (operation) {
276
+ case "submit":
277
+ return "NOTHING was recorded.";
278
+ case "resubmit":
279
+ return ("This attempt recorded nothing, but your intent MAY OR MAY NOT have been recorded: you " +
280
+ "supplied its transaction id, so an earlier submission under that id may have landed or " +
281
+ "may still commit. Do not assume it failed: read it back with get_transaction using the " +
282
+ "transaction id you supplied, and never submit it again under a new id, or it may post twice.");
283
+ case "read":
284
+ return "Nothing was read, and nothing was written — a read never writes.";
285
+ }
286
+ }
287
+ /**
288
+ * What a `421` leaves of a submission: a primary stepping down can turn the write away
289
+ * after proposing it, so it may still land, whoever minted the transaction id. A read
290
+ * writes nothing either way.
291
+ */
292
+ function turnedAway(operation) {
293
+ return submits(operation)
294
+ ? "Your intent MAY OR MAY NOT have been recorded: a primary stepping down can turn a " +
295
+ "write away after proposing it."
296
+ : nothingHappened(operation);
297
+ }
298
+ /**
299
+ * What did not happen when the call never reached the ledger at all.
300
+ *
301
+ * Distinct from {@link nothingHappened}: a misconfigured server and an unexpected throw are
302
+ * failures *before* the wire, so the honest thing to say is that the request never left,
303
+ * not that the ledger declined to record it.
304
+ */
305
+ function nothingLeftTheProcess(operation) {
306
+ return submits(operation) ? "Nothing was submitted." : "Nothing was read.";
307
+ }
308
+ /**
309
+ * A genuine failure: the ledger never got to answer, or this process is misconfigured.
310
+ *
311
+ * `isError: true`, which tells the model the *tool* malfunctioned rather than that the
312
+ * *spend* was denied — see the module docs on why that distinction is load-bearing.
313
+ *
314
+ * The content carries the machine-readable `code` and the **load-bearing extras**. Those
315
+ * extras are not decoration: without `active` no caller can re-pin, and without
316
+ * `primary_api_addr` none can say which replica the follower believed was primary. `@axorum/client` is hand-written
317
+ * precisely so they survive, and this server would be throwing that away if it rendered a
318
+ * bare message.
319
+ *
320
+ * `operation` is **required** rather than defaulted: a default would silently render a
321
+ * failed read in submit-shaped prose, which is exactly the bug it exists to prevent.
322
+ */
323
+ export function renderError(error, operation) {
324
+ if (error instanceof ConfigurationError) {
325
+ return failure(`This Axorum MCP server is misconfigured, so it cannot talk to the ledger at all. ` +
326
+ `${nothingLeftTheProcess(operation)} This is not something you can fix by retrying or ` +
327
+ `by changing your request — a human has to fix the server's environment. ` +
328
+ `Tell them: ${error.message}`, { code: "configuration", detail: error.message });
329
+ }
330
+ if (error instanceof AxorumError) {
331
+ return failure(`${advice(error, operation)} (error code: ${error.code})`, details(error));
332
+ }
333
+ // Not ours, and not the ledger's — a bug here, or something the runtime threw. Say so
334
+ // plainly rather than dressing it up as a ledger verdict.
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ return failure(`The Axorum MCP server hit an unexpected failure and could not complete the call. ` +
337
+ `${nothingLeftTheProcess(operation)} Report this to a human: ${message}`, { code: "unexpected", detail: message });
338
+ }
339
+ /** Prose telling the model what this failure means and what to do about it. */
340
+ function advice(error, operation) {
341
+ const preamble = submits(operation)
342
+ ? "The ledger did not judge your intent"
343
+ : "The ledger did not answer your read";
344
+ if (error instanceof NotPrimaryError) {
345
+ return (`${preamble}: this replica is not the cluster's primary, so it turned the write away. ` +
346
+ `${turnedAway(operation)} ` +
347
+ (error.primaryApiAddr === null
348
+ ? "It does not know which replica is the primary. "
349
+ : `It named '${error.primaryApiAddr}' as the primary's API origin, a hint that may be stale. `) +
350
+ "Do NOT try to follow that address yourself and do NOT retry — a replica turning writes " +
351
+ "away is a topology change, and a human operator needs to see it. Report it and stop.");
352
+ }
353
+ if (error instanceof StalePolicyError) {
354
+ return (`${preamble}: the policy moved while the intent was in flight, and it kept moving — this ` +
355
+ `server re-pinned and resubmitted, and the policy changed again each time. ` +
356
+ `${nothingHappened(operation)} The policy in force was last seen as '${error.active}'. A policy rotating this ` +
357
+ "fast is an operational event, not something to retry into. Report it to a human.");
358
+ }
359
+ if (error instanceof TransactionReusedError) {
360
+ const refused = `${preamble}: the transaction id was already used for a DIFFERENT intent, so it was ` +
361
+ "refused and NOTHING was recorded. A transaction id names exactly one intent. For a new " +
362
+ "intent, submit again WITHOUT a transaction id and one is minted for you.";
363
+ // No hashes means the id was committed by another party: there is nothing of
364
+ // this agent's to read back under it.
365
+ return error.storedContentHash === undefined
366
+ ? `${refused} The id belongs to a transaction another party committed.`
367
+ : `${refused} To read what the original transaction did, use get_transaction with ` +
368
+ "the id below.";
369
+ }
370
+ if (error instanceof CommitUnavailableError) {
371
+ return ("The cluster could not confirm the commit, so your intent MAY OR MAY NOT have been " +
372
+ "recorded — this is the one genuinely ambiguous outcome. Do not assume it failed and do " +
373
+ "not assume it succeeded. Read it back with get_transaction using the transaction id " +
374
+ "below; if it is there, it landed. Retrying the identical intent under the same " +
375
+ "transaction id is also safe: the id is the ledger's idempotency key, so a retry replays " +
376
+ "the stored outcome rather than posting twice.");
377
+ }
378
+ if (error instanceof UnknownAgentError) {
379
+ return (`${preamble}: the ledger does not recognise the agent identity this server acts under, so ` +
380
+ `it was refused at the door. ${nothingHappened(operation)} This is a setup problem, not ` +
381
+ "something your request can fix — a human has to bind this agent's URI to a party in the " +
382
+ "ledger's registry. Report it and stop.");
383
+ }
384
+ if (error instanceof AttestationError) {
385
+ return (`${preamble}: the ledger rejected this server's capability attestation — it is expired, ` +
386
+ "signed by an issuer the ledger does not trust, or does not grant the capability this " +
387
+ `action needs. ${nothingHappened(operation)} A human has to mint a valid attestation; ` +
388
+ "retrying will not help. Report it and stop.");
389
+ }
390
+ if (error.body?.cause === "undeclared_currency") {
391
+ const codes = error.body.currencies ?? [];
392
+ const named = codes.length > 0 ? codes.map((code) => `'${code}'`).join(", ") : "a currency";
393
+ return (`${preamble}: it names ${named}, which this ledger's operator has not declared, so the ` +
394
+ `ledger cannot read its amounts. ${nothingHappened(operation)} You cannot declare a ` +
395
+ "currency, and retrying will not help. Either use a currency the ledger already holds, " +
396
+ "or report this to a human operator with the command the ledger gave: " +
397
+ `${error.message}`);
398
+ }
399
+ if (submits(operation) && error.code === "idempotency_seed_refused" && error.wroteNothing) {
400
+ return (`${preamble}: the ledger refused the primary's idempotency seed, so the primary turns ` +
401
+ `every submission away before proposing it, until an operator fails it over. ` +
402
+ `${nothingHappened(operation)} Retrying will not help until then, under this id or any ` +
403
+ `other. Report it to a human operator: ${error.message}`);
404
+ }
405
+ if (error.code === "quarantined" && error.status === 503) {
406
+ return (`${preamble}: this ledger node found a divergence when it verified its own history, and ` +
407
+ `refuses every request until an operator resolves it. ${nothingHappened(operation)} ` +
408
+ `Retrying will not help until then. Report it to a human operator: ${error.message}`);
409
+ }
410
+ // An answer that recorded nothing says so, for a minted id as for the id alone; for a
411
+ // supplied one it speaks for this attempt only (see `Operation`).
412
+ const proven = submits(operation) && error.wroteNothing ? ` ${nothingHappened(operation)}` : "";
413
+ if (error.isRetriable) {
414
+ return submits(operation)
415
+ ? `${preamble}: ${error.message}${proven} Retrying is safe — the transaction id is the ` +
416
+ "ledger's idempotency key, so a retry either lands it once or replays the outcome it " +
417
+ "already has. It cannot double-post."
418
+ : `${preamble}: ${error.message} Retrying is safe — a read changes nothing, whether it ` +
419
+ "succeeds or fails.";
420
+ }
421
+ return submits(operation)
422
+ ? `${preamble}: ${error.message}${proven} Retrying this unchanged will not help.`
423
+ : `${preamble}: ${error.message} Retrying this unchanged will not help, and nothing was ` +
424
+ "written — a read never writes. If the id was refused as unknown, that may mean it does " +
425
+ "not exist OR that it is not yours to read; the ledger deliberately does not say which, " +
426
+ "so do not conclude it is absent.";
427
+ }
428
+ /**
429
+ * The structured detail for a failure, extras intact.
430
+ *
431
+ * Built by hand rather than by serializing the error object, because an `Error`'s own
432
+ * enumerable properties are not its interesting ones — the interesting ones are the typed
433
+ * fields the client lifted out of the wire body.
434
+ */
435
+ function details(error) {
436
+ const detail = {
437
+ code: error.code,
438
+ message: error.message,
439
+ status: error.status,
440
+ retriable: error.isRetriable,
441
+ requires_repin: error.requiresRepin,
442
+ };
443
+ if (error instanceof NotPrimaryError) {
444
+ detail["primary_api_addr"] = error.primaryApiAddr;
445
+ detail["reason"] = error.reason;
446
+ }
447
+ if (error instanceof StalePolicyError) {
448
+ detail["pinned"] = error.pinned;
449
+ detail["active"] = error.active;
450
+ }
451
+ if (error instanceof CommitUnavailableError) {
452
+ detail["reason"] = error.reason;
453
+ }
454
+ if (error instanceof ApiError && error.retryAfterMs !== null) {
455
+ detail["retry_after_ms"] = error.retryAfterMs;
456
+ }
457
+ if (error instanceof UnknownAgentError) {
458
+ detail["uri"] = error.uri;
459
+ }
460
+ if (error instanceof TransactionReusedError && error.storedContentHash !== undefined) {
461
+ detail["stored_content_hash"] = error.storedContentHash;
462
+ detail["submitted_content_hash"] = error.submittedContentHash;
463
+ }
464
+ // The service's own body, whenever there was one: it may carry extras a newer service
465
+ // invented that this client has no typed field for, and dropping them would be exactly
466
+ // the flattening `@axorum/client` exists to avoid.
467
+ if (error.body !== null) {
468
+ detail["body"] = error.body;
469
+ }
470
+ return detail;
471
+ }
472
+ /** An `isError` result: the prose, then the structured detail. */
473
+ function failure(summary, detail) {
474
+ return {
475
+ isError: true,
476
+ content: [
477
+ { type: "text", text: summary },
478
+ { type: "text", text: toJsonText(detail) },
479
+ ],
480
+ };
481
+ }
482
+ //# sourceMappingURL=render.js.map