@ixo/oracle-runtime-workers 0.26.0 → 0.28.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/README.md +1 -0
- package/package.json +1 -1
- package/src/channels/auth.ts +2 -1
- package/src/channels/contract.ts +104 -0
- package/src/channels/replies.ts +58 -0
- package/src/channels/routing.ts +222 -0
- package/src/channels/turns.ts +1252 -16
- package/src/core/domain-context/turn-context.ts +1 -1
- package/src/core/env.ts +22 -4
- package/src/core/main-agent.ts +31 -5
- package/src/core/prompt-composer.ts +14 -0
- package/src/core/sub-agent-fallback.ts +3 -2
- package/src/core/user-identity.ts +67 -0
- package/src/do/contracts.ts +19 -0
- package/src/do/room-mirror.ts +8 -1
- package/src/do/run-coordinator.ts +27 -0
- package/src/do/transcript.ts +14 -11
- package/src/do/user-oracle-do.ts +269 -63
- package/src/memory/session-history-indexer.ts +442 -103
- package/src/memory/user-context-fetcher.ts +13 -0
- package/src/memory/user-context-invalidation.ts +2 -2
- package/src/plugins/action-proposals/evidence.ts +7 -8
- package/src/plugins/hub-actions/hub-actions.plugin.ts +6 -2
- package/src/plugins/index.ts +6 -2
- package/src/plugins/ixo-chain/blocksync.ts +214 -0
- package/src/plugins/ixo-chain/denoms.ts +189 -0
- package/src/plugins/ixo-chain/http.ts +388 -0
- package/src/plugins/ixo-chain/index.ts +1 -0
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +543 -0
- package/src/plugins/ixo-chain/queries.ts +401 -0
- package/src/plugins/ixo-chain/rest.ts +79 -0
- package/src/plugins/ixo-chain/summaries.ts +799 -0
- package/src/plugins/tasks/middleware.ts +4 -9
- package/src/shell/app.ts +70 -0
- package/src/sqlite/serialization.ts +20 -0
- package/src/sqlite/sessions-store.ts +43 -0
|
@@ -23,10 +23,7 @@ import type { BaseMessage } from '@langchain/core/messages';
|
|
|
23
23
|
import { createMiddleware, type AgentMiddleware } from 'langchain';
|
|
24
24
|
import type { Logger, OracleTasksSurface } from '../../plugin-api/types';
|
|
25
25
|
import { pendingApprovalOf } from '../../tasks/store';
|
|
26
|
-
import {
|
|
27
|
-
stripTurnTimeNote,
|
|
28
|
-
TURN_TIME_NOTE_KWARG,
|
|
29
|
-
} from '../../core/turn-time-note';
|
|
26
|
+
import { userContent } from '../../channels/replies';
|
|
30
27
|
|
|
31
28
|
const APPROVE = new Set([
|
|
32
29
|
'yes',
|
|
@@ -151,17 +148,15 @@ export function userDidFromContext(context: unknown): string | undefined {
|
|
|
151
148
|
|
|
152
149
|
/**
|
|
153
150
|
* Text of the latest HumanMessage — string content or joined text parts —
|
|
154
|
-
* as the user sent it: without the turn time note the runtime puts in front
|
|
151
|
+
* as the user sent it: without the turn time note the runtime puts in front,
|
|
152
|
+
* and without a quoted WhatsApp message (a quoted "yes" is not an approval).
|
|
155
153
|
*/
|
|
156
154
|
export function lastHumanText(messages: BaseMessage[]): string | null {
|
|
157
155
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
158
156
|
const message = messages[i];
|
|
159
157
|
if (!message || message.type !== 'human') continue;
|
|
160
158
|
return textOfContent(
|
|
161
|
-
|
|
162
|
-
message.content,
|
|
163
|
-
message.additional_kwargs?.[TURN_TIME_NOTE_KWARG],
|
|
164
|
-
),
|
|
159
|
+
userContent(message.content, message.additional_kwargs),
|
|
165
160
|
);
|
|
166
161
|
}
|
|
167
162
|
return null;
|
package/src/shell/app.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
ChannelError,
|
|
21
21
|
ChannelTurnBody,
|
|
22
|
+
ChannelControlBody,
|
|
22
23
|
channelRequestHash,
|
|
23
24
|
readChannelBody,
|
|
24
25
|
} from '../channels/contract';
|
|
@@ -247,6 +248,7 @@ export function rateLimitKey(
|
|
|
247
248
|
| 'user'
|
|
248
249
|
| 'poll'
|
|
249
250
|
| 'channel'
|
|
251
|
+
| 'channel-control'
|
|
250
252
|
| 'action'
|
|
251
253
|
| 'artifact'
|
|
252
254
|
| 'socket'
|
|
@@ -455,6 +457,74 @@ export function createShell(
|
|
|
455
457
|
);
|
|
456
458
|
});
|
|
457
459
|
|
|
460
|
+
app.post('/channels/control', async (c) => {
|
|
461
|
+
try {
|
|
462
|
+
if (parseChannelServiceDids(c.env.CHANNEL_SERVICE_DID).length === 0)
|
|
463
|
+
throw new ChannelError(
|
|
464
|
+
503,
|
|
465
|
+
'Channels are not configured',
|
|
466
|
+
'not_configured',
|
|
467
|
+
);
|
|
468
|
+
const raw = await readChannelBody(c.req.raw);
|
|
469
|
+
let json: unknown;
|
|
470
|
+
try {
|
|
471
|
+
json = JSON.parse(raw);
|
|
472
|
+
} catch {
|
|
473
|
+
throw new ChannelError(400, 'Invalid JSON request', 'invalid_request');
|
|
474
|
+
}
|
|
475
|
+
const parsed = ChannelControlBody.safeParse(json);
|
|
476
|
+
if (!parsed.success)
|
|
477
|
+
throw new ChannelError(
|
|
478
|
+
400,
|
|
479
|
+
'Invalid channel control',
|
|
480
|
+
'invalid_request',
|
|
481
|
+
);
|
|
482
|
+
const identity = await authenticateChannel(
|
|
483
|
+
c.req.raw.headers,
|
|
484
|
+
raw,
|
|
485
|
+
parsed.data,
|
|
486
|
+
channelAuthConfig(c.env),
|
|
487
|
+
);
|
|
488
|
+
if (
|
|
489
|
+
c.env.RATE_LIMIT &&
|
|
490
|
+
!(
|
|
491
|
+
await c.env.RATE_LIMIT.limit({
|
|
492
|
+
key: rateLimitKey(c.env, 'channel-control', identity.userDid),
|
|
493
|
+
})
|
|
494
|
+
).success
|
|
495
|
+
)
|
|
496
|
+
throw new ChannelError(429, 'Too many requests', 'rate_limited');
|
|
497
|
+
await assertActiveChannelBindingCached(identity, c.env);
|
|
498
|
+
const outcome = await userStub(c.env, identity.userDid).channelControl(
|
|
499
|
+
identity,
|
|
500
|
+
parsed.data,
|
|
501
|
+
await channelRequestHash(raw),
|
|
502
|
+
);
|
|
503
|
+
return outcome.ok
|
|
504
|
+
? c.json(outcome.result)
|
|
505
|
+
: c.json(
|
|
506
|
+
{ code: outcome.code, message: outcome.message },
|
|
507
|
+
outcome.status,
|
|
508
|
+
);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error instanceof ChannelError)
|
|
511
|
+
return c.json(
|
|
512
|
+
{ code: error.code, message: error.message },
|
|
513
|
+
error.status,
|
|
514
|
+
);
|
|
515
|
+
console.error(
|
|
516
|
+
`[channels] control failed: ${describeChannelFailure(error)}`,
|
|
517
|
+
);
|
|
518
|
+
return c.json(
|
|
519
|
+
{
|
|
520
|
+
code: 'unavailable',
|
|
521
|
+
message: 'Channel control is temporarily unavailable',
|
|
522
|
+
},
|
|
523
|
+
503,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
458
528
|
app.use('/api/internal/actions/outcomes', limitBody(64 * 1024));
|
|
459
529
|
// Auth Hub's signed outcome callbacks: on whenever Hub actions are
|
|
460
530
|
// configured. The signature is checked against the Hub's own did:web key
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* circular-safe variant, typed.
|
|
14
14
|
*/
|
|
15
15
|
import { TURN_TIME_NOTE_KWARG } from '../core/turn-time-note';
|
|
16
|
+
import { CHANNEL_REPLY_NOTE_KWARG } from '../channels/replies';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* `additional_kwargs` key of the tool calls a mid-turn summary carries for
|
|
@@ -259,6 +260,11 @@ export interface CleanAdditionalKwargs {
|
|
|
259
260
|
lc_source?: string;
|
|
260
261
|
/** The exact time note the turn's content starts with (see core/turn-time-note.ts). */
|
|
261
262
|
turn_time_note?: string;
|
|
263
|
+
/** The exact quoted-reply note after it (see channels/replies.ts). */
|
|
264
|
+
channel_reply_note?: string;
|
|
265
|
+
/** Where a channel message came from, and what it replied to, as the turn recorded them. */
|
|
266
|
+
'org.ixo.qi.origin'?: unknown;
|
|
267
|
+
'org.ixo.qi.reply'?: unknown;
|
|
262
268
|
/** Tool calls of the turn a mid-turn summary condensed (newest kept, capped). */
|
|
263
269
|
turn_carry?: unknown[];
|
|
264
270
|
reasoning?: string;
|
|
@@ -361,6 +367,20 @@ export function cleanAdditionalKwargs(
|
|
|
361
367
|
const turnTimeNote = additionalKwargs[TURN_TIME_NOTE_KWARG];
|
|
362
368
|
if (typeof turnTimeNote === 'string')
|
|
363
369
|
cleaned[TURN_TIME_NOTE_KWARG] = turnTimeNote;
|
|
370
|
+
// Kept for the same reason: without it the quoted text would read as the
|
|
371
|
+
// person's own words in the transcript, in memory indexing and in the
|
|
372
|
+
// evidence a proposal is checked against.
|
|
373
|
+
const replyNote = additionalKwargs[CHANNEL_REPLY_NOTE_KWARG];
|
|
374
|
+
if (typeof replyNote === 'string')
|
|
375
|
+
cleaned[CHANNEL_REPLY_NOTE_KWARG] = replyNote;
|
|
376
|
+
// Channel provenance, read back typed by the transcript (ChannelOrigin,
|
|
377
|
+
// ChannelReplyReference); anything malformed is dropped there.
|
|
378
|
+
for (const key of ['org.ixo.qi.origin', 'org.ixo.qi.reply'] as const)
|
|
379
|
+
if (
|
|
380
|
+
typeof additionalKwargs[key] === 'object' &&
|
|
381
|
+
additionalKwargs[key] !== null
|
|
382
|
+
)
|
|
383
|
+
cleaned[key] = additionalKwargs[key];
|
|
364
384
|
// Kept so a run resumed after the summary still knows what this turn ran.
|
|
365
385
|
const carry = cappedTurnCarry(additionalKwargs[TURN_CARRY_KWARG]);
|
|
366
386
|
if (carry !== undefined) cleaned[TURN_CARRY_KWARG] = carry;
|
|
@@ -65,6 +65,9 @@ type SessionRow = {
|
|
|
65
65
|
slack_thread_ts: string | null;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/** Ids per statement in `summaries`. */
|
|
69
|
+
const SUMMARY_CHUNK = 50;
|
|
70
|
+
|
|
68
71
|
type SessionRowWithTotal = SessionRow & { total: number };
|
|
69
72
|
|
|
70
73
|
const SESSION_COLUMNS = `session_id, title, last_updated_at, created_at, oracle_name,
|
|
@@ -242,6 +245,46 @@ export class SessionsStore {
|
|
|
242
245
|
return rooms;
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Title and times of many sessions, without their heavier columns, in
|
|
250
|
+
* statements of at most `SUMMARY_CHUNK` ids (a Durable Object binds at most
|
|
251
|
+
* 100 parameters). Sessions without a row are absent from the map.
|
|
252
|
+
*/
|
|
253
|
+
async summaries(
|
|
254
|
+
sessionIds: readonly string[],
|
|
255
|
+
): Promise<
|
|
256
|
+
Map<
|
|
257
|
+
string,
|
|
258
|
+
{ title: string | null; createdAt: string; lastUpdatedAt: string }
|
|
259
|
+
>
|
|
260
|
+
> {
|
|
261
|
+
const found = new Map<
|
|
262
|
+
string,
|
|
263
|
+
{ title: string | null; createdAt: string; lastUpdatedAt: string }
|
|
264
|
+
>();
|
|
265
|
+
if (sessionIds.length === 0) return found;
|
|
266
|
+
await this.setup();
|
|
267
|
+
for (let at = 0; at < sessionIds.length; at += SUMMARY_CHUNK) {
|
|
268
|
+
const chunk = sessionIds.slice(at, at + SUMMARY_CHUNK);
|
|
269
|
+
const rows = await this.db.exec<{
|
|
270
|
+
session_id: string;
|
|
271
|
+
title: string | null;
|
|
272
|
+
created_at: string;
|
|
273
|
+
last_updated_at: string;
|
|
274
|
+
}>(
|
|
275
|
+
`SELECT session_id, title, created_at, last_updated_at FROM sessions WHERE session_id IN (${chunk.map(() => '?').join(', ')})`,
|
|
276
|
+
[...chunk],
|
|
277
|
+
);
|
|
278
|
+
for (const row of rows)
|
|
279
|
+
found.set(row.session_id, {
|
|
280
|
+
title: row.title,
|
|
281
|
+
createdAt: row.created_at,
|
|
282
|
+
lastUpdatedAt: row.last_updated_at,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
|
|
245
288
|
async getSession(sessionId: string): Promise<SessionRecord | undefined> {
|
|
246
289
|
await this.setup();
|
|
247
290
|
const row = await this.db.get<SessionRow>(
|