@ixo/oracle-runtime-workers 0.29.1 → 0.31.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/package.json +7 -5
- package/src/attachments/channel-links.ts +135 -0
- package/src/attachments/channel.ts +203 -0
- package/src/attachments/download.ts +23 -6
- package/src/attachments/index.ts +23 -0
- package/src/attachments/pipeline.ts +120 -42
- package/src/attachments/retention.ts +11 -3
- package/src/attachments/sanitize.ts +20 -8
- package/src/attachments/types.ts +7 -0
- package/src/attachments/view.ts +24 -7
- package/src/channels/contract.ts +69 -0
- package/src/channels/turns.ts +23 -1
- package/src/core/main-agent-types.ts +2 -0
- package/src/core/runtime-context.ts +3 -0
- package/src/delivery/plan.ts +3 -1
- package/src/do/contracts.ts +3 -0
- package/src/do/turn-attachments.ts +103 -0
- package/src/do/user-oracle-do.ts +142 -42
- package/src/hub-actions/client.ts +87 -7
- package/src/hub-actions/hub-text.ts +82 -0
- package/src/hub-actions/request.ts +7 -4
- package/src/hub-actions/store.ts +5 -1
- package/src/hub-actions/transfer.ts +23 -6
- package/src/index.ts +1 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +43 -2
- package/src/plugins/attachments/view-attachment-tool.ts +7 -3
- package/src/plugins/hub-actions/hub-actions.plugin.ts +96 -17
- package/src/plugins/ixo-chain/balances.ts +141 -0
- package/src/plugins/ixo-chain/blocksync.ts +17 -8
- package/src/plugins/ixo-chain/denoms.ts +84 -36
- package/src/plugins/ixo-chain/http.ts +315 -70
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +182 -301
- package/src/plugins/ixo-chain/kinds/chain/account.ts +516 -0
- package/src/plugins/ixo-chain/kinds/chain/claims.ts +287 -0
- package/src/plugins/ixo-chain/kinds/chain/economics.ts +237 -0
- package/src/plugins/ixo-chain/kinds/chain/governance.ts +455 -0
- package/src/plugins/ixo-chain/kinds/chain/grants.ts +682 -0
- package/src/plugins/ixo-chain/kinds/chain/index.ts +23 -0
- package/src/plugins/ixo-chain/kinds/chain/liquid-staking.ts +279 -0
- package/src/plugins/ixo-chain/kinds/chain/shared.ts +286 -0
- package/src/plugins/ixo-chain/kinds/chain/staking.ts +220 -0
- package/src/plugins/ixo-chain/kinds/coins.ts +112 -0
- package/src/plugins/ixo-chain/kinds/core.ts +246 -0
- package/src/plugins/ixo-chain/kinds/indexed/agent-deposits.ts +124 -0
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +379 -0
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +319 -0
- package/src/plugins/ixo-chain/kinds/indexed/disputes.ts +159 -0
- package/src/plugins/ixo-chain/kinds/indexed/evaluations.ts +172 -0
- package/src/plugins/ixo-chain/kinds/indexed/index.ts +21 -0
- package/src/plugins/ixo-chain/kinds/indexed/liquid-stake-history.ts +160 -0
- package/src/plugins/ixo-chain/kinds/indexed/member-budgets.ts +175 -0
- package/src/plugins/ixo-chain/kinds/indexed/shared.ts +211 -0
- package/src/plugins/ixo-chain/kinds/indexed/token-history.ts +205 -0
- package/src/plugins/ixo-chain/kinds/registry.ts +43 -0
- package/src/plugins/ixo-chain/kinds/types.ts +156 -0
- package/src/plugins/ixo-chain/lookup.ts +470 -0
- package/src/plugins/ixo-chain/overview.ts +1060 -0
- package/src/plugins/ixo-chain/queries.ts +170 -138
- package/src/plugins/ixo-chain/rest.ts +135 -54
- package/src/plugins/ixo-chain/summaries.ts +3 -2
- package/src/plugins/yoma/catalog.ts +780 -0
- package/src/plugins/yoma/clef.ts +98 -0
- package/src/plugins/yoma/drafts.ts +339 -0
- package/src/plugins/yoma/engine.ts +926 -0
- package/src/plugins/yoma/index.ts +13 -0
- package/src/plugins/yoma/judge.ts +358 -0
- package/src/plugins/yoma/permission.ts +62 -0
- package/src/plugins/yoma/reads.ts +195 -0
- package/src/plugins/yoma/submission.ts +534 -0
- package/src/plugins/yoma/text.ts +16 -0
- package/src/plugins/yoma/work.ts +190 -0
- package/src/plugins/yoma/yoma.plugin.ts +2162 -0
- package/src/sqlite/serialization.ts +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `token_history`: the IXO 1155 token transfers, retirements and
|
|
3
|
+
* cancellations of an account, as one alias holding one sub-field per
|
|
4
|
+
* direction (Blocksync's `query` field nests the root query type).
|
|
5
|
+
*
|
|
6
|
+
* Index (ixo-blocksync main): `TokenTransaction_from_to_idx` ("from",
|
|
7
|
+
* "to") serves `from`; `to` and `TokenRetired`/`TokenCancelled` `owner`
|
|
8
|
+
* lead no index (`TokenRetired_name_owner_id_idx` and
|
|
9
|
+
* `TokenCancelled_name_idx` lead with `name`). All four answered in under
|
|
10
|
+
* 1 s on mainnet on Oct 29 2026 (`to` 0.9 s, the slowest), so they are
|
|
11
|
+
* read without `totalCount`, newest indexed row first (`aid`, the primary
|
|
12
|
+
* key). The index stores no time or height for these rows.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { TOKEN_DECIMALS_NOTE } from '../../summaries';
|
|
16
|
+
import { defineKind, type KindResult } from '../types';
|
|
17
|
+
import { accountArg, firstArg } from '../../queries';
|
|
18
|
+
import { text } from '../../summaries';
|
|
19
|
+
import {
|
|
20
|
+
PAGE,
|
|
21
|
+
accountFor,
|
|
22
|
+
amountText,
|
|
23
|
+
compact,
|
|
24
|
+
connection,
|
|
25
|
+
equalTo,
|
|
26
|
+
idText,
|
|
27
|
+
listResult,
|
|
28
|
+
longText,
|
|
29
|
+
opt,
|
|
30
|
+
str,
|
|
31
|
+
unexpected,
|
|
32
|
+
} from './shared';
|
|
33
|
+
|
|
34
|
+
const DIRECTIONS = ['in', 'out', 'retired', 'cancelled'] as const;
|
|
35
|
+
type Direction = (typeof DIRECTIONS)[number];
|
|
36
|
+
|
|
37
|
+
const args = z.strictObject({
|
|
38
|
+
address: accountArg('Token holder'),
|
|
39
|
+
direction: z
|
|
40
|
+
.enum(DIRECTIONS)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(
|
|
43
|
+
'in (received), out (sent), retired or cancelled. Default: in and out.',
|
|
44
|
+
),
|
|
45
|
+
first: firstArg,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const TRANSFER_FIELDS = 'aid from to amount tokenId token { name collection }';
|
|
49
|
+
|
|
50
|
+
/** The sub-fields (name, root and fields) a direction reads. */
|
|
51
|
+
const SUBFIELDS: Record<
|
|
52
|
+
Direction,
|
|
53
|
+
{ name: string; root: string; column: string; fields: string }
|
|
54
|
+
> = {
|
|
55
|
+
out: {
|
|
56
|
+
name: 'sent',
|
|
57
|
+
root: 'tokenTransactions',
|
|
58
|
+
column: 'from',
|
|
59
|
+
fields: TRANSFER_FIELDS,
|
|
60
|
+
},
|
|
61
|
+
in: {
|
|
62
|
+
name: 'received',
|
|
63
|
+
root: 'tokenTransactions',
|
|
64
|
+
column: 'to',
|
|
65
|
+
fields: TRANSFER_FIELDS,
|
|
66
|
+
},
|
|
67
|
+
retired: {
|
|
68
|
+
name: 'retired',
|
|
69
|
+
root: 'tokenRetireds',
|
|
70
|
+
column: 'owner',
|
|
71
|
+
fields: 'aid id name amount reason jurisdiction',
|
|
72
|
+
},
|
|
73
|
+
cancelled: {
|
|
74
|
+
name: 'cancelled',
|
|
75
|
+
root: 'tokenCancelleds',
|
|
76
|
+
column: 'owner',
|
|
77
|
+
fields: 'aid id name amount reason',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const transferNode = z.looseObject({
|
|
82
|
+
aid: z.number(),
|
|
83
|
+
from: str,
|
|
84
|
+
to: str,
|
|
85
|
+
amount: z.union([z.string(), z.number()]),
|
|
86
|
+
tokenId: str,
|
|
87
|
+
token: z.looseObject({ name: opt, collection: opt }).nullish(),
|
|
88
|
+
});
|
|
89
|
+
const burnNode = z.looseObject({
|
|
90
|
+
aid: z.number(),
|
|
91
|
+
id: str,
|
|
92
|
+
name: opt,
|
|
93
|
+
amount: z.union([z.string(), z.number()]),
|
|
94
|
+
reason: opt,
|
|
95
|
+
jurisdiction: opt,
|
|
96
|
+
});
|
|
97
|
+
const answer = z.looseObject({
|
|
98
|
+
sent: connection(transferNode).optional(),
|
|
99
|
+
received: connection(transferNode).optional(),
|
|
100
|
+
retired: connection(burnNode).optional(),
|
|
101
|
+
cancelled: connection(burnNode).optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
type Row = { aid: number; item: Record<string, unknown> };
|
|
105
|
+
|
|
106
|
+
function transferRows(
|
|
107
|
+
nodes: z.infer<typeof transferNode>[],
|
|
108
|
+
direction: 'sent' | 'received',
|
|
109
|
+
): Row[] {
|
|
110
|
+
return nodes.map((node) => {
|
|
111
|
+
// An empty sender is a mint, an empty recipient a burn.
|
|
112
|
+
const counterparty = direction === 'sent' ? node.to : node.from;
|
|
113
|
+
return {
|
|
114
|
+
aid: node.aid,
|
|
115
|
+
item: compact({
|
|
116
|
+
direction,
|
|
117
|
+
tokenId: idText(node.tokenId),
|
|
118
|
+
name: text(node.token?.name),
|
|
119
|
+
collection: idText(node.token?.collection),
|
|
120
|
+
amount: amountText(node.amount),
|
|
121
|
+
counterparty: idText(counterparty),
|
|
122
|
+
minted: direction === 'received' && !node.from ? true : undefined,
|
|
123
|
+
burned: direction === 'sent' && !node.to ? true : undefined,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function burnRows(
|
|
130
|
+
nodes: z.infer<typeof burnNode>[],
|
|
131
|
+
direction: 'retired' | 'cancelled',
|
|
132
|
+
): Row[] {
|
|
133
|
+
return nodes.map((node) => ({
|
|
134
|
+
aid: node.aid,
|
|
135
|
+
item: compact({
|
|
136
|
+
direction,
|
|
137
|
+
tokenId: idText(node.id),
|
|
138
|
+
name: text(node.name),
|
|
139
|
+
amount: amountText(node.amount),
|
|
140
|
+
reason: longText(node.reason),
|
|
141
|
+
jurisdiction: text(node.jurisdiction, 80),
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const tokenHistory = defineKind({
|
|
147
|
+
name: 'token_history',
|
|
148
|
+
summary:
|
|
149
|
+
'IXO 1155 token movements of an account (default: the user): received, sent, retired or cancelled.',
|
|
150
|
+
explain: [
|
|
151
|
+
'IXO 1155 tokens (impact tokens, carbon credits, SUPA…) move between accounts; retiring one takes it out of circulation for good, with a reason and a jurisdiction (an offset claim), and cancelling one voids it.',
|
|
152
|
+
'Each row: direction (received, sent, retired, cancelled), tokenId, name (the token class), collection, amount and, for transfers, the counterparty.',
|
|
153
|
+
'`minted: true` marks a received row with no sender (newly minted to the account); `burned: true` a sent row with no recipient.',
|
|
154
|
+
'IXO 1155 tokens have no decimals on chain: amounts are whole units.',
|
|
155
|
+
'Rows are newest indexed first; the index keeps no time or block height for them, so ask `transactions` for when.',
|
|
156
|
+
'With no direction, received and sent are merged; `more: true` when the index holds older rows.',
|
|
157
|
+
].join(' '),
|
|
158
|
+
source: 'blocksync',
|
|
159
|
+
list: true,
|
|
160
|
+
args,
|
|
161
|
+
example: { direction: 'retired', first: 5 },
|
|
162
|
+
plan(input, ctx) {
|
|
163
|
+
const holder = accountFor(input.address, ctx.identity, 'address');
|
|
164
|
+
if ('invalid' in holder) return holder;
|
|
165
|
+
const directions: Direction[] = input.direction
|
|
166
|
+
? [input.direction]
|
|
167
|
+
: ['in', 'out'];
|
|
168
|
+
const n = ctx.first;
|
|
169
|
+
const subfields = directions
|
|
170
|
+
.map((direction) => {
|
|
171
|
+
const { name, root, column, fields } = SUBFIELDS[direction];
|
|
172
|
+
return `${name}: ${root}(filter: ${equalTo(column, holder.address)}, first: ${n}, orderBy: PRIMARY_KEY_DESC) { ${PAGE} nodes { ${fields} } }`;
|
|
173
|
+
})
|
|
174
|
+
.join(' ');
|
|
175
|
+
return {
|
|
176
|
+
source: 'blocksync',
|
|
177
|
+
field: (alias) => `${alias}: query { ${subfields} }`,
|
|
178
|
+
shape(raw): KindResult {
|
|
179
|
+
const parsed = answer.safeParse(raw);
|
|
180
|
+
if (!parsed.success) return { ok: false, ...unexpected };
|
|
181
|
+
const { sent, received, retired, cancelled } = parsed.data;
|
|
182
|
+
const parts = [sent, received, retired, cancelled];
|
|
183
|
+
// A transfer to oneself is not indexed, so no row is in both lists.
|
|
184
|
+
const rows = [
|
|
185
|
+
...transferRows(sent?.nodes ?? [], 'sent'),
|
|
186
|
+
...transferRows(received?.nodes ?? [], 'received'),
|
|
187
|
+
...burnRows(retired?.nodes ?? [], 'retired'),
|
|
188
|
+
...burnRows(cancelled?.nodes ?? [], 'cancelled'),
|
|
189
|
+
].sort((a, b) => b.aid - a.aid);
|
|
190
|
+
const more =
|
|
191
|
+
rows.length > n ||
|
|
192
|
+
parts.some((part) => part?.pageInfo?.hasNextPage === true);
|
|
193
|
+
return listResult(
|
|
194
|
+
{
|
|
195
|
+
note: TOKEN_DECIMALS_NOTE,
|
|
196
|
+
items: rows.slice(0, n).map((row) => row.item),
|
|
197
|
+
more: more ? true : undefined,
|
|
198
|
+
},
|
|
199
|
+
n,
|
|
200
|
+
holder.subject,
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `chain_lookup` kind, in catalogue order: the core kinds, then the
|
|
3
|
+
* chain REST kinds, then the Blocksync-indexed kinds. Names are unique:
|
|
4
|
+
* a name registered twice fails the module at load.
|
|
5
|
+
*/
|
|
6
|
+
import { CHAIN_KINDS } from './chain';
|
|
7
|
+
import { CORE_KINDS } from './core';
|
|
8
|
+
import { INDEXED_KINDS } from './indexed';
|
|
9
|
+
import type { KindDef } from './types';
|
|
10
|
+
|
|
11
|
+
/** `kinds` by name; throws when two share a name. */
|
|
12
|
+
export function indexKinds(
|
|
13
|
+
kinds: readonly KindDef[],
|
|
14
|
+
): ReadonlyMap<string, KindDef> {
|
|
15
|
+
const byName = new Map<string, KindDef>();
|
|
16
|
+
for (const kind of kinds) {
|
|
17
|
+
if (byName.has(kind.name))
|
|
18
|
+
throw new Error(`ixo-chain: kind "${kind.name}" is registered twice`);
|
|
19
|
+
byName.set(kind.name, kind);
|
|
20
|
+
}
|
|
21
|
+
return byName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const KINDS: readonly KindDef[] = [
|
|
25
|
+
...CORE_KINDS,
|
|
26
|
+
...CHAIN_KINDS,
|
|
27
|
+
...INDEXED_KINDS,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const BY_NAME = indexKinds(KINDS);
|
|
31
|
+
|
|
32
|
+
function namesOf(kinds: readonly KindDef[]): [string, ...string[]] {
|
|
33
|
+
const [first, ...rest] = kinds.map((kind) => kind.name);
|
|
34
|
+
if (first === undefined) throw new Error('ixo-chain: no kinds registered');
|
|
35
|
+
return [first, ...rest];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every kind's name in catalogue order: the `kind` enum of `chain_lookup`. */
|
|
39
|
+
export const kindNames = namesOf(KINDS);
|
|
40
|
+
|
|
41
|
+
export function kindByName(name: string): KindDef | undefined {
|
|
42
|
+
return BY_NAME.get(name);
|
|
43
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract every `chain_lookup` kind implements. A kind is one named
|
|
3
|
+
* read the model can ask for: its arguments, a one-line summary for the
|
|
4
|
+
* catalogue `describe_chain_kinds` returns, a longer explanation shown
|
|
5
|
+
* only on request, and a plan that answers it either as one aliased field
|
|
6
|
+
* of the call's single Blocksync document or as chain REST reads.
|
|
7
|
+
*
|
|
8
|
+
* Arguments are validated by the kind's own schema inside
|
|
9
|
+
* {@link defineKind}: `chain_lookup`'s tool schema only names the kind, so
|
|
10
|
+
* the catalogue does not cost context on every turn.
|
|
11
|
+
*/
|
|
12
|
+
import type { z } from 'zod';
|
|
13
|
+
import type { UserIdentity } from '../../../core/user-identity';
|
|
14
|
+
import type { TokenDisplay } from '../denoms';
|
|
15
|
+
import type { ChainCall, Failure, Outcome, Source } from '../http';
|
|
16
|
+
import type { Subject } from '../queries';
|
|
17
|
+
import type { Absent } from '../rest';
|
|
18
|
+
|
|
19
|
+
/** How a kind bounds one REST read, and which answers mean "none". */
|
|
20
|
+
export interface KindRestOptions {
|
|
21
|
+
label: string;
|
|
22
|
+
notFoundOn404?: boolean;
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
/**
|
|
25
|
+
* HTTP statuses that mean "there is none" (e.g. `[400, 404]` for a vote
|
|
26
|
+
* never cast): such an answer is `{ ok: true, absent: true }`, not a
|
|
27
|
+
* failure, and is not logged.
|
|
28
|
+
*/
|
|
29
|
+
noneOn?: readonly number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What a kind's plan may use: the call, the user and the call's shared readers. */
|
|
33
|
+
export interface KindContext {
|
|
34
|
+
call: ChainCall;
|
|
35
|
+
identity: UserIdentity;
|
|
36
|
+
/**
|
|
37
|
+
* Rows a list kind returns: the request's `first`, already clamped to
|
|
38
|
+
* 1–20 and scaled down for a batch of several requests.
|
|
39
|
+
*/
|
|
40
|
+
first: number;
|
|
41
|
+
/**
|
|
42
|
+
* A chain REST GET through the call's shared concurrency limit and the
|
|
43
|
+
* plugin's retry policy. `path` is appended to the REST base and must
|
|
44
|
+
* only carry values the kind validated.
|
|
45
|
+
*/
|
|
46
|
+
rest<S extends z.ZodType>(
|
|
47
|
+
path: string,
|
|
48
|
+
schema: S,
|
|
49
|
+
opts: KindRestOptions & { noneOn?: undefined },
|
|
50
|
+
): Promise<Outcome<z.infer<S>>>;
|
|
51
|
+
rest<S extends z.ZodType>(
|
|
52
|
+
path: string,
|
|
53
|
+
schema: S,
|
|
54
|
+
opts: KindRestOptions & { noneOn: readonly number[] },
|
|
55
|
+
): Promise<Outcome<z.infer<S>> | Absent>;
|
|
56
|
+
/**
|
|
57
|
+
* How a bank denom is shown (`denoms.ts`, with the oracle's IBC table:
|
|
58
|
+
* the network's built-in entries and `IXO_CHAIN_IBC_DENOMS`). Lookups are
|
|
59
|
+
* cached per isolate and run through the call's REST limit; one request
|
|
60
|
+
* looks up at most a fixed number of distinct denoms, and any further one
|
|
61
|
+
* is shown unread (base units, `metadataUnavailable`).
|
|
62
|
+
*/
|
|
63
|
+
display(denom: string): Promise<TokenDisplay>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One kind's answer. `subject` says whether it is about the user. */
|
|
67
|
+
export type KindResult =
|
|
68
|
+
| {
|
|
69
|
+
ok: true;
|
|
70
|
+
data: unknown;
|
|
71
|
+
subject?: Subject;
|
|
72
|
+
/** The rows a list kind asked for. */
|
|
73
|
+
first?: number;
|
|
74
|
+
}
|
|
75
|
+
| ({ ok: false } & Failure);
|
|
76
|
+
|
|
77
|
+
/** How a validated request is answered. */
|
|
78
|
+
export type KindPlan =
|
|
79
|
+
| {
|
|
80
|
+
source: 'blocksync';
|
|
81
|
+
/** The aliased root field (`${alias}: …`) for the call's one document. */
|
|
82
|
+
field(alias: string): string;
|
|
83
|
+
/** The answer from that alias's data (null when the index has none). */
|
|
84
|
+
shape(raw: unknown): KindResult | Promise<KindResult>;
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
source: 'chain';
|
|
88
|
+
run(): Promise<KindResult>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** A request the kind refuses before any read: the detail the model sees. */
|
|
92
|
+
export type KindInvalid = { invalid: string };
|
|
93
|
+
|
|
94
|
+
/** A registered kind, its argument types erased. */
|
|
95
|
+
export interface KindDef {
|
|
96
|
+
name: string;
|
|
97
|
+
/** One line for the catalogue: what question it answers. */
|
|
98
|
+
summary: string;
|
|
99
|
+
/** What the result means (types, units, how to read it); shown only on request. */
|
|
100
|
+
explain: string;
|
|
101
|
+
source: Source;
|
|
102
|
+
/** Whether the kind returns a list honouring `first`. */
|
|
103
|
+
list: boolean;
|
|
104
|
+
/** The argument schema (for `describe_chain_kinds`). */
|
|
105
|
+
args: z.ZodType;
|
|
106
|
+
/** A valid `args` object. */
|
|
107
|
+
example: Record<string, unknown>;
|
|
108
|
+
/** Validates `rawArgs` and returns the plan, or why the request is refused. */
|
|
109
|
+
plan(rawArgs: unknown, ctx: KindContext): KindPlan | KindInvalid;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The argument problems as one sentence naming what the kind expects. */
|
|
113
|
+
function invalidArgs(name: string, error: z.ZodError): KindInvalid {
|
|
114
|
+
const issues = error.issues
|
|
115
|
+
.slice(0, 3)
|
|
116
|
+
.map((issue) =>
|
|
117
|
+
issue.path.length
|
|
118
|
+
? `${issue.path.join('.')}: ${issue.message}`
|
|
119
|
+
: issue.message,
|
|
120
|
+
)
|
|
121
|
+
.join('; ');
|
|
122
|
+
return {
|
|
123
|
+
invalid: `Invalid args for kind "${name}" (${issues}). Call describe_chain_kinds with ["${name}"] for its arguments.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A kind from its typed spec: `plan` receives the arguments already parsed
|
|
129
|
+
* by `args`, so a kind never reads unvalidated input.
|
|
130
|
+
*/
|
|
131
|
+
export function defineKind<S extends z.ZodType>(spec: {
|
|
132
|
+
name: string;
|
|
133
|
+
summary: string;
|
|
134
|
+
explain: string;
|
|
135
|
+
source: Source;
|
|
136
|
+
list: boolean;
|
|
137
|
+
args: S;
|
|
138
|
+
example: z.input<S> & Record<string, unknown>;
|
|
139
|
+
plan(args: z.output<S>, ctx: KindContext): KindPlan | KindInvalid;
|
|
140
|
+
}): KindDef {
|
|
141
|
+
return {
|
|
142
|
+
name: spec.name,
|
|
143
|
+
summary: spec.summary,
|
|
144
|
+
explain: spec.explain,
|
|
145
|
+
source: spec.source,
|
|
146
|
+
list: spec.list,
|
|
147
|
+
args: spec.args,
|
|
148
|
+
example: spec.example,
|
|
149
|
+
plan(rawArgs, ctx) {
|
|
150
|
+
const parsed = spec.args.safeParse(rawArgs ?? {});
|
|
151
|
+
return parsed.success
|
|
152
|
+
? spec.plan(parsed.data, ctx)
|
|
153
|
+
: invalidArgs(spec.name, parsed.error);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|