@ixo/oracle-runtime-workers 0.30.0 → 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/blocksync.ts +6 -2
- package/src/plugins/ixo-chain/http.ts +16 -8
- 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 +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
package/src/channels/turns.ts
CHANGED
|
@@ -162,6 +162,14 @@ const CHANNEL_TOPIC_LOOKBACK = 5;
|
|
|
162
162
|
/** Ids per statement: a Durable Object binds at most 100 parameters. */
|
|
163
163
|
const CHANNEL_IDS_PER_STATEMENT = 50;
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The text the gateway sends for a file with no caption: `[photo]`,
|
|
167
|
+
* `[video]`, `[audio]` or `[document: <name>]`.
|
|
168
|
+
*/
|
|
169
|
+
export function isAttachmentPlaceholder(message: string): boolean {
|
|
170
|
+
return /^\[(?:photo|video|audio)\]$|^\[document: [^\n]*\]$/.test(message);
|
|
171
|
+
}
|
|
172
|
+
|
|
165
173
|
function clipCodePoints(text: string, limit: number): string {
|
|
166
174
|
const points = Array.from(text);
|
|
167
175
|
return points.length > limit ? points.slice(0, limit).join('') : text;
|
|
@@ -1463,7 +1471,14 @@ export class ChannelTurns {
|
|
|
1463
1471
|
assignment.destination = { kind: 'session', sessionId: session };
|
|
1464
1472
|
} else if (mode === 'on')
|
|
1465
1473
|
assignment.destination = { kind: 'clarify', text: ambiguousThreadText };
|
|
1466
|
-
} else
|
|
1474
|
+
} else if (
|
|
1475
|
+
input.attachments?.length &&
|
|
1476
|
+
isAttachmentPlaceholder(input.message)
|
|
1477
|
+
)
|
|
1478
|
+
// A file with no caption, only the gateway's placeholder: nothing to
|
|
1479
|
+
// classify, so it stays in the current conversation.
|
|
1480
|
+
assignment.reason = 'attachment-placeholder';
|
|
1481
|
+
else {
|
|
1467
1482
|
const candidates = await this.routingCandidates(input, baseline);
|
|
1468
1483
|
if (candidates.length) {
|
|
1469
1484
|
const evidence = {
|
|
@@ -1516,6 +1531,10 @@ export class ChannelTurns {
|
|
|
1516
1531
|
} else assignment.reason = 'shadow';
|
|
1517
1532
|
}
|
|
1518
1533
|
}
|
|
1534
|
+
// A clarifying question is answered instead of the message, so the files
|
|
1535
|
+
// would be dropped: a turn that carries files stays where it is.
|
|
1536
|
+
if (input.attachments?.length && assignment.destination.kind === 'clarify')
|
|
1537
|
+
assignment.destination = { kind: 'session', sessionId: baseline };
|
|
1519
1538
|
await this.serialize(async () => {
|
|
1520
1539
|
await this.assertNotCancelled(input);
|
|
1521
1540
|
await this.db.run(
|
|
@@ -1861,6 +1880,9 @@ export class ChannelTurns {
|
|
|
1861
1880
|
bindingId: input.bindingId,
|
|
1862
1881
|
bindingRevision: input.bindingRevision,
|
|
1863
1882
|
remoteMessageRef: input.remoteMessageRef,
|
|
1883
|
+
...(input.attachments?.length
|
|
1884
|
+
? { attachments: input.attachments }
|
|
1885
|
+
: {}),
|
|
1864
1886
|
...(replyContext ? { replyContext } : {}),
|
|
1865
1887
|
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
1866
1888
|
...(routing?.destination.kind === 'clarify'
|
|
@@ -9,6 +9,7 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
|
|
9
9
|
import type { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
10
10
|
import type { ReactAgent } from 'langchain';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
+
import { ChannelAttachment } from '../channels/contract';
|
|
12
13
|
import type {
|
|
13
14
|
ChatOpenAIFields,
|
|
14
15
|
MergedConfig,
|
|
@@ -48,6 +49,7 @@ export const mainAgentRequestContextSchema = z.object({
|
|
|
48
49
|
provider: z.string(),
|
|
49
50
|
bindingId: z.string(),
|
|
50
51
|
bindingRevision: z.number().int().positive(),
|
|
52
|
+
attachments: z.array(ChannelAttachment).optional(),
|
|
51
53
|
})
|
|
52
54
|
.optional(),
|
|
53
55
|
wsId: z.string().optional(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChannelAttachment } from '../channels/contract';
|
|
1
2
|
import {
|
|
2
3
|
UNAVAILABLE_DECISION_EVALUATOR,
|
|
3
4
|
type DecisionEvaluateOptions,
|
|
@@ -330,6 +331,8 @@ export interface RuntimeSessionContext {
|
|
|
330
331
|
provider: string;
|
|
331
332
|
bindingId: string;
|
|
332
333
|
bindingRevision: number;
|
|
334
|
+
/** Files sent with this message (see `RuntimeContext.session.channel`). */
|
|
335
|
+
attachments?: readonly ChannelAttachment[];
|
|
333
336
|
};
|
|
334
337
|
roomId?: string;
|
|
335
338
|
surface?: SessionSurface;
|
package/src/delivery/plan.ts
CHANGED
|
@@ -165,7 +165,9 @@ export function draftReplyPlan(input: {
|
|
|
165
165
|
for (const call of step.toolCalls) {
|
|
166
166
|
if (
|
|
167
167
|
call.name === 'request_hub_action' ||
|
|
168
|
-
call.name === 'request_chain_transaction'
|
|
168
|
+
call.name === 'request_chain_transaction' ||
|
|
169
|
+
// The Yoma plugin's application and claim cards.
|
|
170
|
+
call.name === 'yoma_form'
|
|
169
171
|
) {
|
|
170
172
|
try {
|
|
171
173
|
const result = JSON.parse(input.toolResults.get(call.id) ?? 'null');
|
package/src/do/contracts.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ChannelAttachment,
|
|
2
3
|
ChannelControlInput,
|
|
3
4
|
ChannelControlOutcome,
|
|
4
5
|
ChannelReplyContext,
|
|
@@ -400,6 +401,8 @@ export interface TurnRequest {
|
|
|
400
401
|
replyTo?: ChannelReplyReference;
|
|
401
402
|
/** Host-generated direct reply: no agent or tools run for an ambiguous destination. */
|
|
402
403
|
routingClarification?: string;
|
|
404
|
+
/** Files the person sent with this message, as the gateway serves them. */
|
|
405
|
+
attachments?: ChannelAttachment[];
|
|
403
406
|
};
|
|
404
407
|
executionProfile?: TaskExecutionProfile;
|
|
405
408
|
identity: TurnIdentity;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attachments of one turn attempt: which files the run reads, how a
|
|
3
|
+
* channel file's `channel:` reference becomes its link (server-side, never
|
|
4
|
+
* through the model), and when nothing is downloaded at all.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
attachmentMetaFor,
|
|
8
|
+
channelAttachmentRef,
|
|
9
|
+
channelMediaOrigins,
|
|
10
|
+
createChannelMediaSource,
|
|
11
|
+
parseAttachmentInputs,
|
|
12
|
+
type AttachmentInput,
|
|
13
|
+
type ChannelAttachmentLinkStore,
|
|
14
|
+
type ChannelMediaLink,
|
|
15
|
+
type ChannelMediaSource,
|
|
16
|
+
type PreparedAttachments,
|
|
17
|
+
} from '../attachments';
|
|
18
|
+
import { parseChannelServiceDids } from '../channels/auth';
|
|
19
|
+
import {
|
|
20
|
+
channelAttachmentInputs,
|
|
21
|
+
type ChannelAttachment,
|
|
22
|
+
} from '../channels/contract';
|
|
23
|
+
import type { AttachmentMeta } from '../sqlite/serialization';
|
|
24
|
+
|
|
25
|
+
/** The part of a turn request the attachment inputs come from. */
|
|
26
|
+
interface TurnFiles {
|
|
27
|
+
client: string;
|
|
28
|
+
attachments?: unknown;
|
|
29
|
+
channel?: { bindingId: string; attachments?: readonly ChannelAttachment[] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What the run reads: a channel turn's files by their `channel:` reference
|
|
34
|
+
* (from the gateway's list on the request), otherwise the request body's
|
|
35
|
+
* attachments as the Portal and Matrix send them. A body can never name a
|
|
36
|
+
* `channel:` reference itself.
|
|
37
|
+
*/
|
|
38
|
+
export function turnAttachmentInputs(
|
|
39
|
+
req: TurnFiles,
|
|
40
|
+
bodyAttachments: unknown,
|
|
41
|
+
): AttachmentInput[] {
|
|
42
|
+
const files = req.client === 'channel' ? req.channel?.attachments : undefined;
|
|
43
|
+
if (files?.length) return channelAttachmentInputs(files);
|
|
44
|
+
return parseAttachmentInputs(bodyAttachments ?? req.attachments);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A channel turn's files as the links the owner object resolves references to. */
|
|
48
|
+
export function channelTurnLinks(
|
|
49
|
+
req: TurnFiles,
|
|
50
|
+
): Array<ChannelMediaLink & { ref: string }> {
|
|
51
|
+
if (req.client !== 'channel' || !req.channel?.attachments?.length) return [];
|
|
52
|
+
const bindingId = req.channel.bindingId;
|
|
53
|
+
return req.channel.attachments.map((file) => ({
|
|
54
|
+
ref: channelAttachmentRef(file.sha256),
|
|
55
|
+
url: file.url,
|
|
56
|
+
sha256: file.sha256,
|
|
57
|
+
bindingId,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The channel source of a turn in `sessionId`: a reference resolves to the
|
|
63
|
+
* turn's own files first, then to the links kept for the session; only the
|
|
64
|
+
* gateways of `CHANNEL_SERVICE_DID` are fetched from.
|
|
65
|
+
*/
|
|
66
|
+
export function turnChannelMediaSource(input: {
|
|
67
|
+
sessionId: string;
|
|
68
|
+
turnLinks: ReadonlyArray<ChannelMediaLink & { ref: string }>;
|
|
69
|
+
store: Pick<ChannelAttachmentLinkStore, 'get'> | null;
|
|
70
|
+
channelServiceDid: string | undefined;
|
|
71
|
+
}): ChannelMediaSource {
|
|
72
|
+
return createChannelMediaSource({
|
|
73
|
+
origins: channelMediaOrigins(
|
|
74
|
+
parseChannelServiceDids(input.channelServiceDid),
|
|
75
|
+
),
|
|
76
|
+
resolve: async (ref) => {
|
|
77
|
+
const own = input.turnLinks.find((link) => link.ref === ref);
|
|
78
|
+
if (own) return own;
|
|
79
|
+
return input.store?.get(input.sessionId, ref);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The turn's prepared attachments, or none: a resumed attempt hands the
|
|
86
|
+
* graph no new input (the person's message, with its files, is already in
|
|
87
|
+
* the checkpoint), so nothing is downloaded again; its metadata is still
|
|
88
|
+
* known for `view_attachment`.
|
|
89
|
+
*/
|
|
90
|
+
export async function prepareTurnAttachments(input: {
|
|
91
|
+
inputs: readonly AttachmentInput[];
|
|
92
|
+
resumed: boolean;
|
|
93
|
+
prepare: () => Promise<PreparedAttachments>;
|
|
94
|
+
}): Promise<{ prepared: PreparedAttachments | null; metas: AttachmentMeta[] }> {
|
|
95
|
+
if (input.inputs.length === 0) return { prepared: null, metas: [] };
|
|
96
|
+
if (input.resumed)
|
|
97
|
+
return {
|
|
98
|
+
prepared: null,
|
|
99
|
+
metas: input.inputs.map((attachment) => attachmentMetaFor(attachment)),
|
|
100
|
+
};
|
|
101
|
+
const prepared = await input.prepare();
|
|
102
|
+
return { prepared, metas: prepared.metas };
|
|
103
|
+
}
|
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -46,7 +46,11 @@ import type {
|
|
|
46
46
|
ActionProposalSubject,
|
|
47
47
|
} from '@ixo/ixo-transaction/action-proposals';
|
|
48
48
|
import { ChannelTurns } from '../channels/turns';
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
HubActionClient,
|
|
51
|
+
type HubOperationStatus,
|
|
52
|
+
type HubReviewStatus,
|
|
53
|
+
} from '../hub-actions/client';
|
|
50
54
|
import { chainTxOperation, prepareHubAction } from '../hub-actions/request';
|
|
51
55
|
import {
|
|
52
56
|
bankReceiptSummary,
|
|
@@ -126,6 +130,7 @@ import {
|
|
|
126
130
|
offloadInlinePayloads,
|
|
127
131
|
parseAttachmentInputs,
|
|
128
132
|
prepareAttachments,
|
|
133
|
+
ChannelAttachmentLinkStore,
|
|
129
134
|
type AttachmentViewSurface,
|
|
130
135
|
type MatrixMediaSource,
|
|
131
136
|
type ViewAttachmentDeps,
|
|
@@ -135,6 +140,12 @@ import {
|
|
|
135
140
|
readBytesCapped,
|
|
136
141
|
AttachmentTextCacheStore,
|
|
137
142
|
} from '../attachments';
|
|
143
|
+
import {
|
|
144
|
+
channelTurnLinks,
|
|
145
|
+
prepareTurnAttachments,
|
|
146
|
+
turnAttachmentInputs,
|
|
147
|
+
turnChannelMediaSource,
|
|
148
|
+
} from './turn-attachments';
|
|
138
149
|
import type { RuntimeCore } from '../core';
|
|
139
150
|
import {
|
|
140
151
|
createLlmAdapter,
|
|
@@ -430,6 +441,7 @@ import {
|
|
|
430
441
|
} from './ucan-service';
|
|
431
442
|
import type {
|
|
432
443
|
ChainTxMessageInput,
|
|
444
|
+
HubActionStatusRead,
|
|
433
445
|
UcanDelegation,
|
|
434
446
|
UserContextData,
|
|
435
447
|
} from '../plugin-api/types';
|
|
@@ -564,6 +576,7 @@ function hasUserContext(value: unknown): boolean {
|
|
|
564
576
|
function admissionApplies(req: TurnRequest): boolean {
|
|
565
577
|
return (
|
|
566
578
|
!req.attachments?.length &&
|
|
579
|
+
!req.channel?.attachments?.length &&
|
|
567
580
|
!req.taskRunId &&
|
|
568
581
|
!req.sessionId.startsWith(TASK_SESSION_PREFIX) &&
|
|
569
582
|
req.roomKind !== 'group'
|
|
@@ -1058,6 +1071,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1058
1071
|
private lastActionPruneAt: number | null = null;
|
|
1059
1072
|
/** Text `view_attachment` extracted, per session (`attachments/view-cache.ts`). */
|
|
1060
1073
|
private attachmentTextCache: AttachmentTextCacheStore | null = null;
|
|
1074
|
+
/** The links behind the `channel:` references of each session's files. */
|
|
1075
|
+
private channelAttachmentLinks: ChannelAttachmentLinkStore | null = null;
|
|
1061
1076
|
/** Large texts of document ingestion, in memory (a few, minutes). */
|
|
1062
1077
|
private readonly ingestTextLru = new IngestTextLru();
|
|
1063
1078
|
/**
|
|
@@ -2019,6 +2034,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2019
2034
|
});
|
|
2020
2035
|
await this.resultStore.setup();
|
|
2021
2036
|
this.attachmentTextCache = new AttachmentTextCacheStore(liveDb, console);
|
|
2037
|
+
this.channelAttachmentLinks = new ChannelAttachmentLinkStore(
|
|
2038
|
+
liveDb,
|
|
2039
|
+
console,
|
|
2040
|
+
);
|
|
2022
2041
|
// Constructing it touches no SQL. Only an observe turn records (and so
|
|
2023
2042
|
// creates `domain_context_runs`); a session delete never creates it but
|
|
2024
2043
|
// still removes rows an earlier observe deployment left behind.
|
|
@@ -2273,8 +2292,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2273
2292
|
}) => this.requestHubAction(input),
|
|
2274
2293
|
chainTransaction: (input) =>
|
|
2275
2294
|
this.chainTransactionHubAction(input),
|
|
2276
|
-
status: (
|
|
2277
|
-
|
|
2295
|
+
status: (
|
|
2296
|
+
ownerDid: string,
|
|
2297
|
+
actionId: string,
|
|
2298
|
+
opts?: { timeoutMs?: number },
|
|
2299
|
+
) => this.hubActionStatusForTool(ownerDid, actionId, opts),
|
|
2278
2300
|
},
|
|
2279
2301
|
}
|
|
2280
2302
|
: {}),
|
|
@@ -2366,6 +2388,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2366
2388
|
this.lastHubOptions = null;
|
|
2367
2389
|
this.topicProposals = null;
|
|
2368
2390
|
this.attachmentTextCache = null;
|
|
2391
|
+
this.channelAttachmentLinks = null;
|
|
2369
2392
|
this.domainContextStore = null;
|
|
2370
2393
|
// Its store is bound to the database just dropped: a caller that sees
|
|
2371
2394
|
// no scheduler boots again instead of reading a closed connection.
|
|
@@ -3329,7 +3352,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3329
3352
|
// The gateway parses with @ixo/action-protocol 0.4.1, whose bank
|
|
3330
3353
|
// receipt is exact: the 0.5.0 `denom`/`amountBaseUnits` fields are
|
|
3331
3354
|
// dropped here (the owner-facing amount line comes from
|
|
3332
|
-
// hub_action_status instead), so the emitted shape stays 0.4.1.
|
|
3355
|
+
// hub_action_status instead), so the emitted shape stays 0.4.1. Every
|
|
3356
|
+
// other receipt goes as Auth Hub signed it, a failure's `detail`
|
|
3357
|
+
// included: the gateway confirms an outcome by comparing the whole
|
|
3358
|
+
// receipt with Auth Hub's copy. Only what the model sees drops it.
|
|
3333
3359
|
const hubActionOutcomes = (
|
|
3334
3360
|
await this.hubOutcomesForRun(ownerDid, result.sessionId, result.runId)
|
|
3335
3361
|
).map((outcome) => ({ ...outcome, receipt: v1Receipt(outcome.receipt) }));
|
|
@@ -3613,24 +3639,58 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3613
3639
|
}
|
|
3614
3640
|
|
|
3615
3641
|
/**
|
|
3616
|
-
* `
|
|
3617
|
-
*
|
|
3642
|
+
* `ctx.hubActions.status` (and `hub_action_status`): Auth Hub's review
|
|
3643
|
+
* and operation status, the outcome and, for a bank send, the
|
|
3644
|
+
* owner-facing line (amount from the token stored at preparation). An
|
|
3645
|
+
* action this object never prepared is `held: false`, without a Hub
|
|
3646
|
+
* read.
|
|
3618
3647
|
*/
|
|
3619
3648
|
private async hubActionStatusForTool(
|
|
3620
3649
|
ownerDid: string,
|
|
3621
3650
|
actionId: string,
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3651
|
+
opts: { timeoutMs?: number } = {},
|
|
3652
|
+
): Promise<HubActionStatusRead> {
|
|
3653
|
+
const read = await this.statusHubAction(
|
|
3654
|
+
ownerDid,
|
|
3655
|
+
actionId,
|
|
3656
|
+
opts.timeoutMs,
|
|
3657
|
+
);
|
|
3658
|
+
if (!read)
|
|
3659
|
+
return {
|
|
3660
|
+
held: false,
|
|
3661
|
+
reviewStatus: null,
|
|
3662
|
+
operationStatus: null,
|
|
3663
|
+
outcome: null,
|
|
3664
|
+
summary: null,
|
|
3665
|
+
};
|
|
3666
|
+
const token = read.outcome
|
|
3667
|
+
? await this.hubActionStore!.token(ownerDid, actionId)
|
|
3668
|
+
: null;
|
|
3669
|
+
return {
|
|
3670
|
+
held: true,
|
|
3671
|
+
reviewStatus: read.reviewStatus,
|
|
3672
|
+
operationStatus: read.operationStatus,
|
|
3673
|
+
outcome: read.outcome,
|
|
3674
|
+
summary: read.outcome
|
|
3675
|
+
? bankReceiptSummary(read.outcome.receipt, token)
|
|
3676
|
+
: null,
|
|
3677
|
+
};
|
|
3627
3678
|
}
|
|
3628
3679
|
|
|
3680
|
+
/**
|
|
3681
|
+
* One authenticated status read of a held action (null: not held), its
|
|
3682
|
+
* outcome recorded when new. An outcome for another revision than the
|
|
3683
|
+
* one Auth Hub reports is not taken (`outcome: null`).
|
|
3684
|
+
*/
|
|
3629
3685
|
private async statusHubAction(
|
|
3630
3686
|
ownerDid: string,
|
|
3631
3687
|
actionId: string,
|
|
3632
3688
|
timeoutMs?: number,
|
|
3633
|
-
): Promise<
|
|
3689
|
+
): Promise<{
|
|
3690
|
+
reviewStatus: HubReviewStatus;
|
|
3691
|
+
operationStatus: HubOperationStatus;
|
|
3692
|
+
outcome: ActionOutcomeV1 | null;
|
|
3693
|
+
} | null> {
|
|
3634
3694
|
const held = await this.hubActionStore?.get(ownerDid, actionId);
|
|
3635
3695
|
if (!held || !this.hubActionClient) return null;
|
|
3636
3696
|
const status = await this.hubActionClient.status(actionId, timeoutMs);
|
|
@@ -3639,16 +3699,20 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3639
3699
|
canonicalJson(status.reference) !== canonicalJson(held.reference)
|
|
3640
3700
|
)
|
|
3641
3701
|
throw new Error('Hub status does not match the prepared action');
|
|
3702
|
+
const read = {
|
|
3703
|
+
reviewStatus: status.reviewStatus,
|
|
3704
|
+
operationStatus: status.operationStatus,
|
|
3705
|
+
};
|
|
3642
3706
|
if (status.outcome) {
|
|
3643
3707
|
if (
|
|
3644
3708
|
status.revision !== status.outcome.authorizedRevision ||
|
|
3645
3709
|
status.digest !== status.outcome.authorizedDigest
|
|
3646
3710
|
)
|
|
3647
|
-
return null;
|
|
3711
|
+
return { ...read, outcome: null };
|
|
3648
3712
|
if (await this.hubActionStore!.recordOutcome(status.outcome))
|
|
3649
3713
|
this.markDirty();
|
|
3650
3714
|
}
|
|
3651
|
-
return status.outcome;
|
|
3715
|
+
return { ...read, outcome: status.outcome };
|
|
3652
3716
|
}
|
|
3653
3717
|
|
|
3654
3718
|
/**
|
|
@@ -3687,11 +3751,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3687
3751
|
// refreshed writes it, at most one per poll.
|
|
3688
3752
|
this.markDirty();
|
|
3689
3753
|
try {
|
|
3690
|
-
const fresh =
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3754
|
+
const fresh = (
|
|
3755
|
+
await this.statusHubAction(
|
|
3756
|
+
ownerDid,
|
|
3757
|
+
row.reference.id,
|
|
3758
|
+
HUB_POLL_TIMEOUT_MS,
|
|
3759
|
+
)
|
|
3760
|
+
)?.outcome;
|
|
3695
3761
|
if (fresh && (!latest || fresh.sequence > latest.sequence))
|
|
3696
3762
|
latest = fresh;
|
|
3697
3763
|
} catch (error) {
|
|
@@ -3848,6 +3914,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3848
3914
|
`[user-do] could not delete the cached attachment text of ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3849
3915
|
);
|
|
3850
3916
|
});
|
|
3917
|
+
await this.channelAttachmentLinks
|
|
3918
|
+
?.forgetSession(sessionId)
|
|
3919
|
+
.catch((err: unknown) => {
|
|
3920
|
+
console.warn(
|
|
3921
|
+
`[user-do] could not delete the channel file links of ${sessionId}; they expire with the links: ${err instanceof Error ? err.message : String(err)}`,
|
|
3922
|
+
);
|
|
3923
|
+
});
|
|
3851
3924
|
await this.resultStore
|
|
3852
3925
|
?.deleteForSession(sessionId)
|
|
3853
3926
|
.catch((err: unknown) => {
|
|
@@ -7038,6 +7111,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7038
7111
|
signal: AbortSignal;
|
|
7039
7112
|
/** Attachments of the turn now running (not necessarily persisted yet). */
|
|
7040
7113
|
turnMetas: readonly AttachmentMeta[];
|
|
7114
|
+
/** The links of this turn's channel files (earlier ones come from the store). */
|
|
7115
|
+
channelLinks: ReturnType<typeof channelTurnLinks>;
|
|
7041
7116
|
}): Promise<AttachmentViewSurface> {
|
|
7042
7117
|
const saver = this.saver!;
|
|
7043
7118
|
const offloaded = await saver.hasThreadMessageMatching(input.sessionId, {
|
|
@@ -7048,6 +7123,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7048
7123
|
findPersistedAttachmentMeta(saver, input.sessionId, ref);
|
|
7049
7124
|
const viewDeps = (): ViewAttachmentDeps => ({
|
|
7050
7125
|
source: this.matrixMediaSource(),
|
|
7126
|
+
channel: turnChannelMediaSource({
|
|
7127
|
+
sessionId: input.sessionId,
|
|
7128
|
+
turnLinks: input.channelLinks,
|
|
7129
|
+
store: this.channelAttachmentLinks,
|
|
7130
|
+
channelServiceDid: this.env.CHANNEL_SERVICE_DID,
|
|
7131
|
+
}),
|
|
7051
7132
|
extraction: this.extractionProvider(input.ambientLlm, input.platform),
|
|
7052
7133
|
caps: getModelCapabilities(input.model),
|
|
7053
7134
|
roomId: input.roomId,
|
|
@@ -7390,32 +7471,47 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7390
7471
|
// native capabilities, send images/files inline when it reads them,
|
|
7391
7472
|
// otherwise the `vision` role turns them into text on the platform key;
|
|
7392
7473
|
// originals are archived to the user's sandbox when one is reachable.
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
)
|
|
7474
|
+
// A channel turn's files are read by their `channel:` reference; their
|
|
7475
|
+
// links stay on the server (the run request and the session's link
|
|
7476
|
+
// store), out of every message and note.
|
|
7477
|
+
const attachmentInputs = turnAttachmentInputs(req, body.attachments);
|
|
7396
7478
|
const attachmentModel =
|
|
7397
7479
|
effectiveModel && isAllowedModel(effectiveModel)
|
|
7398
7480
|
? effectiveModel
|
|
7399
7481
|
: getDefaultModelId(core.validatedEnv);
|
|
7400
|
-
const
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7482
|
+
const turnLinks = channelTurnLinks(req);
|
|
7483
|
+
const channelMedia = turnLinks.length
|
|
7484
|
+
? turnChannelMediaSource({
|
|
7485
|
+
sessionId: req.sessionId,
|
|
7486
|
+
turnLinks,
|
|
7487
|
+
store: this.channelAttachmentLinks,
|
|
7488
|
+
channelServiceDid: this.env.CHANNEL_SERVICE_DID,
|
|
7489
|
+
})
|
|
7490
|
+
: undefined;
|
|
7491
|
+
if (turnLinks.length && !run.resumed)
|
|
7492
|
+
await this.channelAttachmentLinks?.put(req.sessionId, turnLinks);
|
|
7493
|
+
const { prepared, metas: turnMetas } = await prepareTurnAttachments({
|
|
7494
|
+
inputs: attachmentInputs,
|
|
7495
|
+
resumed: run.resumed,
|
|
7496
|
+
prepare: async () =>
|
|
7497
|
+
prepareAttachments(
|
|
7498
|
+
{
|
|
7499
|
+
text: body.message,
|
|
7500
|
+
attachments: attachmentInputs,
|
|
7501
|
+
roomId: sessionRoomId,
|
|
7502
|
+
model: attachmentModel,
|
|
7503
|
+
caps: getModelCapabilities(attachmentModel),
|
|
7504
|
+
},
|
|
7505
|
+
{
|
|
7506
|
+
source: this.matrixMediaSource(),
|
|
7507
|
+
...(channelMedia ? { channel: channelMedia } : {}),
|
|
7508
|
+
extraction: this.extractionProvider(baseAmbient.llm, core.llm),
|
|
7509
|
+
sandbox: await this.sandboxArchiveConfig(req.identity.userDid),
|
|
7510
|
+
signal: abortController.signal,
|
|
7511
|
+
logger: console,
|
|
7512
|
+
},
|
|
7513
|
+
),
|
|
7514
|
+
});
|
|
7419
7515
|
if (prepared) {
|
|
7420
7516
|
console.log(
|
|
7421
7517
|
`[user-do] attachments: ${attachmentInputs.length} attached, ${prepared.extracted.length} extracted, ${Array.isArray(prepared.content) ? prepared.content.length - 1 : 0} native`,
|
|
@@ -7439,7 +7535,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7439
7535
|
ambientLlm: baseAmbient.llm,
|
|
7440
7536
|
platform: core.llm,
|
|
7441
7537
|
signal: abortController.signal,
|
|
7442
|
-
turnMetas
|
|
7538
|
+
turnMetas,
|
|
7539
|
+
channelLinks: turnLinks,
|
|
7443
7540
|
});
|
|
7444
7541
|
|
|
7445
7542
|
// Request metadata (editor room, space, session run, entity) → state,
|
|
@@ -7773,6 +7870,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7773
7870
|
provider: req.channel.provider,
|
|
7774
7871
|
bindingId: req.channel.bindingId,
|
|
7775
7872
|
bindingRevision: req.channel.bindingRevision,
|
|
7873
|
+
...(req.channel.attachments?.length
|
|
7874
|
+
? { attachments: req.channel.attachments }
|
|
7875
|
+
: {}),
|
|
7776
7876
|
},
|
|
7777
7877
|
}
|
|
7778
7878
|
: {}),
|