@ixo/oracle-runtime-workers 0.35.0 → 0.36.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 +4 -4
- package/src/artifacts/tool.ts +7 -4
- package/src/attachments/pipeline.ts +3 -1
- package/src/attachments/route.ts +11 -0
- package/src/attachments/view.ts +5 -1
- package/src/channels/contract.ts +3 -2
- package/src/channels/turns.ts +27 -9
- package/src/core/env.ts +4 -2
- package/src/core/llm.ts +70 -1
- package/src/core/main-agent.ts +5 -0
- package/src/core/middlewares/result-cap.ts +2 -0
- package/src/core/middlewares/turn-boundary.ts +14 -0
- package/src/delivery/plan.ts +89 -20
- package/src/delivery/profile.ts +25 -20
- package/src/delivery/prompt.ts +33 -7
- package/src/delivery/schema.ts +69 -5
- package/src/delivery/shaper.ts +329 -87
- package/src/delivery/types.ts +48 -8
- package/src/do/contracts.ts +2 -0
- package/src/do/run-coordinator.ts +13 -3
- package/src/do/run-store.ts +53 -3
- package/src/do/transcript.ts +17 -1
- package/src/do/user-oracle-do.ts +51 -3
- package/src/matrix/reply-parts.ts +9 -1
- package/src/plugins/ixo-chain/names/client.ts +7 -3
- package/src/plugins/sandbox/index.ts +1 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +41 -8
- package/src/plugins/sandbox/send-file.ts +302 -0
- package/src/sqlite/blob-codec.ts +16 -31
package/src/delivery/prompt.ts
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
import type { DeliveryProfile } from './types';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Whether the surface delivers a file as an attachment. A Matrix room has no
|
|
5
|
+
* gateway to fetch it, so a file arrives there as a link line.
|
|
6
|
+
*/
|
|
7
|
+
export function deliversAttachments(surface: string): boolean {
|
|
8
|
+
return surface !== 'matrix';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** `maxBubbles` as the prompt asks for it: "two or three messages". */
|
|
12
|
+
function preferredMessages(maxBubbles: number): string {
|
|
13
|
+
if (maxBubbles <= 1) return 'one message';
|
|
14
|
+
if (maxBubbles === 2) return 'one or two messages';
|
|
15
|
+
if (maxBubbles === 3) return 'two or three messages';
|
|
16
|
+
return `up to ${maxBubbles} messages`;
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* The prompt section a chat turn gets: write for the surface. Empty on the
|
|
5
|
-
* Portal. `artifacts` says whether `create_artifact` is bound this turn
|
|
21
|
+
* Portal. `artifacts` says whether `create_artifact` is bound this turn,
|
|
22
|
+
* `files` whether `send_file` is. Agrees with the shaper and the tools' own
|
|
23
|
+
* descriptions: ordinary answers stay in chat, only document-length output
|
|
24
|
+
* becomes an artefact, and a made file reaches the user as an attachment.
|
|
6
25
|
*/
|
|
7
26
|
export function renderSurfaceSection(
|
|
8
27
|
profile: DeliveryProfile | undefined,
|
|
9
28
|
artifacts: boolean,
|
|
29
|
+
files: boolean,
|
|
10
30
|
): string {
|
|
11
31
|
if (!profile || profile.kind !== 'chat') return '';
|
|
12
|
-
const { label, limits } = profile;
|
|
32
|
+
const { label, limits, surface } = profile;
|
|
33
|
+
const preferred = preferredMessages(limits.maxBubbles);
|
|
13
34
|
return [
|
|
14
35
|
'## Where this conversation is happening',
|
|
15
36
|
'',
|
|
16
|
-
`You are replying in ${label}. The user reads your reply as chat messages,
|
|
37
|
+
`You are replying in ${label}. The user reads your reply as chat messages, often on a phone with a chat-only data plan and no browser, so the answer itself belongs in the messages.`,
|
|
17
38
|
'',
|
|
18
39
|
'- Lead with the answer. Write like a sharp person texting: short sentences, no preamble, no sign-off.',
|
|
19
|
-
`-
|
|
20
|
-
`- No headings
|
|
40
|
+
`- Prefer ${preferred}. A blank line marks where a message may break; short paragraphs are grouped so the reply reads as ${preferred}. Keep each paragraph under about ${limits.bubbleTarget} characters. Use more messages when the answer needs them; never leave content out to fit.`,
|
|
41
|
+
`- Ordinary answers stay in chat, including explanations, comparisons, short plans and step-by-step instructions. No headings or horizontal rules. Lists up to ${limits.maxListItems} items, code up to ${limits.maxCodeLines} lines and **bold** for the key fact are fine in chat. Write a small comparison as one short line per item, not a table.`,
|
|
42
|
+
...(files
|
|
43
|
+
? [
|
|
44
|
+
`- Deliver every file meant for the user (a Word document, PDF, spreadsheet, slide deck, image, CSV or anything else) with \`send_file\`: ${deliversAttachments(surface) ? 'the user receives the file itself here, as an attachment' : 'a link to the file is posted here for you'}. When the user asks for a file in a particular format, build it in the sandbox and send it this way. Never paste a download link, and do not describe how to open or download it. Do not send helper scripts or intermediate files. Call it once per file, after the sandbox call that saved the file has finished.`,
|
|
45
|
+
]
|
|
46
|
+
: []),
|
|
21
47
|
artifacts
|
|
22
|
-
? '
|
|
23
|
-
: '-
|
|
48
|
+
? `- Use \`create_artifact\` ${files ? 'for a document with no particular file format' : 'only when the user asks for a document or file'}, or when the output is genuinely document-length: several screens of text, long code, a long report. Even then, its message carries the conclusions, decisions and next actions, and the document holds the detail. Call it last: the user gets the message, the document, then at most one follow-up question.`
|
|
49
|
+
: '- Everything you write is delivered as messages; when something is genuinely long, put the essential part first.',
|
|
24
50
|
'- The user sees a typing indicator while you work. Do not narrate your steps or announce tool calls.',
|
|
25
51
|
'- End with at most one question.',
|
|
26
52
|
].join('\n');
|
package/src/delivery/schema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActionReferenceSchema } from '@ixo/ixo-transaction/action-proposals';
|
|
2
2
|
import { parseReference, type ActionReferenceV1 } from '@ixo/action-protocol';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import type { ReplyContent, ReplyPlan } from './types';
|
|
4
|
+
import type { ReplyContent, ReplyFile, ReplyPlan } from './types';
|
|
5
5
|
|
|
6
6
|
const ArtifactRefSchema = z.object({
|
|
7
7
|
artifactId: z.string(),
|
|
@@ -12,6 +12,33 @@ const ArtifactRefSchema = z.object({
|
|
|
12
12
|
expiresAt: z.string(),
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
function isSignedLink(value: string): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(value);
|
|
18
|
+
return url.protocol === 'https:' && !url.username && !url.password;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The most a gateway takes (`maxFileUrlChars`): with room for its caption the
|
|
26
|
+
* link must still fit one 4,096-character text message.
|
|
27
|
+
*/
|
|
28
|
+
export const REPLY_FILE_URL_MAX = 3800;
|
|
29
|
+
|
|
30
|
+
export const ReplyFileSchema = z.object({
|
|
31
|
+
url: z
|
|
32
|
+
.string()
|
|
33
|
+
.max(REPLY_FILE_URL_MAX)
|
|
34
|
+
.refine(isSignedLink, 'must be an https URL without credentials'),
|
|
35
|
+
filename: z.string().min(1),
|
|
36
|
+
mimetype: z.string().min(1),
|
|
37
|
+
bytes: z.number().int().nonnegative().optional(),
|
|
38
|
+
caption: z.string().max(1024).optional(),
|
|
39
|
+
expiresAt: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
15
42
|
export const ReplyPlanSchema = z.object({
|
|
16
43
|
v: z.literal(1),
|
|
17
44
|
parts: z.array(
|
|
@@ -44,6 +71,11 @@ export const ReplyPlanSchema = z.object({
|
|
|
44
71
|
kind: z.literal('artifact'),
|
|
45
72
|
artifact: ArtifactRefSchema,
|
|
46
73
|
}),
|
|
74
|
+
z.object({
|
|
75
|
+
partId: z.string(),
|
|
76
|
+
kind: z.literal('file'),
|
|
77
|
+
file: ReplyFileSchema,
|
|
78
|
+
}),
|
|
47
79
|
]),
|
|
48
80
|
),
|
|
49
81
|
});
|
|
@@ -65,10 +97,17 @@ function linkLabel(title: string): string {
|
|
|
65
97
|
return title.replace(/[[\]]/g, '');
|
|
66
98
|
}
|
|
67
99
|
|
|
100
|
+
function fileLine(file: ReplyFile): string {
|
|
101
|
+
const link = `[${linkLabel(file.filename)}](${file.url})`;
|
|
102
|
+
return file.caption ? `${file.caption}\n\n${link}` : link;
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
/**
|
|
69
|
-
* The plan as one Markdown message: what
|
|
70
|
-
*
|
|
71
|
-
*
|
|
106
|
+
* The plan as one Markdown message: what gateways that predate plans receive.
|
|
107
|
+
* A file is a link line, so a gateway that cannot read the `file` part
|
|
108
|
+
* degrades instead of losing the file. Never stored as a run's text, which
|
|
109
|
+
* stays the model's own, and never shown to the model or mirrored to a room:
|
|
110
|
+
* see `planTextForModel`.
|
|
72
111
|
*/
|
|
73
112
|
export function planText(plan: { parts: readonly ReplyContent[] }): string {
|
|
74
113
|
return plan.parts
|
|
@@ -79,7 +118,32 @@ export function planText(plan: { parts: readonly ReplyContent[] }): string {
|
|
|
79
118
|
? `[${linkLabel(part.action.title)}](${part.action.url})`
|
|
80
119
|
: part.kind === 'hub-action'
|
|
81
120
|
? `[${linkLabel(part.summary)}](${part.actionReference.url})`
|
|
82
|
-
:
|
|
121
|
+
: part.kind === 'file'
|
|
122
|
+
? fileLine(part.file)
|
|
123
|
+
: `[${linkLabel(part.artifact.title)}](${part.artifact.url})`,
|
|
83
124
|
)
|
|
84
125
|
.join('\n\n');
|
|
85
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `planText` without a file's signed link, which is named instead: for the
|
|
130
|
+
* model, as when a user quotes an earlier reply, for the room mirror, and for
|
|
131
|
+
* the transcript, all of which outlive the link.
|
|
132
|
+
*/
|
|
133
|
+
export function planTextForModel(plan: {
|
|
134
|
+
parts: readonly ReplyContent[];
|
|
135
|
+
}): string {
|
|
136
|
+
return planText({
|
|
137
|
+
parts: plan.parts.map(
|
|
138
|
+
(part): ReplyContent =>
|
|
139
|
+
part.kind === 'file'
|
|
140
|
+
? {
|
|
141
|
+
kind: 'text',
|
|
142
|
+
text: [part.file.caption, `[File sent: ${part.file.filename}]`]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join('\n\n'),
|
|
145
|
+
}
|
|
146
|
+
: part,
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
}
|