@ixo/oracle-runtime-workers 0.41.0 → 0.41.1
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 +2 -2
- package/src/plugins/asks/skills.ts +11 -18
- package/src/plugins/asks/testing.ts +0 -1
- package/src/plugins/asks/tools.ts +39 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"protobufjs": "^7.6.5",
|
|
68
68
|
"@ixo/topic-protocol": "1.0.0-rc.8",
|
|
69
69
|
"survey-core": "2.3.11",
|
|
70
|
+
"@ixo/ucan": "2.3.0",
|
|
70
71
|
"@ixo/common": "1.8.0",
|
|
71
72
|
"@ixo/ixo-transaction": "0.2.0",
|
|
72
|
-
"@ixo/ucan": "2.3.0",
|
|
73
73
|
"@ixo/action-interactions": "0.1.1"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* The asks skills, read at runtime from the released ixo-plugins repo so a
|
|
3
3
|
* newly published skill needs no oracle release: mainnet reads
|
|
4
4
|
* `plugins/asks/skills/<name>/SKILL.md`, testnet and devnet
|
|
5
|
-
* `plugins/asks-testnet/...` (practice money). The names come from
|
|
6
|
-
* folder
|
|
5
|
+
* `plugins/asks-testnet/...` (practice money). The names come from
|
|
6
|
+
* `plugins/<folder>/skills/index.json`.
|
|
7
7
|
*
|
|
8
8
|
* Never on a turn's path: `skills()` answers from memory, then the Workers
|
|
9
9
|
* cache (across isolate restarts), then the bundled copy, and refreshes in
|
|
10
10
|
* the background once the copy is an hour old, with `If-None-Match` so an
|
|
11
|
-
* unchanged file costs a 304
|
|
12
|
-
*
|
|
11
|
+
* unchanged file costs a 304. Everything is read from
|
|
12
|
+
* raw.githubusercontent.com. Any failure keeps what is there.
|
|
13
13
|
*
|
|
14
14
|
* The bundled copy (`skills.bundled.json`) was taken from ixo-plugins `main`;
|
|
15
15
|
* refresh it by re-reading the same two folders when the skills change a lot.
|
|
@@ -158,25 +158,18 @@ export class AsksSkills {
|
|
|
158
158
|
private async pull(): Promise<void> {
|
|
159
159
|
const { folder } = this.deps;
|
|
160
160
|
const before = this.snapshot ?? bundledSnapshot(folder);
|
|
161
|
+
// Not GitHub's API: unauthenticated, it answers 403 on Cloudflare's shared IPs.
|
|
161
162
|
const list = await this.deps.fetch(
|
|
162
|
-
`https://
|
|
163
|
-
{
|
|
164
|
-
headers: {
|
|
165
|
-
accept: 'application/vnd.github+json',
|
|
166
|
-
'user-agent': 'ixo-oracle',
|
|
167
|
-
...(before.listEtag ? { 'if-none-match': before.listEtag } : {}),
|
|
168
|
-
},
|
|
169
|
-
},
|
|
163
|
+
`https://raw.githubusercontent.com/${REPO}/${BRANCH}/plugins/${folder}/skills/index.json`,
|
|
164
|
+
{ headers: before.listEtag ? { 'if-none-match': before.listEtag } : {} },
|
|
170
165
|
);
|
|
171
166
|
let names: string[];
|
|
172
167
|
if (list.status === 304) names = Object.keys(before.files);
|
|
173
168
|
else if (list.ok) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
names =
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
} else throw new Error(`listing answered ${list.status}`);
|
|
169
|
+
const index = await list.json<unknown>();
|
|
170
|
+
if (!Array.isArray(index)) throw new Error('index.json is not a list');
|
|
171
|
+
names = index.filter((name): name is string => typeof name === 'string');
|
|
172
|
+
} else throw new Error(`index.json answered ${list.status}`);
|
|
180
173
|
names = names.filter((name) => SKILL_NAME.test(name)).slice(0, MAX_SKILLS);
|
|
181
174
|
|
|
182
175
|
const files: SkillSnapshot['files'] = {};
|
|
@@ -470,8 +470,13 @@ function connectorTool(
|
|
|
470
470
|
};
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
/**
|
|
474
|
-
|
|
473
|
+
/**
|
|
474
|
+
* The connector's tools for this turn: cached definitions, a lazy client.
|
|
475
|
+
* `null`: signed out. `'loading'`: none cached yet, being fetched for the next message.
|
|
476
|
+
*/
|
|
477
|
+
async function connectorTools(
|
|
478
|
+
turn: AsksTurn,
|
|
479
|
+
): Promise<PluginTool[] | null | 'loading'> {
|
|
475
480
|
const { mcpUrl } = turn;
|
|
476
481
|
const cached = defsCache.get(mcpUrl);
|
|
477
482
|
const list = async (token: string) => {
|
|
@@ -479,8 +484,30 @@ async function connectorTools(turn: AsksTurn): Promise<PluginTool[] | null> {
|
|
|
479
484
|
defsCache.set(mcpUrl, { defs: fresh, expiresAt: turn.now() + DEFS_TTL_MS });
|
|
480
485
|
return fresh;
|
|
481
486
|
};
|
|
487
|
+
const refresh = (background: (work: Promise<unknown>) => void) => {
|
|
488
|
+
if (defsRefreshing.has(mcpUrl)) return;
|
|
489
|
+
defsRefreshing.add(mcpUrl);
|
|
490
|
+
const work = turn.conn
|
|
491
|
+
.token()
|
|
492
|
+
.then(async (read) => {
|
|
493
|
+
if (read.ok) await list(read.access);
|
|
494
|
+
else if (read.reason !== 'unreachable') await markSeen(turn, null);
|
|
495
|
+
})
|
|
496
|
+
.catch((error: unknown) =>
|
|
497
|
+
turn.ctx.logger.warn(
|
|
498
|
+
`[asks] could not list asks tools: ${String(error)}`,
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
.finally(() => defsRefreshing.delete(mcpUrl));
|
|
502
|
+
background(work);
|
|
503
|
+
};
|
|
482
504
|
let defs = cached?.defs;
|
|
483
505
|
if (!defs) {
|
|
506
|
+
// Never hold a turn on asks.world: the full list is there from the next message.
|
|
507
|
+
if (turn.ctx.background) {
|
|
508
|
+
refresh(turn.ctx.background);
|
|
509
|
+
return 'loading';
|
|
510
|
+
}
|
|
484
511
|
const read = await turn.conn.token();
|
|
485
512
|
if (!read.ok) {
|
|
486
513
|
if (read.reason === 'unreachable')
|
|
@@ -489,19 +516,9 @@ async function connectorTools(turn: AsksTurn): Promise<PluginTool[] | null> {
|
|
|
489
516
|
return null;
|
|
490
517
|
}
|
|
491
518
|
defs = await list(read.access);
|
|
492
|
-
} else if (
|
|
493
|
-
cached &&
|
|
494
|
-
cached.expiresAt <= turn.now() &&
|
|
495
|
-
!defsRefreshing.has(mcpUrl)
|
|
496
|
-
) {
|
|
519
|
+
} else if (cached && cached.expiresAt <= turn.now() && turn.ctx.background) {
|
|
497
520
|
// Stale definitions keep serving while they refresh in the background.
|
|
498
|
-
|
|
499
|
-
const work = turn.conn
|
|
500
|
-
.token()
|
|
501
|
-
.then((read) => (read.ok ? list(read.access) : undefined))
|
|
502
|
-
.catch(() => undefined)
|
|
503
|
-
.finally(() => defsRefreshing.delete(mcpUrl));
|
|
504
|
-
turn.ctx.background?.(work);
|
|
521
|
+
refresh(turn.ctx.background);
|
|
505
522
|
}
|
|
506
523
|
const session = new AsksSession(turn);
|
|
507
524
|
return defs.map((def) => connectorTool(turn, session, def));
|
|
@@ -710,5 +727,13 @@ export async function asksTools(turn: AsksTurn): Promise<PluginTool[]> {
|
|
|
710
727
|
return [];
|
|
711
728
|
});
|
|
712
729
|
if (remote === null) return base;
|
|
730
|
+
if (remote === 'loading') {
|
|
731
|
+
const status = base[1]!;
|
|
732
|
+
base[1] = {
|
|
733
|
+
...status,
|
|
734
|
+
description: `${status.description} asks.world's other actions are still loading and are there from the next message: never say asks cannot do something.`,
|
|
735
|
+
};
|
|
736
|
+
return [...base, disconnectTool(turn), attachTool(turn)];
|
|
737
|
+
}
|
|
713
738
|
return [...base, disconnectTool(turn), attachTool(turn), ...remote];
|
|
714
739
|
}
|