@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
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(),
|
|
@@ -7,6 +7,7 @@ import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
|
7
7
|
import type { Logger } from '../../plugin-api/types';
|
|
8
8
|
import { NOOP_LOGGER } from '../utils';
|
|
9
9
|
import { isCapabilityGateRefusal } from './capability-gate';
|
|
10
|
+
import { isToolNotRun } from '../tool-not-run';
|
|
10
11
|
import { turnCarryOf, turnStart } from './turn-boundary';
|
|
11
12
|
|
|
12
13
|
export { turnStart };
|
|
@@ -145,9 +146,11 @@ export const createToolRepetitionGuardMiddleware = (
|
|
|
145
146
|
const msg = messages[i];
|
|
146
147
|
if (!(msg instanceof ToolMessage)) continue;
|
|
147
148
|
if (msg.status !== 'error') continue;
|
|
148
|
-
// The capability gate refused it without running it
|
|
149
|
-
//
|
|
150
|
-
|
|
149
|
+
// The capability gate refused it without running it, or the tool
|
|
150
|
+
// said it did not run until a step comes first: after that step
|
|
151
|
+
// (`load_capability`, reading the rules) the same call is the
|
|
152
|
+
// expected next one.
|
|
153
|
+
if (isCapabilityGateRefusal(msg) || isToolNotRun(msg)) continue;
|
|
151
154
|
if (msg.name !== toolName) continue;
|
|
152
155
|
|
|
153
156
|
const priorArgs = argsById.get(msg.tool_call_id);
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@langchain/core/messages';
|
|
23
23
|
import { isAttachmentViewMessage } from '../../attachments/retention';
|
|
24
24
|
import { isCapabilityGateRefusal } from './capability-gate';
|
|
25
|
+
import { isToolNotRun } from '../tool-not-run';
|
|
25
26
|
|
|
26
27
|
export const SUMMARY_PREFIX = 'Here is a summary of the conversation to date:';
|
|
27
28
|
|
|
@@ -140,7 +141,11 @@ export function carriedCallsOfTurn(
|
|
|
140
141
|
if (call.id) calls.set(call.id, { name: call.name, args: call.args });
|
|
141
142
|
continue;
|
|
142
143
|
}
|
|
143
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
!ToolMessage.isInstance(message) ||
|
|
146
|
+
isCapabilityGateRefusal(message) ||
|
|
147
|
+
isToolNotRun(message)
|
|
148
|
+
)
|
|
144
149
|
continue;
|
|
145
150
|
const call = calls.get(message.tool_call_id);
|
|
146
151
|
if (!call) continue;
|
|
@@ -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;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
|
|
3
|
+
/** `response_metadata` key of a result whose tool did not run (never sent to a provider). */
|
|
4
|
+
const NOT_RUN = 'toolNotRun';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A tool result saying the tool did not run because another step must come
|
|
8
|
+
* first (for example reading a message type's rules before preparing it).
|
|
9
|
+
* It is an error result, so it never counts as a write that happened, and
|
|
10
|
+
* like a capability-gate refusal the repetition guard lets the identical
|
|
11
|
+
* call through once the step is done (`isToolNotRun`).
|
|
12
|
+
*/
|
|
13
|
+
export function toolNotRunMessage(input: {
|
|
14
|
+
toolCallId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
content: string;
|
|
17
|
+
}): ToolMessage {
|
|
18
|
+
return new ToolMessage({
|
|
19
|
+
tool_call_id: input.toolCallId,
|
|
20
|
+
name: input.name,
|
|
21
|
+
content: input.content,
|
|
22
|
+
status: 'error',
|
|
23
|
+
response_metadata: { [NOT_RUN]: true },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A result `toolNotRunMessage` made: the tool did not run. */
|
|
28
|
+
export function isToolNotRun(message: ToolMessage): boolean {
|
|
29
|
+
return message.response_metadata?.[NOT_RUN] === true;
|
|
30
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolMessage } from '@langchain/core/messages';
|
|
1
2
|
import { Command } from '@langchain/langgraph';
|
|
2
3
|
import { capToolResult, type ResultCapConfig } from './middlewares/result-cap';
|
|
3
4
|
import { tool } from '@langchain/core/tools';
|
|
@@ -125,7 +126,14 @@ export function wrapPluginTool(
|
|
|
125
126
|
if (pluginName !== undefined)
|
|
126
127
|
requireToolPlane(ctx, pluginName, pluginTool);
|
|
127
128
|
const output = await pluginTool.handler(args, ctx);
|
|
128
|
-
|
|
129
|
+
// A tool's own message (a result that says the tool did not run) is
|
|
130
|
+
// returned whole, never capped into text.
|
|
131
|
+
if (
|
|
132
|
+
!resultCap ||
|
|
133
|
+
output instanceof Command ||
|
|
134
|
+
ToolMessage.isInstance(output) ||
|
|
135
|
+
output == null
|
|
136
|
+
)
|
|
129
137
|
return output;
|
|
130
138
|
const text = typeof output === 'string' ? output : JSON.stringify(output);
|
|
131
139
|
if (typeof text !== 'string' || text.length <= resultCap.capChars)
|
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
|
+
}
|