@ixo/oracle-runtime-workers 0.13.0 → 0.16.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 +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +310 -0
- package/src/channels/contract.ts +246 -0
- package/src/channels/turns.ts +280 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +2003 -274
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +562 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic shaper: one model step's Markdown → chat messages, or a
|
|
3
|
+
* spill (lead message, artefact, closing question) when the step is too long
|
|
4
|
+
* or too structured for chat. No model call; the same input always gives the
|
|
5
|
+
* same output, which is what makes a Reply Plan safe to rebuild on recovery.
|
|
6
|
+
*/
|
|
7
|
+
import { marked, type Token, type Tokens } from 'marked';
|
|
8
|
+
import type { ChatLimits } from './types';
|
|
9
|
+
|
|
10
|
+
export interface SpillShape {
|
|
11
|
+
title: string;
|
|
12
|
+
/** The whole step as the model wrote it: the artefact's content. */
|
|
13
|
+
markdown: string;
|
|
14
|
+
/** What stays in chat: the first block, or a list's lead-in and first items. */
|
|
15
|
+
lead: string;
|
|
16
|
+
/** The step's closing question, kept as its own message after the link. */
|
|
17
|
+
closing?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StepShape =
|
|
21
|
+
| { kind: 'messages'; messages: string[] }
|
|
22
|
+
| { kind: 'spill'; spill: SpillShape };
|
|
23
|
+
|
|
24
|
+
type BlockKind = 'heading' | 'paragraph' | 'list' | 'code' | 'table' | 'quote';
|
|
25
|
+
|
|
26
|
+
interface Block {
|
|
27
|
+
kind: BlockKind;
|
|
28
|
+
md: string;
|
|
29
|
+
token: Token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TITLE_MAX = 60;
|
|
33
|
+
|
|
34
|
+
const isHeading = (t: Token): t is Tokens.Heading => t.type === 'heading';
|
|
35
|
+
const isList = (t: Token): t is Tokens.List => t.type === 'list';
|
|
36
|
+
const isCode = (t: Token): t is Tokens.Code => t.type === 'code';
|
|
37
|
+
const isTable = (t: Token): t is Tokens.Table => t.type === 'table';
|
|
38
|
+
const isHtml = (t: Token): t is Tokens.HTML => t.type === 'html';
|
|
39
|
+
|
|
40
|
+
const sentenceSegmenter = new Intl.Segmenter(undefined, {
|
|
41
|
+
granularity: 'sentence',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HTML elements a model mixes into Markdown. Anything else in angle brackets
|
|
46
|
+
* (`Promise<string>`, `<name>` placeholders) is text and stays.
|
|
47
|
+
*/
|
|
48
|
+
const HTML_ELEMENTS = new Set(
|
|
49
|
+
(
|
|
50
|
+
'a abbr address article aside b bdi bdo blockquote body br button ' +
|
|
51
|
+
'caption center cite code col colgroup dd del details dfn div dl dt ' +
|
|
52
|
+
'em figcaption figure font footer form h1 h2 h3 h4 h5 h6 head ' +
|
|
53
|
+
'header hr html i iframe img input ins kbd label li main mark nav ' +
|
|
54
|
+
'ol p picture pre q s samp script section select small source span ' +
|
|
55
|
+
'strike strong style sub summary sup table tbody td textarea tfoot ' +
|
|
56
|
+
'th thead tr tt u ul var video wbr'
|
|
57
|
+
).split(' '),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Raw HTML → its text: HTML elements' tags and comments removed, `<br>` a
|
|
62
|
+
* line break. Only ever applied to what the lexer read as raw HTML, never to
|
|
63
|
+
* code. `a < b and c > d` is left alone.
|
|
64
|
+
*/
|
|
65
|
+
function stripHtmlTags(html: string): string {
|
|
66
|
+
return html
|
|
67
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
68
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
69
|
+
.replace(
|
|
70
|
+
/<\/?([a-zA-Z][a-zA-Z0-9-]*)(\s[^<>]*)?\/?>/g,
|
|
71
|
+
(tag, name: string) => (HTML_ELEMENTS.has(name.toLowerCase()) ? '' : tag),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Characters that render as nothing: whitespace and zero-width marks. */
|
|
76
|
+
const INVISIBLE = /[\s\u200B-\u200D\u2060\uFEFF]/g;
|
|
77
|
+
|
|
78
|
+
/** True when a message would look empty: only whitespace or zero-width characters. */
|
|
79
|
+
export function isBlank(text: string): boolean {
|
|
80
|
+
return text.replace(INVISIBLE, '') === '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function childTokens(token: Token): Token[] {
|
|
84
|
+
if (isList(token)) return token.items;
|
|
85
|
+
return 'tokens' in token && Array.isArray(token.tokens) ? token.tokens : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isLinkOrImage = (t: Token): t is Tokens.Link | Tokens.Image =>
|
|
89
|
+
t.type === 'link' || t.type === 'image';
|
|
90
|
+
|
|
91
|
+
/** `[text][ref]`, `[text][]` or `[ref]`, inline: its definition is not delivered. */
|
|
92
|
+
function referenceLinkInline(token: Token): string | null {
|
|
93
|
+
if (!isLinkOrImage(token) || token.raw.endsWith(')')) return null;
|
|
94
|
+
const image = token.type === 'image';
|
|
95
|
+
if (!token.raw.startsWith(image ? '`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A block's Markdown for chat: `token.raw` with raw HTML reduced to its text
|
|
102
|
+
* and reference links written inline (their definitions are not delivered).
|
|
103
|
+
* Code spans and code blocks stay verbatim, so `` `<div>` `` survives.
|
|
104
|
+
*/
|
|
105
|
+
function chatMarkdown(token: Token): string {
|
|
106
|
+
const raw = token.raw;
|
|
107
|
+
const edits: Array<{ start: number; end: number; text: string }> = [];
|
|
108
|
+
let cursor = 0;
|
|
109
|
+
const locate = (part: string): number => {
|
|
110
|
+
const at = part ? raw.indexOf(part, cursor) : -1;
|
|
111
|
+
if (at >= 0) cursor = at + part.length;
|
|
112
|
+
return at;
|
|
113
|
+
};
|
|
114
|
+
const visit = (node: Token): void => {
|
|
115
|
+
if (node.type === 'code' || node.type === 'codespan') {
|
|
116
|
+
locate(node.raw);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const inline = referenceLinkInline(node);
|
|
120
|
+
const replacement = node.type === 'html' ? stripHtmlTags(node.raw) : inline;
|
|
121
|
+
if (replacement !== null) {
|
|
122
|
+
const at = locate(node.raw);
|
|
123
|
+
if (at >= 0)
|
|
124
|
+
edits.push({ start: at, end: at + node.raw.length, text: replacement });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const children = childTokens(node);
|
|
128
|
+
if (children.length === 0) locate(node.raw);
|
|
129
|
+
else for (const child of children) visit(child);
|
|
130
|
+
};
|
|
131
|
+
for (const child of childTokens(token)) visit(child);
|
|
132
|
+
let out = '';
|
|
133
|
+
let from = 0;
|
|
134
|
+
for (const edit of edits) {
|
|
135
|
+
out += raw.slice(from, edit.start) + edit.text;
|
|
136
|
+
from = edit.end;
|
|
137
|
+
}
|
|
138
|
+
return out + raw.slice(from);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function plainText(text: string): string {
|
|
142
|
+
return stripHtmlTags(text)
|
|
143
|
+
.replace(/[*_`#>~]/g, '')
|
|
144
|
+
.replace(/\s+/g, ' ')
|
|
145
|
+
.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function tableAsList(table: Tokens.Table): string {
|
|
149
|
+
return table.rows
|
|
150
|
+
.map((row) => {
|
|
151
|
+
const [first, ...rest] = row.map((cell) => cell.text.trim());
|
|
152
|
+
const pairs = rest.map((value, i) => {
|
|
153
|
+
const header = table.header[i + 1]?.text.trim();
|
|
154
|
+
return header ? `${header}: ${value}` : value;
|
|
155
|
+
});
|
|
156
|
+
return `- **${first ?? ''}**${pairs.length ? ` · ${pairs.join(' · ')}` : ''}`;
|
|
157
|
+
})
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toBlocks(markdown: string, limits: ChatLimits): Block[] {
|
|
162
|
+
const blocks: Block[] = [];
|
|
163
|
+
for (const token of marked.lexer(markdown)) {
|
|
164
|
+
if (token.type === 'space' || token.type === 'hr' || token.type === 'def')
|
|
165
|
+
continue;
|
|
166
|
+
if (isHeading(token)) {
|
|
167
|
+
const text = plainText(token.text);
|
|
168
|
+
if (text) blocks.push({ kind: 'heading', md: `**${text}**`, token });
|
|
169
|
+
} else if (isList(token)) {
|
|
170
|
+
blocks.push({ kind: 'list', md: chatMarkdown(token).trim(), token });
|
|
171
|
+
} else if (isCode(token)) {
|
|
172
|
+
blocks.push({ kind: 'code', md: token.raw.trim(), token });
|
|
173
|
+
} else if (isTable(token)) {
|
|
174
|
+
blocks.push({
|
|
175
|
+
kind: 'table',
|
|
176
|
+
md: limits.tables ? token.raw.trim() : tableAsList(token),
|
|
177
|
+
token,
|
|
178
|
+
});
|
|
179
|
+
} else if (token.type === 'blockquote') {
|
|
180
|
+
blocks.push({ kind: 'quote', md: chatMarkdown(token).trim(), token });
|
|
181
|
+
} else {
|
|
182
|
+
const md = (
|
|
183
|
+
isHtml(token) ? stripHtmlTags(token.text) : chatMarkdown(token)
|
|
184
|
+
).trim();
|
|
185
|
+
if (!isBlank(md)) blocks.push({ kind: 'paragraph', md, token });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return blocks;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sentencesOf(text: string): string[] {
|
|
192
|
+
return Array.from(sentenceSegmenter.segment(text), (s) => s.segment);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function packPieces(
|
|
196
|
+
pieces: string[],
|
|
197
|
+
separator: string,
|
|
198
|
+
max: number,
|
|
199
|
+
): string[] {
|
|
200
|
+
const out: string[] = [];
|
|
201
|
+
let current = '';
|
|
202
|
+
for (const piece of pieces) {
|
|
203
|
+
const candidate = current ? current + separator + piece : piece;
|
|
204
|
+
if (candidate.length <= max) {
|
|
205
|
+
current = candidate;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (current) out.push(current);
|
|
209
|
+
if (piece.length > max) {
|
|
210
|
+
out.push(...splitText(piece, max));
|
|
211
|
+
current = '';
|
|
212
|
+
} else current = piece;
|
|
213
|
+
}
|
|
214
|
+
if (current) out.push(current);
|
|
215
|
+
return out.map((s) => s.trim()).filter((s) => !isBlank(s));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Split at the largest boundary that works: paragraph, line, sentence, word. */
|
|
219
|
+
export function splitText(text: string, max: number): string[] {
|
|
220
|
+
// A size below one character would never move through the text.
|
|
221
|
+
const size = Number.isFinite(max) && max >= 1 ? Math.floor(max) : 1;
|
|
222
|
+
if (text.length <= size) return [text];
|
|
223
|
+
for (const separator of ['\n\n', '\n']) {
|
|
224
|
+
const pieces = text.split(separator);
|
|
225
|
+
if (pieces.length > 1) return packPieces(pieces, separator, size);
|
|
226
|
+
}
|
|
227
|
+
const sentences = sentencesOf(text);
|
|
228
|
+
if (sentences.length > 1) return packPieces(sentences, '', size);
|
|
229
|
+
const words = text.split(/(?<=\s)/);
|
|
230
|
+
if (words.length > 1) return packPieces(words, '', size);
|
|
231
|
+
const out: string[] = [];
|
|
232
|
+
for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size));
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A code block too long for one message is re-fenced per piece. */
|
|
237
|
+
function splitBlock(block: Block, max: number): string[] {
|
|
238
|
+
if (block.md.length <= max) return [block.md];
|
|
239
|
+
if (isCode(block.token)) {
|
|
240
|
+
const fence = '```' + (block.token.lang ?? '');
|
|
241
|
+
const room = max - fence.length - 5;
|
|
242
|
+
if (room >= 1)
|
|
243
|
+
return splitText(block.token.text, room).map(
|
|
244
|
+
(piece) => `${fence}\n${piece}\n\`\`\``,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return splitText(block.md, max);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function trimToSentence(text: string, max: number): string {
|
|
251
|
+
if (text.length <= max) return text;
|
|
252
|
+
let out = '';
|
|
253
|
+
for (const sentence of sentencesOf(text)) {
|
|
254
|
+
if ((out + sentence).length > max) break;
|
|
255
|
+
out += sentence;
|
|
256
|
+
}
|
|
257
|
+
if (out.trim()) return out.trim();
|
|
258
|
+
const cut = text.slice(0, max - 1);
|
|
259
|
+
const space = cut.lastIndexOf(' ');
|
|
260
|
+
return `${(space > max / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function titleOf(text: string): string {
|
|
264
|
+
const plain = plainText(sentencesOf(text)[0] ?? text).replace(/[:.]$/, '');
|
|
265
|
+
if (plain.length <= TITLE_MAX) return plain;
|
|
266
|
+
const cut = plain.slice(0, TITLE_MAX - 1);
|
|
267
|
+
const space = cut.lastIndexOf(' ');
|
|
268
|
+
return `${(space > TITLE_MAX / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function firstLine(text: string): string {
|
|
272
|
+
return (text.split('\n')[0] ?? '').trim();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function preview(list: Tokens.List, count: number): string {
|
|
276
|
+
const start = typeof list.start === 'number' ? list.start : 1;
|
|
277
|
+
const items = list.items
|
|
278
|
+
.slice(0, count)
|
|
279
|
+
.map(
|
|
280
|
+
(item, i) =>
|
|
281
|
+
`${list.ordered ? `${start + i}.` : '-'} ${firstLine(item.text)}`,
|
|
282
|
+
);
|
|
283
|
+
const more = list.items.length - count;
|
|
284
|
+
return items.join('\n') + (more > 0 ? `\n…and ${more} more` : '');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const endsWithColon = (md: string): boolean => /:\s*$/.test(md);
|
|
288
|
+
const joinBlocks = (a: string, b: string): string => (a ? `${a}\n\n${b}` : b);
|
|
289
|
+
|
|
290
|
+
/** Blocks → messages: headings and "…:" lead-ins stay with what follows. */
|
|
291
|
+
function pack(blocks: Block[], limits: ChatLimits): string[] {
|
|
292
|
+
const units: string[] = [];
|
|
293
|
+
let carry = '';
|
|
294
|
+
blocks.forEach((block, i) => {
|
|
295
|
+
const next = blocks[i + 1];
|
|
296
|
+
const leadIn =
|
|
297
|
+
block.kind === 'paragraph' &&
|
|
298
|
+
endsWithColon(block.md) &&
|
|
299
|
+
next !== undefined &&
|
|
300
|
+
(next.kind === 'list' || next.kind === 'code' || next.kind === 'table');
|
|
301
|
+
if (block.kind === 'heading' || leadIn) {
|
|
302
|
+
carry = joinBlocks(carry, block.md);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const pieces = splitBlock(block, limits.bubbleMax);
|
|
306
|
+
pieces.forEach((piece, j) => {
|
|
307
|
+
units.push(j === 0 ? joinBlocks(carry, piece) : piece);
|
|
308
|
+
});
|
|
309
|
+
carry = '';
|
|
310
|
+
});
|
|
311
|
+
if (carry) units.push(carry);
|
|
312
|
+
const merged: string[] = [];
|
|
313
|
+
for (let i = 0; i < units.length; i++) {
|
|
314
|
+
const unit = units[i]!;
|
|
315
|
+
if (unit.length < limits.minBubble && i + 1 < units.length)
|
|
316
|
+
units[i + 1] = joinBlocks(unit, units[i + 1]!);
|
|
317
|
+
else merged.push(unit);
|
|
318
|
+
}
|
|
319
|
+
return merged;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function spillOf(
|
|
323
|
+
markdown: string,
|
|
324
|
+
blocks: Block[],
|
|
325
|
+
limits: ChatLimits,
|
|
326
|
+
): SpillShape {
|
|
327
|
+
const heading = blocks.find((b) => b.kind === 'heading');
|
|
328
|
+
const content = blocks.filter((b) => b.kind !== 'heading');
|
|
329
|
+
const [first, second] = content;
|
|
330
|
+
let lead = '';
|
|
331
|
+
if (first && isList(first.token))
|
|
332
|
+
lead = preview(first.token, limits.previewItems);
|
|
333
|
+
else if (
|
|
334
|
+
first?.kind === 'paragraph' &&
|
|
335
|
+
endsWithColon(first.md) &&
|
|
336
|
+
second &&
|
|
337
|
+
isList(second.token)
|
|
338
|
+
)
|
|
339
|
+
lead = joinBlocks(first.md, preview(second.token, limits.previewItems));
|
|
340
|
+
else if (first && (first.kind === 'paragraph' || first.kind === 'quote'))
|
|
341
|
+
lead = trimToSentence(first.md, limits.bubbleTarget);
|
|
342
|
+
const last = content.at(-1);
|
|
343
|
+
const closing =
|
|
344
|
+
last &&
|
|
345
|
+
last !== first &&
|
|
346
|
+
last.kind === 'paragraph' &&
|
|
347
|
+
/\?\s*$/.test(last.md) &&
|
|
348
|
+
last.md.length <= limits.bubbleMax
|
|
349
|
+
? last.md
|
|
350
|
+
: undefined;
|
|
351
|
+
const titleSource =
|
|
352
|
+
heading?.md ?? (first?.kind === 'paragraph' ? first.md : undefined);
|
|
353
|
+
return {
|
|
354
|
+
title: (titleSource && titleOf(titleSource)) || 'Details',
|
|
355
|
+
markdown,
|
|
356
|
+
lead,
|
|
357
|
+
...(closing ? { closing } : {}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Shape one model step for a chat surface. With `canSpill` false (no
|
|
363
|
+
* artefact storage configured) the step is always split into messages:
|
|
364
|
+
* tables become lists and nothing is dropped.
|
|
365
|
+
*/
|
|
366
|
+
export function shapeStep(
|
|
367
|
+
markdown: string,
|
|
368
|
+
limits: ChatLimits,
|
|
369
|
+
canSpill: boolean,
|
|
370
|
+
): StepShape {
|
|
371
|
+
const blocks = toBlocks(markdown.trim(), limits);
|
|
372
|
+
const messages = pack(blocks, limits);
|
|
373
|
+
if (!canSpill) return { kind: 'messages', messages };
|
|
374
|
+
const length = blocks.reduce((n, b) => n + b.md.length + 2, 0);
|
|
375
|
+
const tooStructured = blocks.some(
|
|
376
|
+
(b) =>
|
|
377
|
+
(b.kind === 'table' && !limits.tables) ||
|
|
378
|
+
(isCode(b.token) &&
|
|
379
|
+
b.token.text.split('\n').length > limits.maxCodeLines) ||
|
|
380
|
+
(isList(b.token) && b.token.items.length > limits.maxListItems),
|
|
381
|
+
);
|
|
382
|
+
if (
|
|
383
|
+
length > limits.spillChars ||
|
|
384
|
+
tooStructured ||
|
|
385
|
+
messages.length > limits.maxBubbles
|
|
386
|
+
)
|
|
387
|
+
return { kind: 'spill', spill: spillOf(markdown.trim(), blocks, limits) };
|
|
388
|
+
return { kind: 'messages', messages };
|
|
389
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat delivery: how a finished turn reaches a chat surface (an IXO Channels
|
|
3
|
+
* provider such as WhatsApp, or a Matrix room) as a few short messages and
|
|
4
|
+
* artefact links instead of the one streamed document the Portal renders.
|
|
5
|
+
*
|
|
6
|
+
* Plain data only. The Matrix gateway script imports these types without the
|
|
7
|
+
* agent core, and IXO Channels reads the same shapes off `/channels/turn`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Size and shape limits the shaper enforces on one chat surface. */
|
|
11
|
+
export interface ChatLimits {
|
|
12
|
+
/** Soft size of one message, in characters. */
|
|
13
|
+
bubbleTarget: number;
|
|
14
|
+
/** Hard size of one message before it is split at a sentence boundary. */
|
|
15
|
+
bubbleMax: number;
|
|
16
|
+
/** Fragments shorter than this merge into the message that follows. */
|
|
17
|
+
minBubble: number;
|
|
18
|
+
/** Messages one model step may become before it moves to an artefact. */
|
|
19
|
+
maxBubbles: number;
|
|
20
|
+
/**
|
|
21
|
+
* Parts one run delivers at most, met by merging text within `bubbleMax`
|
|
22
|
+
* and dropping spill framing. Text found nowhere else is never dropped.
|
|
23
|
+
*/
|
|
24
|
+
maxPartsPerRun: number;
|
|
25
|
+
/** A step longer than this moves to an artefact. */
|
|
26
|
+
spillChars: number;
|
|
27
|
+
/** A longer list moves to an artefact, previewed by its first items. */
|
|
28
|
+
maxListItems: number;
|
|
29
|
+
/** List items kept in the message when a list moves to an artefact. */
|
|
30
|
+
previewItems: number;
|
|
31
|
+
/** A longer code block moves to an artefact. */
|
|
32
|
+
maxCodeLines: number;
|
|
33
|
+
/** Whether a Markdown table may stay inside a message. */
|
|
34
|
+
tables: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How one turn's reply is delivered. `stream` is the Portal: SSE frames,
|
|
39
|
+
* nothing reshaped. `chat` builds a Reply Plan under `limits`.
|
|
40
|
+
*/
|
|
41
|
+
export type DeliveryProfile =
|
|
42
|
+
| { kind: 'stream' }
|
|
43
|
+
| {
|
|
44
|
+
kind: 'chat';
|
|
45
|
+
/** `whatsapp`, `telegram`, `slack`, `matrix` or `generic`. */
|
|
46
|
+
surface: string;
|
|
47
|
+
/** How the prompt names the surface: "WhatsApp", "a Matrix chat room". */
|
|
48
|
+
label: string;
|
|
49
|
+
limits: ChatLimits;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** A browser-openable document delivered as a link. */
|
|
53
|
+
export interface ArtifactRef {
|
|
54
|
+
artifactId: string;
|
|
55
|
+
title: string;
|
|
56
|
+
/**
|
|
57
|
+
* Viewer link. Its decryption key sits after `#`, so R2 and the viewer host
|
|
58
|
+
* never receive it; the oracle stores it with the user's data.
|
|
59
|
+
*/
|
|
60
|
+
url: string;
|
|
61
|
+
mime: 'text/markdown';
|
|
62
|
+
bytes: number;
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One delivery unit. `text` carries chat Markdown: bold, italic, strike,
|
|
68
|
+
* inline code, fenced code, `-` and `1.` lists, `>` quotes and links. No
|
|
69
|
+
* headings, tables or raw HTML reach a text part.
|
|
70
|
+
*/
|
|
71
|
+
export type ReplyPart =
|
|
72
|
+
| { partId: string; kind: 'text'; text: string }
|
|
73
|
+
| { partId: string; kind: 'artifact'; artifact: ArtifactRef };
|
|
74
|
+
|
|
75
|
+
/** A part before it is numbered: what a plan is assembled from. */
|
|
76
|
+
export type ReplyContent =
|
|
77
|
+
| { kind: 'text'; text: string }
|
|
78
|
+
| { kind: 'artifact'; artifact: ArtifactRef };
|
|
79
|
+
|
|
80
|
+
/** What a chat surface delivers for one finished run, in delivery order. */
|
|
81
|
+
export interface ReplyPlan {
|
|
82
|
+
v: 1;
|
|
83
|
+
parts: ReplyPart[];
|
|
84
|
+
}
|
package/src/do/ambient.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - emit → routed to the SSE writer of the turn that owns the
|
|
13
13
|
* payload's `sessionId` (same shape as the Node `wsEmitter`).
|
|
14
14
|
*/
|
|
15
|
+
import type { DecisionEvaluator } from '@ixo/common/ai/decisions';
|
|
15
16
|
import type {
|
|
16
17
|
AmbientServices,
|
|
17
18
|
DelegationLike,
|
|
@@ -19,12 +20,16 @@ import type {
|
|
|
19
20
|
RawEventPayload,
|
|
20
21
|
SecretsAdapter,
|
|
21
22
|
} from '../core/runtime-context';
|
|
22
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
delegationHasCapability,
|
|
25
|
+
type LlmAdapter,
|
|
26
|
+
} from '../core/runtime-context';
|
|
23
27
|
import type {
|
|
24
28
|
Logger,
|
|
25
29
|
MatrixEvent,
|
|
26
30
|
OracleIdentity,
|
|
27
31
|
RoomStateSnapshot,
|
|
32
|
+
UserKvSurface,
|
|
28
33
|
} from '../plugin-api/types';
|
|
29
34
|
import type { MatrixGatewayObject } from './contracts';
|
|
30
35
|
import { type WorkersUcanService } from './ucan-service';
|
|
@@ -127,6 +132,44 @@ export function createBlobStore(
|
|
|
127
132
|
};
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
/** Keys one blob sweep call reads at most (one `storage.list` page). */
|
|
136
|
+
export const BLOB_SWEEP_PAGE = 128;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Delete expired blobs (a plugin may store one and never read it back; a
|
|
140
|
+
* read deletes an expired blob, nothing else does). Bounded: one call reads
|
|
141
|
+
* at most `limit` entries in key order after `startAfter`, deletes the
|
|
142
|
+
* expired ones, and returns where the next call continues (`next`), or
|
|
143
|
+
* `null` once the end of the `blob:` keys was reached.
|
|
144
|
+
*/
|
|
145
|
+
export async function sweepExpiredBlobs(
|
|
146
|
+
storage: Pick<DurableObjectStorage, 'list' | 'delete'>,
|
|
147
|
+
options: { startAfter?: string; limit?: number; now?: number } = {},
|
|
148
|
+
): Promise<{ deleted: number; next: string | null }> {
|
|
149
|
+
const limit = Math.max(1, Math.min(options.limit ?? BLOB_SWEEP_PAGE, 128));
|
|
150
|
+
const now = options.now ?? Date.now();
|
|
151
|
+
const page = await storage.list<{ expiresAt?: unknown }>({
|
|
152
|
+
prefix: 'blob:',
|
|
153
|
+
limit,
|
|
154
|
+
...(options.startAfter !== undefined
|
|
155
|
+
? { startAfter: options.startAfter }
|
|
156
|
+
: {}),
|
|
157
|
+
});
|
|
158
|
+
const expired: string[] = [];
|
|
159
|
+
let last: string | null = null;
|
|
160
|
+
for (const [key, entry] of page) {
|
|
161
|
+
last = key;
|
|
162
|
+
const expiresAt = entry?.expiresAt;
|
|
163
|
+
if (typeof expiresAt !== 'number' || expiresAt <= now) expired.push(key);
|
|
164
|
+
}
|
|
165
|
+
// `delete` takes at most 128 keys, the page size cap.
|
|
166
|
+
if (expired.length > 0) await storage.delete(expired);
|
|
167
|
+
return {
|
|
168
|
+
deleted: expired.length,
|
|
169
|
+
next: page.size < limit ? null : last,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
export function createMatrixAdapter(
|
|
131
174
|
gateway: DurableObjectStub<MatrixGatewayObject>,
|
|
132
175
|
): AmbientServices['matrix'] {
|
|
@@ -183,25 +226,12 @@ export function createMatrixAdapter(
|
|
|
183
226
|
};
|
|
184
227
|
}
|
|
185
228
|
|
|
186
|
-
function abilityCovers(granted: string, required: string): boolean {
|
|
187
|
-
if (granted === '*' || granted === required) return true;
|
|
188
|
-
if (granted.endsWith('/*')) {
|
|
189
|
-
const ns = granted.slice(0, -2);
|
|
190
|
-
return required === ns || required.startsWith(`${ns}/`);
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
229
|
export function createUcanAdapter(
|
|
196
230
|
service: WorkersUcanService,
|
|
197
231
|
delegationFor: (userDid: string) => { raw?: string } | undefined,
|
|
198
232
|
): AmbientServices['ucan'] {
|
|
199
233
|
const has = (delegation: DelegationLike, resource: string, action: string) =>
|
|
200
|
-
(delegation
|
|
201
|
-
(c) =>
|
|
202
|
-
(c.resource === resource || c.resource.startsWith(resource)) &&
|
|
203
|
-
abilityCovers(c.action, action),
|
|
204
|
-
);
|
|
234
|
+
delegationHasCapability(delegation, resource, action);
|
|
205
235
|
return {
|
|
206
236
|
hasCapability: has,
|
|
207
237
|
requireCapability: (delegation, resource, action) => {
|
|
@@ -239,6 +269,8 @@ export interface CreateAmbientInput {
|
|
|
239
269
|
/** Host task scheduler surface for this user, when tasks are enabled. */
|
|
240
270
|
tasks?: import('../plugin-api/types').OracleTasksSurface;
|
|
241
271
|
preferences?: import('../plugin-api/types').UserPreferencesSurface;
|
|
272
|
+
/** `ctx.kv` over the user's working copy (`SqliteUserKv`). */
|
|
273
|
+
kv?: UserKvSurface;
|
|
242
274
|
/** Host bridge to the user's browser (realtime channel), when terminated here. */
|
|
243
275
|
frontend?: import('../plugin-api/types').FrontendCallSurface;
|
|
244
276
|
/** Per-turn cleanup registry (see `RuntimeContext.onTurnEnd`). */
|
|
@@ -249,6 +281,8 @@ export interface CreateAmbientInput {
|
|
|
249
281
|
identity: OracleIdentity;
|
|
250
282
|
availablePlugins: ReadonlySet<string>;
|
|
251
283
|
llm: LlmAdapter;
|
|
284
|
+
/** The core's Decision evaluator (`RuntimeCore.decisions`), behind `ctx.decisions`. */
|
|
285
|
+
decisions?: DecisionEvaluator;
|
|
252
286
|
logger: Logger;
|
|
253
287
|
storage: DurableObjectStorage;
|
|
254
288
|
gateway: DurableObjectStub<MatrixGatewayObject>;
|
|
@@ -269,6 +303,7 @@ export function createAmbientServices(
|
|
|
269
303
|
return {
|
|
270
304
|
...(input.tasks ? { tasks: input.tasks } : {}),
|
|
271
305
|
...(input.preferences ? { preferences: input.preferences } : {}),
|
|
306
|
+
...(input.kv ? { kv: input.kv } : {}),
|
|
272
307
|
...(input.frontend ? { frontend: input.frontend } : {}),
|
|
273
308
|
...(input.onTurnEnd ? { onTurnEnd: input.onTurnEnd } : {}),
|
|
274
309
|
...(input.background ? { background: input.background } : {}),
|
|
@@ -276,6 +311,7 @@ export function createAmbientServices(
|
|
|
276
311
|
identity: input.identity,
|
|
277
312
|
availablePlugins: input.availablePlugins,
|
|
278
313
|
llm: input.llm,
|
|
314
|
+
...(input.decisions ? { decisions: input.decisions } : {}),
|
|
279
315
|
logger: input.logger,
|
|
280
316
|
emit: input.events,
|
|
281
317
|
blobStore: createBlobStore(input.storage),
|