@ixo/oracle-runtime-workers 0.30.0 → 0.32.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/middlewares/tool-repetition-guard.ts +6 -3
- package/src/core/middlewares/turn-boundary.ts +6 -1
- package/src/core/runtime-context.ts +3 -0
- package/src/core/tool-not-run.ts +30 -0
- package/src/core/wrap-plugin-tool.ts +9 -1
- 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 +220 -57
- package/src/hub-actions/chain-tx-refusals.ts +217 -0
- package/src/hub-actions/client.ts +170 -26
- package/src/hub-actions/hub-text.ts +82 -0
- package/src/hub-actions/request.ts +39 -29
- package/src/hub-actions/store.ts +5 -1
- package/src/hub-actions/transfer.ts +99 -32
- package/src/index.ts +1 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +102 -20
- package/src/plugins/attachments/view-attachment-tool.ts +7 -3
- package/src/plugins/hub-actions/chain-tx-catalog.ts +1239 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +490 -232
- package/src/plugins/ixo-chain/blocksync.ts +6 -2
- package/src/plugins/ixo-chain/http.ts +16 -8
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +1 -1
- package/src/plugins/ixo-chain/kinds/chain/index.ts +2 -0
- package/src/plugins/ixo-chain/kinds/chain/validators.ts +151 -0
- package/src/plugins/ixo-chain/rest.ts +7 -1
- 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 +978 -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 +531 -0
- package/src/plugins/yoma/text.ts +16 -0
- package/src/plugins/yoma/work.ts +190 -0
- package/src/plugins/yoma/yoma.plugin.ts +2171 -0
- package/src/sqlite/serialization.ts +2 -0
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CHAIN_TX_MAX_MESSAGES,
|
|
3
|
-
CHAIN_TX_MAX_TOKEN_LINES,
|
|
4
3
|
CHAIN_TX_MESSAGE_TYPES,
|
|
5
|
-
DENOM,
|
|
6
4
|
operationJsonSchema,
|
|
7
5
|
parseOperation,
|
|
6
|
+
type ActionOutcomeV1,
|
|
7
|
+
type ActionReceiptV1,
|
|
8
8
|
type ActionReferenceV1,
|
|
9
9
|
type ChainTxMessageType,
|
|
10
10
|
} from '@ixo/action-protocol';
|
|
11
|
+
import {
|
|
12
|
+
AIMessage,
|
|
13
|
+
ToolMessage,
|
|
14
|
+
type BaseMessage,
|
|
15
|
+
} from '@langchain/core/messages';
|
|
16
|
+
import { tool } from '@langchain/core/tools';
|
|
17
|
+
import { convertToOpenAITool } from '@langchain/core/utils/function_calling';
|
|
18
|
+
import { createMiddleware, type AgentMiddleware } from 'langchain';
|
|
11
19
|
import { z } from 'zod';
|
|
20
|
+
import { quoteHubText } from '../../hub-actions/hub-text';
|
|
12
21
|
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
13
22
|
import type {
|
|
14
23
|
ChainTxMessageInput,
|
|
@@ -17,6 +26,19 @@ import type {
|
|
|
17
26
|
RuntimeContext,
|
|
18
27
|
} from '../../plugin-api/types';
|
|
19
28
|
import { ixoAddressOf } from '../../core/user-identity';
|
|
29
|
+
import { toolNotRunMessage } from '../../core/tool-not-run';
|
|
30
|
+
import {
|
|
31
|
+
CHAIN_TX_CATALOG,
|
|
32
|
+
CHAIN_TX_GENERAL_RULES,
|
|
33
|
+
chainTxMessageInput,
|
|
34
|
+
hasUnsafeText,
|
|
35
|
+
} from './chain-tx-catalog';
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
CHAIN_TX_CATALOG,
|
|
39
|
+
CHAIN_TX_GENERAL_RULES,
|
|
40
|
+
hasUnsafeText,
|
|
41
|
+
} from './chain-tx-catalog';
|
|
20
42
|
|
|
21
43
|
const configSchema = z.object({
|
|
22
44
|
HUB_ACTIONS_ENABLED: z.enum(['true', 'false']).default('false'),
|
|
@@ -27,14 +49,18 @@ const configSchema = z.object({
|
|
|
27
49
|
* (`ixo.chain.tx.v1`, coin sends included) is built by the runtime from
|
|
28
50
|
* `request_chain_transaction`'s messages, and the older bank send
|
|
29
51
|
* (`ixo.bank.send.v1`) is no longer prepared by this runtime (Auth Hub
|
|
30
|
-
* still serves it for older runtimes).
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* model
|
|
52
|
+
* still serves it for older runtimes). An application (`ixo.bid.submit.v1`)
|
|
53
|
+
* or a claim (`ixo.claim.submit.v1`) is built by the plugin that runs the
|
|
54
|
+
* claim collection, from the person's answers and the files of the turn
|
|
55
|
+
* (`ctx.session.channel.attachments`): the model never names an evidence
|
|
56
|
+
* link. They are left out of the generic tool's schema (its handler refuses
|
|
57
|
+
* them anyway), which also keeps their branches out of every model request.
|
|
34
58
|
*/
|
|
35
|
-
const RUNTIME_BUILT_KINDS: readonly string[] = [
|
|
59
|
+
export const RUNTIME_BUILT_KINDS: readonly string[] = [
|
|
36
60
|
'ixo.bank.send.v1',
|
|
37
61
|
'ixo.chain.tx.v1',
|
|
62
|
+
'ixo.bid.submit.v1',
|
|
63
|
+
'ixo.claim.submit.v1',
|
|
38
64
|
];
|
|
39
65
|
function branchKind(branch: Record<string, unknown>): unknown {
|
|
40
66
|
const properties = branch.properties;
|
|
@@ -58,192 +84,7 @@ const statusSchema = z.strictObject({
|
|
|
58
84
|
});
|
|
59
85
|
const discoverSchema = z.strictObject({});
|
|
60
86
|
|
|
61
|
-
/**
|
|
62
|
-
* The messages of one chain transaction (`ixo.chain.tx.v1`), as the model
|
|
63
|
-
* writes them. The shapes are the protocol's minus the owner's identity
|
|
64
|
-
* (a claim's `agentDid`, an evaluation's `agentAddress`), which the runtime
|
|
65
|
-
* fills, as it fills the chain id, network and signer; a bank step names a
|
|
66
|
-
* display amount, which the owner object converts to base units from the
|
|
67
|
-
* owner's token list. The protocol parses the whole operation again before
|
|
68
|
-
* it reaches the Hub. The rules match Auth Hub's (ids, address length, DID
|
|
69
|
-
* escapes, memo characters), so a message the Hub would refuse is refused
|
|
70
|
-
* here, before any Hub call.
|
|
71
|
-
*/
|
|
72
|
-
/** A 20-byte bech32 account address: `ixo1` and 38 characters (a 32-byte contract address is longer). */
|
|
73
|
-
const IXO_ADDRESS = /^ixo1[023456789acdefghjklmnpqrstuvwxyz]{38}$/;
|
|
74
|
-
const BASE_UNITS = /^[1-9][0-9]{0,77}$/;
|
|
75
|
-
/** A DID; `%` only as a `%xx` escape. */
|
|
76
|
-
const ANY_DID = /^did:[a-z0-9]+:(?:[A-Za-z0-9._:-]|%[0-9A-Fa-f]{2})+$/;
|
|
77
|
-
/** Claim, collection, token and proof ids: no spaces, quotes or markup. */
|
|
78
|
-
const ID_CHARS = /^[A-Za-z0-9:._/-]+$/;
|
|
79
|
-
const ID_RULE = 'Only letters, digits and : . _ / - (copied exactly).';
|
|
80
|
-
/**
|
|
81
|
-
* Characters that would let text render as something else: C0 and C1
|
|
82
|
-
* controls, zero-width and direction marks (U+200B–U+200F), line and
|
|
83
|
-
* paragraph separators (U+2028–U+2029), bidi embeddings, overrides and
|
|
84
|
-
* isolates (U+202A–U+202E, U+2066–U+2069), the Arabic letter mark (U+061C)
|
|
85
|
-
* and the byte order mark (U+FEFF). Auth Hub refuses the same set.
|
|
86
|
-
*/
|
|
87
|
-
export function hasUnsafeText(value: string): boolean {
|
|
88
|
-
for (const char of value) {
|
|
89
|
-
const code = char.codePointAt(0) ?? 0;
|
|
90
|
-
if (
|
|
91
|
-
code <= 0x1f ||
|
|
92
|
-
(code >= 0x7f && code <= 0x9f) ||
|
|
93
|
-
code === 0x061c ||
|
|
94
|
-
(code >= 0x200b && code <= 0x200f) ||
|
|
95
|
-
(code >= 0x2028 && code <= 0x202e) ||
|
|
96
|
-
(code >= 0x2066 && code <= 0x2069) ||
|
|
97
|
-
code === 0xfeff
|
|
98
|
-
)
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
87
|
const MEMO_BYTES = 256;
|
|
104
|
-
const address = (what: string) =>
|
|
105
|
-
z
|
|
106
|
-
.string()
|
|
107
|
-
.regex(
|
|
108
|
-
IXO_ADDRESS,
|
|
109
|
-
'A 20-byte ixo1… account address (42 characters); contract addresses are refused.',
|
|
110
|
-
)
|
|
111
|
-
.describe(`${what} (an ixo1… account address).`);
|
|
112
|
-
const id = (max: number) => z.string().min(1).max(max).regex(ID_CHARS, ID_RULE);
|
|
113
|
-
const baseUnits = (what: string) =>
|
|
114
|
-
z
|
|
115
|
-
.string()
|
|
116
|
-
.regex(BASE_UNITS, 'A whole number of base units above zero.')
|
|
117
|
-
.describe(what);
|
|
118
|
-
const claimId = id(256).describe('The claim id, exactly as stored.');
|
|
119
|
-
const collectionId = z
|
|
120
|
-
.string()
|
|
121
|
-
.regex(/^(0|[1-9][0-9]{0,19})$/, 'Digits only, no leading zeros.')
|
|
122
|
-
.describe('The claim collection id (digits).');
|
|
123
|
-
|
|
124
|
-
/** A display amount: a plain decimal in the token's units, as the user said it. */
|
|
125
|
-
const DISPLAY_AMOUNT = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* One chain-transaction message type: the one-line `summary` the request
|
|
129
|
-
* tool's description lists, the full `rules` describe_chain_messages
|
|
130
|
-
* returns (with the fields of `schema`), and the strict `schema` the
|
|
131
|
-
* request handler checks each message against. Adding a message type is
|
|
132
|
-
* one entry here (once `@ixo/action-protocol` and Auth Hub support it).
|
|
133
|
-
*/
|
|
134
|
-
interface ChainTxCatalogEntry {
|
|
135
|
-
summary: string;
|
|
136
|
-
rules: string;
|
|
137
|
-
schema: z.ZodObject;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const CHAIN_TX_CATALOG = {
|
|
141
|
-
'cosmos.bank.send': {
|
|
142
|
-
summary:
|
|
143
|
-
"send coins (IXO or another token the owner holds) from the owner's wallet.",
|
|
144
|
-
rules: [
|
|
145
|
-
"Sends coins from the owner's wallet to `toAddress`, alone (a single coin send) or as a step of a batch.",
|
|
146
|
-
'`amount` is the amount as the user said it, in the token’s display units ("1.5" for 1.5 IXO): a plain decimal, never base units. The runtime looks the token up in the owner’s token list (hub_action_options `transfer.tokens`), converts the amount with the listed `decimals`, and refuses a token not in the list, more digits after the point than the token has, zero, and more than the listed balance (the bank steps of one token together).',
|
|
147
|
-
'`denom` is optional and defaults to uixo (IXO, 6 decimals). For another token copy its `denom` exactly from `transfer.tokens` (never guess an `ibc/…` hash); name the full denom to the owner for anything but IXO, and say when the token is unverified (it lists 0 decimals: its amount is a whole number of base units).',
|
|
148
|
-
].join(' '),
|
|
149
|
-
schema: z.strictObject({
|
|
150
|
-
type: z.literal('cosmos.bank.send'),
|
|
151
|
-
toAddress: address('The recipient'),
|
|
152
|
-
amount: z
|
|
153
|
-
.string()
|
|
154
|
-
.max(80)
|
|
155
|
-
.regex(
|
|
156
|
-
DISPLAY_AMOUNT,
|
|
157
|
-
'A plain decimal amount in the token’s display units, such as 1.5 (never base units).',
|
|
158
|
-
)
|
|
159
|
-
.describe(
|
|
160
|
-
'The amount as the user said it, in the token’s display units (1.5 = 1.5 IXO).',
|
|
161
|
-
),
|
|
162
|
-
denom: z
|
|
163
|
-
.string()
|
|
164
|
-
.regex(DENOM, 'A denom exactly as hub_action_options lists it.')
|
|
165
|
-
.optional()
|
|
166
|
-
.describe(
|
|
167
|
-
'The token’s denom, copied exactly from hub_action_options `transfer.tokens`; leave out for uixo (IXO).',
|
|
168
|
-
),
|
|
169
|
-
}),
|
|
170
|
-
},
|
|
171
|
-
'ixo.claims.submit': {
|
|
172
|
-
summary:
|
|
173
|
-
'file a claim the owner has already created with a claim collection.',
|
|
174
|
-
rules:
|
|
175
|
-
'Files a claim the owner has already created with a claim collection. `claimId` is the stored claim’s id, `collectionId` the collection, `adminAddress` the collection admin’s address. The runtime adds the owner’s DID as the filer. The collection admin must have authorized the owner to submit claims. Collections with member budgets cannot be filed into here (no member address is sent). Set `useIntent` only when the collection works with intents.',
|
|
176
|
-
schema: z.strictObject({
|
|
177
|
-
type: z.literal('ixo.claims.submit'),
|
|
178
|
-
collectionId,
|
|
179
|
-
claimId,
|
|
180
|
-
adminAddress: address("The collection admin's address"),
|
|
181
|
-
useIntent: z
|
|
182
|
-
.boolean()
|
|
183
|
-
.optional()
|
|
184
|
-
.describe('true only when the collection pays claims through intents.'),
|
|
185
|
-
}),
|
|
186
|
-
},
|
|
187
|
-
'ixo.claims.evaluate': {
|
|
188
|
-
summary:
|
|
189
|
-
'record the owner’s decision, as evaluator, on someone else’s claim (approve, reject, dispute, invalidate).',
|
|
190
|
-
rules:
|
|
191
|
-
'Records the owner’s decision, as evaluator, on someone else’s claim (`claimId` in `collectionId`). `status` 1 approve, 2 reject, 3 dispute (the chain may refuse it as deprecated), 4 invalidate. It is recorded as the owner’s (the runtime adds their DID and address, never the claimant’s); `oracle` is the evaluator DID recorded on the evaluation (normally the owner’s DID, or the oracle entity DID the collection admin authorized); `adminAddress` is the collection admin’s address; `reason` (a number) and `verificationProof` are optional. The collection admin must have authorized the owner as an evaluator. An approval can pay the claimant.',
|
|
192
|
-
schema: z.strictObject({
|
|
193
|
-
type: z.literal('ixo.claims.evaluate'),
|
|
194
|
-
claimId,
|
|
195
|
-
collectionId,
|
|
196
|
-
oracle: z
|
|
197
|
-
.string()
|
|
198
|
-
.max(200)
|
|
199
|
-
.regex(ANY_DID, 'A DID (did:<method>:…; % only as %xx).')
|
|
200
|
-
.describe('The evaluator DID recorded on the evaluation (a did:…).'),
|
|
201
|
-
adminAddress: address("The collection admin's address"),
|
|
202
|
-
status: z
|
|
203
|
-
.literal([1, 2, 3, 4])
|
|
204
|
-
.describe(
|
|
205
|
-
'1 approve, 2 reject, 3 dispute (the chain may refuse it as deprecated), 4 invalidate.',
|
|
206
|
-
),
|
|
207
|
-
reason: z
|
|
208
|
-
.number()
|
|
209
|
-
.int()
|
|
210
|
-
.min(0)
|
|
211
|
-
.max(4_294_967_295)
|
|
212
|
-
.optional()
|
|
213
|
-
.describe("The evaluator's reason code, when there is one."),
|
|
214
|
-
verificationProof: id(300)
|
|
215
|
-
.optional()
|
|
216
|
-
.describe(
|
|
217
|
-
'The evaluation proof (a credential CID), when there is one.',
|
|
218
|
-
),
|
|
219
|
-
}),
|
|
220
|
-
},
|
|
221
|
-
'ixo.token.transfer': {
|
|
222
|
-
summary:
|
|
223
|
-
"move IXO impact tokens (1155-style token ids, whole amounts) from the owner's wallet.",
|
|
224
|
-
rules: `Moves IXO impact tokens (1155-style: each line a token \`id\` of 32 hex characters and a whole \`amount\`, 1 to ${CHAIN_TX_MAX_TOKEN_LINES} lines) from the owner’s wallet to \`recipient\`. Impact tokens have no decimals: an amount is a whole number of tokens. All tokens in one message must come from one token contract. These are not the IXO coin: coins go through \`cosmos.bank.send\`.`,
|
|
225
|
-
schema: z.strictObject({
|
|
226
|
-
type: z.literal('ixo.token.transfer'),
|
|
227
|
-
recipient: address('The recipient'),
|
|
228
|
-
tokens: z
|
|
229
|
-
.array(
|
|
230
|
-
z.strictObject({
|
|
231
|
-
id: id(128).describe(
|
|
232
|
-
'The impact token id (32 hex characters), exactly as listed.',
|
|
233
|
-
),
|
|
234
|
-
amount: baseUnits('A whole number of tokens above zero.'),
|
|
235
|
-
}),
|
|
236
|
-
)
|
|
237
|
-
.min(1)
|
|
238
|
-
.max(CHAIN_TX_MAX_TOKEN_LINES),
|
|
239
|
-
}),
|
|
240
|
-
},
|
|
241
|
-
} as const satisfies Record<ChainTxMessageType, ChainTxCatalogEntry>;
|
|
242
|
-
|
|
243
|
-
/** What every message type shares, returned once by describe_chain_messages. */
|
|
244
|
-
export const CHAIN_TX_GENERAL_RULES =
|
|
245
|
-
'Addresses are 20-byte `ixo1…` account addresses (contract addresses are refused). Ids (`claimId`, `collectionId`, token `id`, `verificationProof`) use only letters, digits and `: . _ / -`; copy them exactly and never invent them. Give only the fields listed for a type: the runtime adds the chain, the network, the signer and the owner’s own DID and address where a message needs them, and Auth Hub sets the fee. Up to 8 messages per transaction, run in order; all succeed or none do.';
|
|
246
|
-
|
|
247
88
|
const memoSchema = z
|
|
248
89
|
.string()
|
|
249
90
|
.max(MEMO_BYTES)
|
|
@@ -272,6 +113,8 @@ export function fieldsOnly(schema: z.core.$ZodType): z.ZodType {
|
|
|
272
113
|
);
|
|
273
114
|
if (schema instanceof z.ZodOptional)
|
|
274
115
|
return fieldsOnly(schema.unwrap()).optional();
|
|
116
|
+
if (schema instanceof z.ZodNullable)
|
|
117
|
+
return fieldsOnly(schema.unwrap()).nullable();
|
|
275
118
|
if (schema instanceof z.ZodArray) return z.array(fieldsOnly(schema.element));
|
|
276
119
|
if (schema instanceof z.ZodString) return z.string();
|
|
277
120
|
if (schema instanceof z.ZodNumber) return z.number();
|
|
@@ -300,17 +143,34 @@ function messageFieldsOnly(schema: z.ZodObject) {
|
|
|
300
143
|
}
|
|
301
144
|
|
|
302
145
|
/**
|
|
303
|
-
* The schema the model is given: per type
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
146
|
+
* The schema the model is given: per type its fields (messageFieldsOnly)
|
|
147
|
+
* when the type was described in this session (`described`; absent: every
|
|
148
|
+
* type), and one open branch naming every other type (`type` one of them,
|
|
149
|
+
* any other field allowed). A type the model has not read about costs its
|
|
150
|
+
* name only; the request handler refuses it until it is described, and
|
|
151
|
+
* the next model call is given its fields (`chainTxFieldsMiddleware`).
|
|
152
|
+
* Each message is checked by the handler against its full catalogue
|
|
153
|
+
* schema.
|
|
307
154
|
*/
|
|
308
155
|
export function chainTransactionSchemaFor(
|
|
309
156
|
catalog: Readonly<Record<string, { schema: z.ZodObject }>>,
|
|
157
|
+
described?: ReadonlySet<string>,
|
|
310
158
|
) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
159
|
+
const entries = Object.entries(catalog);
|
|
160
|
+
const withFields = entries
|
|
161
|
+
.filter(([type]) => !described || described.has(type))
|
|
162
|
+
.map(([, entry]) => messageFieldsOnly(entry.schema));
|
|
163
|
+
const [firstUnread, ...unread] = entries
|
|
164
|
+
.filter(([type]) => described && !described.has(type))
|
|
165
|
+
.map(([type]) => type);
|
|
166
|
+
const branches =
|
|
167
|
+
firstUnread === undefined
|
|
168
|
+
? withFields
|
|
169
|
+
: [
|
|
170
|
+
...withFields,
|
|
171
|
+
z.looseObject({ type: z.literal([firstUnread, ...unread]) }),
|
|
172
|
+
];
|
|
173
|
+
const [first, ...rest] = branches;
|
|
314
174
|
if (!first) throw new Error('The chain transaction catalogue is empty');
|
|
315
175
|
return z.strictObject({
|
|
316
176
|
messages: z
|
|
@@ -320,7 +180,6 @@ export function chainTransactionSchemaFor(
|
|
|
320
180
|
memo: memoSchema,
|
|
321
181
|
});
|
|
322
182
|
}
|
|
323
|
-
const chainTransactionSchema = chainTransactionSchemaFor(CHAIN_TX_CATALOG);
|
|
324
183
|
|
|
325
184
|
/**
|
|
326
185
|
* The request tool's description: what it does, the discovery step, one
|
|
@@ -335,9 +194,285 @@ export function chainTransactionDescription(
|
|
|
335
194
|
...Object.entries(catalog).map(
|
|
336
195
|
([type, entry]) => `- \`${type}\`: ${entry.summary}`,
|
|
337
196
|
),
|
|
338
|
-
'Up to 8 messages run in order as one transaction: all succeed or none do. Never invent ids or addresses (ask the owner or use other tools); coin amounts are display units ("1.5"), never base units. When the result is unavailable, tell the owner its reason in plain words; when it says retrying or changing the fields will not help, do not retry.',
|
|
197
|
+
'Up to 8 messages run in order as one transaction: all succeed or none do. Never invent ids or addresses (ask the owner or use other tools); coin and stake amounts are display units ("1.5"), never base units. Tell the owner what will happen in plain everyday words, never message type names or chain terms. When the result is unavailable, tell the owner its reason in plain words; when it says retrying or changing the fields will not help, do not retry.',
|
|
339
198
|
].join('\n');
|
|
340
199
|
}
|
|
200
|
+
|
|
201
|
+
/** The catalogue entries of `types`, in catalogue order. */
|
|
202
|
+
function catalogOf(types: readonly ChainTxMessageType[]) {
|
|
203
|
+
return Object.fromEntries(
|
|
204
|
+
CHAIN_TX_MESSAGE_TYPES.filter((type) => types.includes(type)).map(
|
|
205
|
+
(type) => [type, CHAIN_TX_CATALOG[type]],
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* request_chain_transaction for the message types this account may use
|
|
212
|
+
* (`availableChainTxTypes`): the model is given their summaries, and the
|
|
213
|
+
* fields of those `described` in this session (null: of every one), so it
|
|
214
|
+
* neither sees nor pays for a type Auth Hub would refuse or one it has not
|
|
215
|
+
* read about, and its arguments are checked against the same schema (a
|
|
216
|
+
* message of another type is refused before any Hub call). Null when the
|
|
217
|
+
* account may use none (describe_chain_messages then answers
|
|
218
|
+
* `availableTypes: []`). Built once per (types, described) pair.
|
|
219
|
+
*/
|
|
220
|
+
export function chainTransactionTool(
|
|
221
|
+
types: readonly ChainTxMessageType[],
|
|
222
|
+
described: readonly ChainTxMessageType[] | null = null,
|
|
223
|
+
): PluginTool | null {
|
|
224
|
+
const catalog = catalogOf(types);
|
|
225
|
+
if (!Object.keys(catalog).length) return null;
|
|
226
|
+
const fields = described
|
|
227
|
+
? new Set(
|
|
228
|
+
Object.keys(catalog).filter((type) =>
|
|
229
|
+
described.some((d) => d === type),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
: undefined;
|
|
233
|
+
const key = `${Object.keys(catalog).join(',')}|${fields ? [...fields].join(',') : '*'}`;
|
|
234
|
+
const cached = chainTransactionTools.get(key);
|
|
235
|
+
if (cached) {
|
|
236
|
+
// Most recently used last, so the bound below drops the oldest.
|
|
237
|
+
chainTransactionTools.delete(key);
|
|
238
|
+
chainTransactionTools.set(key, cached);
|
|
239
|
+
return cached;
|
|
240
|
+
}
|
|
241
|
+
const schema = chainTransactionSchemaFor(catalog, fields);
|
|
242
|
+
const tool: PluginTool = {
|
|
243
|
+
name: 'request_chain_transaction',
|
|
244
|
+
description: chainTransactionDescription(catalog),
|
|
245
|
+
schema,
|
|
246
|
+
effect: 'write',
|
|
247
|
+
handler: (input, ctx) => requestChainTransaction(schema, input, ctx),
|
|
248
|
+
};
|
|
249
|
+
// Type lists come from Auth Hub capabilities (one or two per deployment)
|
|
250
|
+
// and described sets from sessions; the bound keeps the map small, the
|
|
251
|
+
// least recently used pair is built again when needed.
|
|
252
|
+
if (chainTransactionTools.size >= CHAIN_TRANSACTION_TOOL_CACHE)
|
|
253
|
+
chainTransactionTools.delete(chainTransactionTools.keys().next().value!);
|
|
254
|
+
chainTransactionTools.set(key, tool);
|
|
255
|
+
return tool;
|
|
256
|
+
}
|
|
257
|
+
const CHAIN_TRANSACTION_TOOL_CACHE = 32;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The message types described in a conversation (`describe_chain_messages`),
|
|
261
|
+
* per session id in the owner's own key-value rows, each with the run that
|
|
262
|
+
* first described it: durable across restarts of the owner object, kept
|
|
263
|
+
* while used (`DESCRIBED_IDLE_TTL_MS`), and at most `DESCRIBED_MAX_SESSIONS`
|
|
264
|
+
* sessions per owner. A new session starts with none.
|
|
265
|
+
*/
|
|
266
|
+
const DESCRIBED_NAMESPACE = 'hub-actions/chain-tx-described';
|
|
267
|
+
const DESCRIBED_IDLE_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
268
|
+
const DESCRIBED_MAX_SESSIONS = 200;
|
|
269
|
+
|
|
270
|
+
/** The stored `{type: runId}` record of a session (anything else: none). */
|
|
271
|
+
function describedRecord(
|
|
272
|
+
value: unknown,
|
|
273
|
+
): Partial<Record<ChainTxMessageType, string>> {
|
|
274
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
275
|
+
const record: Partial<Record<ChainTxMessageType, string>> = {};
|
|
276
|
+
for (const type of CHAIN_TX_MESSAGE_TYPES) {
|
|
277
|
+
const run: unknown = Reflect.get(value, type);
|
|
278
|
+
if (typeof run === 'string') record[type] = run;
|
|
279
|
+
}
|
|
280
|
+
return record;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The types whose rules a `describe_chain_messages` call in `messages` read:
|
|
285
|
+
* its arguments (on the AI message that made the call), counted only when
|
|
286
|
+
* its result is there too and is not an error.
|
|
287
|
+
*/
|
|
288
|
+
function describedInHistory(
|
|
289
|
+
messages: readonly BaseMessage[],
|
|
290
|
+
): Set<ChainTxMessageType> {
|
|
291
|
+
const calls = new Map<string, unknown>();
|
|
292
|
+
for (const message of messages)
|
|
293
|
+
if (AIMessage.isInstance(message))
|
|
294
|
+
for (const call of message.tool_calls ?? [])
|
|
295
|
+
if (call.name === 'describe_chain_messages' && call.id)
|
|
296
|
+
calls.set(call.id, call.args);
|
|
297
|
+
const described = new Set<ChainTxMessageType>();
|
|
298
|
+
for (const message of messages) {
|
|
299
|
+
if (
|
|
300
|
+
!ToolMessage.isInstance(message) ||
|
|
301
|
+
message.name !== 'describe_chain_messages' ||
|
|
302
|
+
message.status === 'error'
|
|
303
|
+
)
|
|
304
|
+
continue;
|
|
305
|
+
const args = describeChainMessagesSchema.safeParse(
|
|
306
|
+
calls.get(message.tool_call_id),
|
|
307
|
+
);
|
|
308
|
+
if (args.success) for (const type of args.data.types) described.add(type);
|
|
309
|
+
}
|
|
310
|
+
return described;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** This turn in the session: `${sessionId}|${runId}`. */
|
|
314
|
+
const runKeyOf = (ctx: RuntimeContext) =>
|
|
315
|
+
`${ctx.session.id}|${ctx.session.runId ?? ''}`;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Turns whose described types could not be stored: they fall back to every
|
|
319
|
+
* type's fields and no refusal (a store failure must not make chain cards
|
|
320
|
+
* unusable). Bounded; the oldest turn is dropped first.
|
|
321
|
+
*/
|
|
322
|
+
const UNSTORED_TURNS_MAX = 256;
|
|
323
|
+
const unstoredTurns = new Set<string>();
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The types this call may use (and whose fields the model is given), by
|
|
327
|
+
* one rule: described in the history this call sees (`messages`), or
|
|
328
|
+
* stored by an earlier run of the session. A type the current run stored
|
|
329
|
+
* counts only once its `describe_chain_messages` result is in that history,
|
|
330
|
+
* so a describe made in the same model response as the request (a parallel
|
|
331
|
+
* call) never lets the request through, whichever finishes first. Null when
|
|
332
|
+
* the session cannot remember (no store or session, or the store failed):
|
|
333
|
+
* every type then carries its fields and none is refused.
|
|
334
|
+
*/
|
|
335
|
+
async function describedChainTxTypes(
|
|
336
|
+
ctx: RuntimeContext,
|
|
337
|
+
messages: readonly BaseMessage[],
|
|
338
|
+
): Promise<ChainTxMessageType[] | null> {
|
|
339
|
+
if (!ctx.kv || !ctx.session.id || unstoredTurns.has(runKeyOf(ctx)))
|
|
340
|
+
return null;
|
|
341
|
+
let stored: Partial<Record<ChainTxMessageType, string>>;
|
|
342
|
+
try {
|
|
343
|
+
stored = describedRecord(
|
|
344
|
+
await ctx.kv.get(DESCRIBED_NAMESPACE, ctx.session.id),
|
|
345
|
+
);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
ctx.logger.warn(
|
|
348
|
+
`[hub-actions] described chain message types unreadable; every type keeps its fields: ${error instanceof Error ? error.message : String(error)}`,
|
|
349
|
+
);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const inHistory = describedInHistory(messages);
|
|
353
|
+
const runId = ctx.session.runId ?? '';
|
|
354
|
+
return CHAIN_TX_MESSAGE_TYPES.filter(
|
|
355
|
+
(type) =>
|
|
356
|
+
inHistory.has(type) ||
|
|
357
|
+
(stored[type] !== undefined && stored[type] !== runId),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Records the described types; a type keeps the run that first described it. */
|
|
362
|
+
async function rememberDescribed(
|
|
363
|
+
ctx: RuntimeContext,
|
|
364
|
+
types: readonly ChainTxMessageType[],
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
if (!ctx.kv || !ctx.session.id) return;
|
|
367
|
+
const runId = ctx.session.runId ?? '';
|
|
368
|
+
try {
|
|
369
|
+
await ctx.kv.update(
|
|
370
|
+
DESCRIBED_NAMESPACE,
|
|
371
|
+
ctx.session.id,
|
|
372
|
+
(current) => ({
|
|
373
|
+
...Object.fromEntries(types.map((type) => [type, runId])),
|
|
374
|
+
...describedRecord(current),
|
|
375
|
+
}),
|
|
376
|
+
{ idleTtlMs: DESCRIBED_IDLE_TTL_MS, maxEntries: DESCRIBED_MAX_SESSIONS },
|
|
377
|
+
);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
ctx.logger.warn(
|
|
380
|
+
`[hub-actions] described chain message types not stored; this turn gives every type's fields: ${error instanceof Error ? error.message : String(error)}`,
|
|
381
|
+
);
|
|
382
|
+
if (unstoredTurns.size >= UNSTORED_TURNS_MAX)
|
|
383
|
+
unstoredTurns.delete(unstoredTurns.values().next().value!);
|
|
384
|
+
unstoredTurns.add(runKeyOf(ctx));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The account's chain types for a turn, read once (by the request tool and
|
|
390
|
+
* then reused by `chainTxFieldsMiddleware` for every model call).
|
|
391
|
+
*/
|
|
392
|
+
const turnChainTxTypes = new WeakMap<
|
|
393
|
+
RuntimeContext,
|
|
394
|
+
Promise<readonly ChainTxMessageType[]>
|
|
395
|
+
>();
|
|
396
|
+
function chainTxTypesForTurn(
|
|
397
|
+
ctx: RuntimeContext,
|
|
398
|
+
): Promise<readonly ChainTxMessageType[]> {
|
|
399
|
+
let types = turnChainTxTypes.get(ctx);
|
|
400
|
+
if (!types) {
|
|
401
|
+
types = availableChainTxTypes(ctx);
|
|
402
|
+
turnChainTxTypes.set(ctx, types);
|
|
403
|
+
}
|
|
404
|
+
return types;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The function definition of a request tool, per description, built once. */
|
|
408
|
+
const toolDefinitions = new WeakMap<
|
|
409
|
+
PluginTool,
|
|
410
|
+
Map<string, Record<string, unknown>>
|
|
411
|
+
>();
|
|
412
|
+
function definitionOf(
|
|
413
|
+
pluginTool: PluginTool,
|
|
414
|
+
description: string,
|
|
415
|
+
): Record<string, unknown> {
|
|
416
|
+
let byDescription = toolDefinitions.get(pluginTool);
|
|
417
|
+
if (!byDescription) {
|
|
418
|
+
byDescription = new Map();
|
|
419
|
+
toolDefinitions.set(pluginTool, byDescription);
|
|
420
|
+
}
|
|
421
|
+
let definition = byDescription.get(description);
|
|
422
|
+
if (!definition) {
|
|
423
|
+
const { type, function: fn } = convertToOpenAITool(
|
|
424
|
+
tool(async () => '', {
|
|
425
|
+
name: pluginTool.name,
|
|
426
|
+
description,
|
|
427
|
+
schema: pluginTool.schema,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
// A plain object (a server-tool definition to LangChain), not a tool
|
|
431
|
+
// instance: LangChain refuses a new instance of a registered tool.
|
|
432
|
+
definition = { type, function: fn };
|
|
433
|
+
byDescription.set(description, definition);
|
|
434
|
+
}
|
|
435
|
+
return definition;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The per-turn tool is built before the turn's first model call, so a type
|
|
440
|
+
* described during the turn would reach the next model call without its
|
|
441
|
+
* fields, and a provider drops fields its schema does not declare (the
|
|
442
|
+
* 0.29.0 failure). Before each model call this gives the model the request
|
|
443
|
+
* tool for what is described by then (`describedChainTxTypes` over the
|
|
444
|
+
* call's messages), as its function definition with the registered tool's
|
|
445
|
+
* name and description; the registered tool, whose schema accepts the
|
|
446
|
+
* fields of every type, still runs the call. A tool the capability gate
|
|
447
|
+
* hid is not in the request, so nothing is added; a failure to read the
|
|
448
|
+
* store leaves the request as it is.
|
|
449
|
+
*/
|
|
450
|
+
function chainTxFieldsMiddleware(ctx: RuntimeContext) {
|
|
451
|
+
return createMiddleware({
|
|
452
|
+
name: 'HubActionsChainTxFieldsMiddleware',
|
|
453
|
+
wrapModelCall: async (request, handler) => {
|
|
454
|
+
const index = request.tools.findIndex(
|
|
455
|
+
(entry) => entry.name === 'request_chain_transaction',
|
|
456
|
+
);
|
|
457
|
+
const registered = request.tools[index];
|
|
458
|
+
if (!registered || typeof registered.description !== 'string')
|
|
459
|
+
return handler(request);
|
|
460
|
+
const described = await describedChainTxTypes(ctx, request.messages);
|
|
461
|
+
if (!described) return handler(request);
|
|
462
|
+
const current = chainTransactionTool(
|
|
463
|
+
await chainTxTypesForTurn(ctx),
|
|
464
|
+
described,
|
|
465
|
+
);
|
|
466
|
+
if (!current) return handler(request);
|
|
467
|
+
const tools = [...request.tools];
|
|
468
|
+
tools[index] = definitionOf(current, registered.description);
|
|
469
|
+
return handler({ ...request, tools });
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const chainTransactionTools = new Map<string, PluginTool>();
|
|
474
|
+
|
|
475
|
+
/** The request tool as the whole catalogue gives it (no account capabilities). */
|
|
341
476
|
export const CHAIN_TRANSACTION_TOOL_DESCRIPTION =
|
|
342
477
|
chainTransactionDescription(CHAIN_TX_CATALOG);
|
|
343
478
|
|
|
@@ -353,7 +488,7 @@ const describeChainMessagesSchema = z.strictObject({
|
|
|
353
488
|
});
|
|
354
489
|
|
|
355
490
|
export const DESCRIBE_CHAIN_MESSAGES_TOOL_DESCRIPTION =
|
|
356
|
-
'Read the full rules of chain-transaction message types before using them in request_chain_transaction: every field with its meaning, format and limits, what the runtime fills, and chain caveats. Ask only for the types you need. Also returns `availableTypes`: the types this account may use.';
|
|
491
|
+
'Read the full rules of chain-transaction message types before using them in request_chain_transaction: every field with its meaning, format and limits, what the runtime fills, and chain caveats. Ask only for the types you need: request_chain_transaction takes only types described in this conversation, and gives their fields from the next call on. Also returns `availableTypes`: the types this account may use.';
|
|
357
492
|
|
|
358
493
|
export type HubActionRequestResult =
|
|
359
494
|
| {
|
|
@@ -442,6 +577,12 @@ async function requestHubAction(
|
|
|
442
577
|
reason:
|
|
443
578
|
'Use request_chain_transaction with only the messages for a chain transaction.',
|
|
444
579
|
};
|
|
580
|
+
if (kind === 'ixo.bid.submit.v1' || kind === 'ixo.claim.submit.v1')
|
|
581
|
+
return {
|
|
582
|
+
status: 'unavailable',
|
|
583
|
+
reason:
|
|
584
|
+
'Applications and claims are prepared by the tools of the plugin that runs the claim collection, never with request_hub_action.',
|
|
585
|
+
};
|
|
445
586
|
const parsed = requestSchema.safeParse(input);
|
|
446
587
|
if (!parsed.success)
|
|
447
588
|
return { status: 'unavailable', reason: 'An operation is required.' };
|
|
@@ -497,11 +638,33 @@ async function requestHubAction(
|
|
|
497
638
|
}
|
|
498
639
|
|
|
499
640
|
async function requestChainTransaction(
|
|
641
|
+
schema: ReturnType<typeof chainTransactionSchemaFor>,
|
|
500
642
|
input: unknown,
|
|
501
643
|
ctx: RuntimeContext,
|
|
502
|
-
): Promise<HubActionRequestResult> {
|
|
503
|
-
const parsed =
|
|
644
|
+
): Promise<HubActionRequestResult | ToolMessage> {
|
|
645
|
+
const parsed = schema.safeParse(input);
|
|
504
646
|
if (!parsed.success) return chainTxShapeRefusal(issuesOf(parsed.error));
|
|
647
|
+
const described = await describedChainTxTypes(ctx, ctx.history.messages);
|
|
648
|
+
if (described) {
|
|
649
|
+
const unread = [
|
|
650
|
+
...new Set(
|
|
651
|
+
parsed.data.messages
|
|
652
|
+
.map(chainTxTypeOf)
|
|
653
|
+
.filter(
|
|
654
|
+
(type): type is ChainTxMessageType =>
|
|
655
|
+
type !== null && !described.includes(type),
|
|
656
|
+
),
|
|
657
|
+
),
|
|
658
|
+
];
|
|
659
|
+
if (unread.length)
|
|
660
|
+
// Not run, and not a failure: the repetition guard lets the same
|
|
661
|
+
// call through once the rules are read.
|
|
662
|
+
return toolNotRunMessage({
|
|
663
|
+
toolCallId: ctx.toolCallId ?? '',
|
|
664
|
+
name: 'request_chain_transaction',
|
|
665
|
+
content: `For you, not the person: nothing was sent and nothing is wrong. Before a card with ${unread.map((type) => `\`${type}\``).join(', ')}, read the rules: call describe_chain_messages with types ${JSON.stringify(unread)}, then call request_chain_transaction again with exactly the fields it lists. Do not mention this step to the person.`,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
505
668
|
const checked = strictChainTxMessages(parsed.data.messages);
|
|
506
669
|
if ('issues' in checked) return chainTxShapeRefusal(checked.issues);
|
|
507
670
|
if (ctx.config.HUB_ACTIONS_ENABLED !== 'true' || !ctx.hubActions)
|
|
@@ -563,7 +726,7 @@ function strictChainTxMessages(
|
|
|
563
726
|
return;
|
|
564
727
|
}
|
|
565
728
|
const result = CHAIN_TX_CATALOG[type].schema.safeParse(message);
|
|
566
|
-
if (result.success) checked.push(result.data);
|
|
729
|
+
if (result.success) checked.push(chainTxMessageInput(result.data));
|
|
567
730
|
else issues.push(...issuesOf(result.error, ['messages', index]));
|
|
568
731
|
});
|
|
569
732
|
return issues.length ? { issues } : { messages: checked };
|
|
@@ -621,9 +784,12 @@ function issuesOf(
|
|
|
621
784
|
|
|
622
785
|
/**
|
|
623
786
|
* The catalogue types this account may use: the Hub capabilities'
|
|
624
|
-
* `chainTx.messageTypes
|
|
625
|
-
*
|
|
626
|
-
*
|
|
787
|
+
* `chainTx.messageTypes`. None when chain transactions cannot be used
|
|
788
|
+
* here: not a channel turn, Hub actions off, chain transactions off for the
|
|
789
|
+
* account, a Hub at protocol level 5 (the 0.6.0 types only, as it lists
|
|
790
|
+
* them), or capabilities that cannot be read (the owner object answers its
|
|
791
|
+
* last good capabilities while a read fails, and does not retry a failed
|
|
792
|
+
* read for 30 s, so this never holds a turn for long).
|
|
627
793
|
*/
|
|
628
794
|
async function availableChainTxTypes(
|
|
629
795
|
ctx: RuntimeContext,
|
|
@@ -634,15 +800,15 @@ async function availableChainTxTypes(
|
|
|
634
800
|
!ctx.session.channel ||
|
|
635
801
|
!ctx.hubActions
|
|
636
802
|
)
|
|
637
|
-
return
|
|
803
|
+
return [];
|
|
638
804
|
let chainTx;
|
|
639
805
|
try {
|
|
640
|
-
chainTx = (await ctx.hubActions.capabilities()).chainTx;
|
|
806
|
+
chainTx = (await ctx.hubActions.capabilities({ turn: true })).chainTx;
|
|
641
807
|
} catch (error) {
|
|
642
808
|
ctx.logger.warn(
|
|
643
|
-
`[hub-actions] capabilities unavailable
|
|
809
|
+
`[hub-actions] capabilities unavailable, no chain transaction types this turn: ${error instanceof Error ? error.message : String(error)}`,
|
|
644
810
|
);
|
|
645
|
-
return
|
|
811
|
+
return [];
|
|
646
812
|
}
|
|
647
813
|
return CHAIN_TX_MESSAGE_TYPES.filter((type) =>
|
|
648
814
|
chainTx?.messageTypes.includes(type),
|
|
@@ -673,6 +839,7 @@ async function describeChainMessages(
|
|
|
673
839
|
status: 'unavailable',
|
|
674
840
|
reason: `Name 1 to ${CHAIN_TX_MAX_MESSAGES} different message types, each one of: ${CHAIN_TX_MESSAGE_TYPES.join(', ')}.`,
|
|
675
841
|
};
|
|
842
|
+
await rememberDescribed(ctx, parsed.data.types);
|
|
676
843
|
return {
|
|
677
844
|
status: 'known',
|
|
678
845
|
general: CHAIN_TX_GENERAL_RULES,
|
|
@@ -702,6 +869,51 @@ function refusalOf(error: unknown): HubActionRequestResult | null {
|
|
|
702
869
|
return null;
|
|
703
870
|
}
|
|
704
871
|
|
|
872
|
+
/**
|
|
873
|
+
* A receipt as the model reads it. A failure receipt keeps only its plain
|
|
874
|
+
* fields: the Hub's `message` (its words for the person) is quoted and
|
|
875
|
+
* labelled as data, and anything else the Hub adds, its technical `detail`
|
|
876
|
+
* included (for the review page only), is left out. A chain transaction's
|
|
877
|
+
* `message` is quoted the same way. Every other receipt passes as
|
|
878
|
+
* `@ixo/action-protocol` parsed it (exact keys, no free Hub text).
|
|
879
|
+
*/
|
|
880
|
+
export function modelReceipt(
|
|
881
|
+
receipt: ActionReceiptV1 | null,
|
|
882
|
+
): Record<string, unknown> | null {
|
|
883
|
+
if (!receipt) return null;
|
|
884
|
+
if (receipt.kind === 'ixo.submission.failed.v1')
|
|
885
|
+
return {
|
|
886
|
+
kind: receipt.kind,
|
|
887
|
+
actionId: receipt.actionId,
|
|
888
|
+
revision: receipt.revision,
|
|
889
|
+
digest: receipt.digest,
|
|
890
|
+
step: receipt.step,
|
|
891
|
+
message: quoteHubText('message', receipt.message),
|
|
892
|
+
failedAt: receipt.failedAt,
|
|
893
|
+
};
|
|
894
|
+
if (receipt.kind === 'ixo.chain.tx.v1' && receipt.message !== undefined)
|
|
895
|
+
return { ...receipt, message: quoteHubText('message', receipt.message) };
|
|
896
|
+
return { ...receipt };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** An outcome as the model reads it: its receipt through `modelReceipt`. */
|
|
900
|
+
export function modelOutcome(
|
|
901
|
+
outcome: ActionOutcomeV1 | null,
|
|
902
|
+
): Record<string, unknown> | null {
|
|
903
|
+
if (!outcome) return null;
|
|
904
|
+
return { ...outcome, receipt: modelReceipt(outcome.receipt) };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* What a `cancelled` outcome means. Auth Hub withdraws a card when the
|
|
909
|
+
* person cancels it, and also on its own: a chain transaction whose details
|
|
910
|
+
* changed on chain after the review (`409 review_changed`, reason
|
|
911
|
+
* `chain_changed`, shown on the review page only). The oracle gets
|
|
912
|
+
* `cancelled` either way, so the model must not say who withdrew it.
|
|
913
|
+
*/
|
|
914
|
+
export const CANCELLED_MEANING =
|
|
915
|
+
'Withdrawn before anything was sent: nothing happened on chain. The person may have cancelled it, or Auth Hub withdrew it because something it depended on changed after the review; do not say the person cancelled it. If they still want it, offer to prepare a fresh one.';
|
|
916
|
+
|
|
705
917
|
async function hubActionStatus(
|
|
706
918
|
input: unknown,
|
|
707
919
|
ctx: RuntimeContext,
|
|
@@ -713,11 +925,32 @@ async function hubActionStatus(
|
|
|
713
925
|
ctx.config.HUB_ACTIONS_ENABLED !== 'true'
|
|
714
926
|
)
|
|
715
927
|
return { status: 'unavailable' };
|
|
716
|
-
|
|
717
|
-
ctx.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
928
|
+
try {
|
|
929
|
+
const read = await ctx.hubActions.status(
|
|
930
|
+
ctx.user.did,
|
|
931
|
+
parsed.data.actionId,
|
|
932
|
+
);
|
|
933
|
+
if (!read.held)
|
|
934
|
+
return {
|
|
935
|
+
status: 'unavailable',
|
|
936
|
+
reason: 'No Hub action with this id was prepared for this owner.',
|
|
937
|
+
};
|
|
938
|
+
return {
|
|
939
|
+
status: 'known',
|
|
940
|
+
reviewStatus: read.reviewStatus,
|
|
941
|
+
operationStatus: read.operationStatus,
|
|
942
|
+
outcome: modelOutcome(read.outcome),
|
|
943
|
+
...(read.summary ? { summary: read.summary } : {}),
|
|
944
|
+
...(read.outcome?.status === 'cancelled'
|
|
945
|
+
? { meaning: CANCELLED_MEANING }
|
|
946
|
+
: {}),
|
|
947
|
+
};
|
|
948
|
+
} catch (error) {
|
|
949
|
+
// An Auth Hub that must be updated first: plain words, not a tool error.
|
|
950
|
+
const refused = refusalOf(error);
|
|
951
|
+
if (refused) return refused;
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
721
954
|
}
|
|
722
955
|
|
|
723
956
|
async function hubActionCapabilities(
|
|
@@ -731,7 +964,13 @@ async function hubActionCapabilities(
|
|
|
731
964
|
!ctx.hubActions
|
|
732
965
|
)
|
|
733
966
|
return { status: 'unavailable' };
|
|
734
|
-
|
|
967
|
+
try {
|
|
968
|
+
return await ctx.hubActions.capabilities();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
const refused = refusalOf(error);
|
|
971
|
+
if (refused) return refused;
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
735
974
|
}
|
|
736
975
|
|
|
737
976
|
async function hubActionOptions(
|
|
@@ -746,12 +985,19 @@ async function hubActionOptions(
|
|
|
746
985
|
!ctx.hubActions
|
|
747
986
|
)
|
|
748
987
|
return { status: 'unavailable' };
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
988
|
+
let options;
|
|
989
|
+
try {
|
|
990
|
+
options = await ctx.hubActions.options({
|
|
991
|
+
ownerDid: ctx.user.did,
|
|
992
|
+
provider: channel.provider,
|
|
993
|
+
bindingId: channel.bindingId,
|
|
994
|
+
bindingRevision: channel.bindingRevision,
|
|
995
|
+
});
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const refused = refusalOf(error);
|
|
998
|
+
if (refused) return refused;
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
755
1001
|
// The address is read off the owner's DID, so the model can name the
|
|
756
1002
|
// account the listed balances belong to without a lookup.
|
|
757
1003
|
return { ...options, ownerAddress: ixoAddressOf(ctx.user.did) };
|
|
@@ -764,6 +1010,8 @@ const manifest: PluginManifest = {
|
|
|
764
1010
|
whenToUse: [
|
|
765
1011
|
'The user requests a registered form submission or a coin send.',
|
|
766
1012
|
'The user asks to file or evaluate a claim, or to send coins or impact tokens on chain.',
|
|
1013
|
+
'The user asks to stake, unstake or move IXO, claim staking rewards, vote on a network proposal or retire impact tokens.',
|
|
1014
|
+
'The user asks to reserve or dispute a claim, create or change a claim collection, give or remove a permission, create or update an entity, or change the links of an identity.',
|
|
767
1015
|
'The user asks to review an exact private resource or to sign a statement.',
|
|
768
1016
|
'The user asks to connect Gmail or Calendar, send a reviewed email or add an event.',
|
|
769
1017
|
'The user asks for document access or to save a private note.',
|
|
@@ -792,6 +1040,23 @@ export class HubActionsPlugin extends OraclePlugin {
|
|
|
792
1040
|
env.AUTH_HUB_CHANNEL_SERVICE_KEY.length > 0
|
|
793
1041
|
);
|
|
794
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* request_chain_transaction is built per turn: its schema names only the
|
|
1045
|
+
* message types this account may use (Auth Hub capabilities, cached by
|
|
1046
|
+
* the owner object), so 33 types do not reach a model that may use 4.
|
|
1047
|
+
*/
|
|
1048
|
+
override async getRequestTools(ctx: RuntimeContext): Promise<PluginTool[]> {
|
|
1049
|
+
const [available, described] = await Promise.all([
|
|
1050
|
+
chainTxTypesForTurn(ctx),
|
|
1051
|
+
describedChainTxTypes(ctx, ctx.history.messages),
|
|
1052
|
+
]);
|
|
1053
|
+
const tool = chainTransactionTool(available, described);
|
|
1054
|
+
return tool ? [tool] : [];
|
|
1055
|
+
}
|
|
1056
|
+
/** Gives each model call the request tool with the fields described so far in the turn. */
|
|
1057
|
+
override getRequestMiddlewares(ctx: RuntimeContext): AgentMiddleware[] {
|
|
1058
|
+
return [chainTxFieldsMiddleware(ctx)];
|
|
1059
|
+
}
|
|
795
1060
|
override getTools(): PluginTool[] {
|
|
796
1061
|
return [
|
|
797
1062
|
{
|
|
@@ -825,17 +1090,10 @@ export class HubActionsPlugin extends OraclePlugin {
|
|
|
825
1090
|
effect: 'read',
|
|
826
1091
|
handler: describeChainMessages,
|
|
827
1092
|
},
|
|
828
|
-
{
|
|
829
|
-
name: 'request_chain_transaction',
|
|
830
|
-
description: CHAIN_TRANSACTION_TOOL_DESCRIPTION,
|
|
831
|
-
schema: chainTransactionSchema,
|
|
832
|
-
effect: 'write',
|
|
833
|
-
handler: requestChainTransaction,
|
|
834
|
-
},
|
|
835
1093
|
{
|
|
836
1094
|
name: 'hub_action_status',
|
|
837
1095
|
description:
|
|
838
|
-
'Read the verified status of a Hub action already prepared for this owner. Never executes.',
|
|
1096
|
+
'Read the verified status of a Hub action already prepared for this owner. Never executes. A `cancelled` outcome was withdrawn before anything was sent, by the person or by Auth Hub (`meaning` says so): never say the person cancelled it.',
|
|
839
1097
|
schema: statusSchema,
|
|
840
1098
|
effect: 'read',
|
|
841
1099
|
handler: hubActionStatus,
|