@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content assistant's tool surface: three reads and five writes over one
|
|
3
|
+
* BlockNote document.
|
|
4
|
+
*
|
|
5
|
+
* Every write goes through `applyDocumentEdit` (see `content-session.ts`), so
|
|
6
|
+
* the flow guard, the access guard, the prop allowlist, the single transaction,
|
|
7
|
+
* and the flush-before-success rule are enforced in one place rather than
|
|
8
|
+
* per tool.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
12
|
+
import type * as Y from 'yjs';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import { tool as pluginTool } from '../../plugin-api/tool-helper';
|
|
16
|
+
import type { PluginTool } from '../../plugin-api/types';
|
|
17
|
+
import {
|
|
18
|
+
markdownToBlockContainers,
|
|
19
|
+
markdownToInlineContent,
|
|
20
|
+
} from './blocknote-bridge';
|
|
21
|
+
import { applyDocumentEdit, withDocument } from './content-session';
|
|
22
|
+
import {
|
|
23
|
+
appendBlocks,
|
|
24
|
+
flattenBlocks,
|
|
25
|
+
insertBlocksRelativeTo,
|
|
26
|
+
isDescendantBlock,
|
|
27
|
+
locateBlock,
|
|
28
|
+
moveBlock,
|
|
29
|
+
prependBlocks,
|
|
30
|
+
readBlockById,
|
|
31
|
+
readDocumentBlocks,
|
|
32
|
+
readDocumentMetadata,
|
|
33
|
+
readDocumentTitle,
|
|
34
|
+
removeBlock,
|
|
35
|
+
replaceTextInDocument,
|
|
36
|
+
writeBlockInlineContent,
|
|
37
|
+
writeBlockProps,
|
|
38
|
+
DOCUMENT_FRAGMENT_NAME,
|
|
39
|
+
getContentElement,
|
|
40
|
+
type DocumentBlock,
|
|
41
|
+
type FlatDocumentBlock,
|
|
42
|
+
} from './document-model';
|
|
43
|
+
import {
|
|
44
|
+
blockNotFound,
|
|
45
|
+
editorError,
|
|
46
|
+
propNotEditable,
|
|
47
|
+
type EditorFailure,
|
|
48
|
+
} from './failures';
|
|
49
|
+
import {
|
|
50
|
+
filterWritableProps,
|
|
51
|
+
isTextEditable,
|
|
52
|
+
redactProps,
|
|
53
|
+
} from './prop-policy';
|
|
54
|
+
import type { AppConfig } from './provider';
|
|
55
|
+
|
|
56
|
+
export interface ContentToolsOptions {
|
|
57
|
+
matrixClient: MatrixClient;
|
|
58
|
+
/** Provider config with the target room already baked in. */
|
|
59
|
+
appConfig: AppConfig;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_READ_LIMIT = 60;
|
|
63
|
+
const MAX_READ_LIMIT = 200;
|
|
64
|
+
|
|
65
|
+
const blockIdSchema = z
|
|
66
|
+
.string()
|
|
67
|
+
.min(1)
|
|
68
|
+
.describe('The block id, exactly as returned by read_document.');
|
|
69
|
+
|
|
70
|
+
function json(value: unknown): string {
|
|
71
|
+
return JSON.stringify(value, null, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Redact prop values the assistant must not relay, at every depth. */
|
|
75
|
+
function redactBlock(block: DocumentBlock): DocumentBlock {
|
|
76
|
+
return {
|
|
77
|
+
...block,
|
|
78
|
+
props: redactProps(block.type, block.props),
|
|
79
|
+
children: block.children.map(redactBlock),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function redactFlat(block: FlatDocumentBlock): FlatDocumentBlock {
|
|
84
|
+
return { ...block, props: redactProps(block.type, block.props) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Case-insensitive substring match. */
|
|
88
|
+
function matches(haystack: string, needle: string): boolean {
|
|
89
|
+
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Reads ────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function createReadDocumentTool(options: ContentToolsOptions): PluginTool {
|
|
95
|
+
const schema = z.object({
|
|
96
|
+
start: z
|
|
97
|
+
.number()
|
|
98
|
+
.int()
|
|
99
|
+
.min(0)
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Position of the first block to return (default 0).'),
|
|
102
|
+
limit: z
|
|
103
|
+
.number()
|
|
104
|
+
.int()
|
|
105
|
+
.min(1)
|
|
106
|
+
.max(MAX_READ_LIMIT)
|
|
107
|
+
.optional()
|
|
108
|
+
.describe(`How many blocks to return (default ${DEFAULT_READ_LIMIT}).`),
|
|
109
|
+
block_type: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Return only blocks of this type, e.g. 'heading'."),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return pluginTool(
|
|
116
|
+
async (rawArgs) => {
|
|
117
|
+
const args = schema.parse(rawArgs);
|
|
118
|
+
const start = args.start ?? 0;
|
|
119
|
+
const limit = args.limit ?? DEFAULT_READ_LIMIT;
|
|
120
|
+
|
|
121
|
+
const result = await withDocument(options, async (session) => {
|
|
122
|
+
const blocks = flattenBlocks(readDocumentBlocks(session.doc)).map(
|
|
123
|
+
redactFlat,
|
|
124
|
+
);
|
|
125
|
+
const filtered = args.block_type
|
|
126
|
+
? blocks.filter((block) => block.type === args.block_type)
|
|
127
|
+
: blocks;
|
|
128
|
+
const page = filtered.slice(start, start + limit);
|
|
129
|
+
return {
|
|
130
|
+
ok: true as const,
|
|
131
|
+
roomId: session.roomId,
|
|
132
|
+
readOnly: session.isFlow,
|
|
133
|
+
title: readDocumentTitle(session.doc),
|
|
134
|
+
metadata: readDocumentMetadata(session.doc),
|
|
135
|
+
total: filtered.length,
|
|
136
|
+
start,
|
|
137
|
+
count: page.length,
|
|
138
|
+
blocks: page,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return json(result);
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'read_document',
|
|
146
|
+
description:
|
|
147
|
+
"Read the open document's blocks in order. Returns each block's id, " +
|
|
148
|
+
'type, props, and text (markdown), plus the page title. Call this ' +
|
|
149
|
+
'before any edit — block ids come from here and must never be guessed ' +
|
|
150
|
+
'or reused from an earlier turn. Paginate with start/limit for long ' +
|
|
151
|
+
'documents. `readOnly: true` means the document is a live flow and ' +
|
|
152
|
+
'edits will be refused.',
|
|
153
|
+
schema,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createReadBlockTool(options: ContentToolsOptions): PluginTool {
|
|
159
|
+
const schema = z.object({ block_id: blockIdSchema });
|
|
160
|
+
|
|
161
|
+
return pluginTool(
|
|
162
|
+
async (rawArgs) => {
|
|
163
|
+
const args = schema.parse(rawArgs);
|
|
164
|
+
const result = await withDocument(options, async (session) => {
|
|
165
|
+
const block = readBlockById(session.doc, args.block_id);
|
|
166
|
+
if (!block) return blockNotFound(args.block_id);
|
|
167
|
+
return {
|
|
168
|
+
ok: true as const,
|
|
169
|
+
roomId: session.roomId,
|
|
170
|
+
block: redactBlock(block),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return json(result);
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'read_block',
|
|
177
|
+
description:
|
|
178
|
+
'Read one block by id, including its nested child blocks. Use after ' +
|
|
179
|
+
'read_document when you need the full detail of a single block.',
|
|
180
|
+
schema,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createSearchDocumentTool(options: ContentToolsOptions): PluginTool {
|
|
186
|
+
const schema = z.object({
|
|
187
|
+
query: z.string().min(1).describe('Text to look for.'),
|
|
188
|
+
limit: z
|
|
189
|
+
.number()
|
|
190
|
+
.int()
|
|
191
|
+
.min(1)
|
|
192
|
+
.max(MAX_READ_LIMIT)
|
|
193
|
+
.optional()
|
|
194
|
+
.describe('Maximum matches to return (default 20).'),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return pluginTool(
|
|
198
|
+
async (rawArgs) => {
|
|
199
|
+
const args = schema.parse(rawArgs);
|
|
200
|
+
const limit = args.limit ?? 20;
|
|
201
|
+
|
|
202
|
+
const result = await withDocument(options, async (session) => {
|
|
203
|
+
const blocks = flattenBlocks(readDocumentBlocks(session.doc)).map(
|
|
204
|
+
redactFlat,
|
|
205
|
+
);
|
|
206
|
+
const hits: Array<{
|
|
207
|
+
matchedIn: 'text' | 'props';
|
|
208
|
+
block: FlatDocumentBlock;
|
|
209
|
+
}> = [];
|
|
210
|
+
|
|
211
|
+
for (const block of blocks) {
|
|
212
|
+
if (matches(block.text, args.query)) {
|
|
213
|
+
hits.push({ matchedIn: 'text', block });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const propHit = Object.values(block.props).some(
|
|
217
|
+
(value) => typeof value === 'string' && matches(value, args.query),
|
|
218
|
+
);
|
|
219
|
+
if (propHit) hits.push({ matchedIn: 'props', block });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
ok: true as const,
|
|
224
|
+
roomId: session.roomId,
|
|
225
|
+
query: args.query,
|
|
226
|
+
total: hits.length,
|
|
227
|
+
matches: hits.slice(0, limit),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return json(result);
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'search_document',
|
|
235
|
+
description:
|
|
236
|
+
'Find blocks whose text or props contain a phrase. Returns the same ' +
|
|
237
|
+
'block shape as read_document. Use this instead of reading a long ' +
|
|
238
|
+
'document page by page when you know what you are looking for.',
|
|
239
|
+
schema,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Writes ───────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
function createInsertContentTool(options: ContentToolsOptions): PluginTool {
|
|
247
|
+
const schema = z.object({
|
|
248
|
+
markdown: z
|
|
249
|
+
.string()
|
|
250
|
+
.min(1)
|
|
251
|
+
.describe(
|
|
252
|
+
'The content to insert, as markdown. Headings, lists, bold/italic, ' +
|
|
253
|
+
'code and links are supported; each markdown block becomes one ' +
|
|
254
|
+
'document block.',
|
|
255
|
+
),
|
|
256
|
+
position: z
|
|
257
|
+
.enum(['end', 'start', 'before', 'after'])
|
|
258
|
+
.optional()
|
|
259
|
+
.describe(
|
|
260
|
+
"Where to insert (default 'end'). 'before'/'after' require " +
|
|
261
|
+
'reference_block_id.',
|
|
262
|
+
),
|
|
263
|
+
reference_block_id: z
|
|
264
|
+
.string()
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Anchor block for 'before'/'after'."),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return pluginTool(
|
|
270
|
+
async (rawArgs) => {
|
|
271
|
+
const args = schema.parse(rawArgs);
|
|
272
|
+
const position = args.position ?? 'end';
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
(position === 'before' || position === 'after') &&
|
|
276
|
+
!args.reference_block_id
|
|
277
|
+
) {
|
|
278
|
+
return json(
|
|
279
|
+
editorError(
|
|
280
|
+
`position '${position}' requires reference_block_id. Call ` +
|
|
281
|
+
'read_document to get a block id, or use end/start.',
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parsing is independent of the document, so it happens before the write
|
|
287
|
+
// path opens its transaction.
|
|
288
|
+
const containers = await markdownToBlockContainers(args.markdown);
|
|
289
|
+
if (containers.length === 0) {
|
|
290
|
+
return json(
|
|
291
|
+
editorError('That markdown produced no blocks — nothing to insert.'),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const result = await withDocument(options, async (session) =>
|
|
296
|
+
applyDocumentEdit(session, {
|
|
297
|
+
plan: (doc) => {
|
|
298
|
+
// Anchored inserts need a live reference; end/start do not.
|
|
299
|
+
if (position !== 'before' && position !== 'after') {
|
|
300
|
+
return { reference: '' };
|
|
301
|
+
}
|
|
302
|
+
const reference = args.reference_block_id ?? '';
|
|
303
|
+
const located = locateBlock(
|
|
304
|
+
doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME),
|
|
305
|
+
reference,
|
|
306
|
+
);
|
|
307
|
+
if (!located) return blockNotFound(reference);
|
|
308
|
+
return { reference };
|
|
309
|
+
},
|
|
310
|
+
apply: (doc, plan) => {
|
|
311
|
+
if (position === 'end') {
|
|
312
|
+
appendBlocks(doc, containers);
|
|
313
|
+
} else if (position === 'start') {
|
|
314
|
+
prependBlocks(doc, containers);
|
|
315
|
+
} else {
|
|
316
|
+
insertBlocksRelativeTo(
|
|
317
|
+
doc,
|
|
318
|
+
plan.reference,
|
|
319
|
+
position === 'before' ? 'before' : 'after',
|
|
320
|
+
containers,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
ok: true as const,
|
|
325
|
+
roomId: session.roomId,
|
|
326
|
+
inserted: containers.length,
|
|
327
|
+
blockIds: containers.map(
|
|
328
|
+
(container) => container.getAttribute('id') ?? '',
|
|
329
|
+
),
|
|
330
|
+
position,
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return json(result);
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'insert_content',
|
|
340
|
+
description:
|
|
341
|
+
'Add new content to the document from markdown. Inserts at the end by ' +
|
|
342
|
+
'default, or relative to an existing block. This is how you write new ' +
|
|
343
|
+
'prose — do not try to create custom blocks (checkboxes, actions, ' +
|
|
344
|
+
'forms) with it.',
|
|
345
|
+
schema,
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface PreparedEdit {
|
|
351
|
+
blockId: string;
|
|
352
|
+
props: Record<string, string>;
|
|
353
|
+
inlineNodes: Array<Y.XmlText | Y.XmlElement> | undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function createEditBlockTool(options: ContentToolsOptions): PluginTool {
|
|
357
|
+
const editSchema = z.object({
|
|
358
|
+
block_id: blockIdSchema,
|
|
359
|
+
text: z
|
|
360
|
+
.string()
|
|
361
|
+
.optional()
|
|
362
|
+
.describe(
|
|
363
|
+
'Replacement text for the block, as markdown inline content. Only ' +
|
|
364
|
+
'valid on prose blocks (paragraph, heading, list items, quote, …).',
|
|
365
|
+
),
|
|
366
|
+
props: z
|
|
367
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
368
|
+
.optional()
|
|
369
|
+
.describe(
|
|
370
|
+
'Props to set. On prose blocks any prop is allowed. On custom IXO ' +
|
|
371
|
+
"blocks only 'title' and 'description' are editable — everything " +
|
|
372
|
+
'else configures behaviour and will be refused.',
|
|
373
|
+
),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const schema = z.object({
|
|
377
|
+
edits: z
|
|
378
|
+
.array(editSchema)
|
|
379
|
+
.min(1)
|
|
380
|
+
.describe(
|
|
381
|
+
'The edits to apply. All of them land in one atomic change, so batch ' +
|
|
382
|
+
'related edits into a single call instead of calling repeatedly.',
|
|
383
|
+
),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return pluginTool(
|
|
387
|
+
async (rawArgs) => {
|
|
388
|
+
const args = schema.parse(rawArgs);
|
|
389
|
+
|
|
390
|
+
// Markdown is parsed up front: it does not depend on the document, and
|
|
391
|
+
// keeping it out of the transaction keeps the write window small.
|
|
392
|
+
const parsedText = new Map<number, Array<Y.XmlText | Y.XmlElement>>();
|
|
393
|
+
for (const [index, edit] of args.edits.entries()) {
|
|
394
|
+
if (edit.text === undefined) continue;
|
|
395
|
+
parsedText.set(index, await markdownToInlineContent(edit.text));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = await withDocument(options, async (session) =>
|
|
399
|
+
applyDocumentEdit(session, {
|
|
400
|
+
plan: (doc): PreparedEdit[] | EditorFailure => {
|
|
401
|
+
const prepared: PreparedEdit[] = [];
|
|
402
|
+
const fragment = doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME);
|
|
403
|
+
|
|
404
|
+
for (const [index, edit] of args.edits.entries()) {
|
|
405
|
+
const located = locateBlock(fragment, edit.block_id);
|
|
406
|
+
if (!located) return blockNotFound(edit.block_id);
|
|
407
|
+
const content = getContentElement(located.container);
|
|
408
|
+
if (!content) {
|
|
409
|
+
return editorError(
|
|
410
|
+
`Block ${edit.block_id} has no content element and cannot be edited.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const blockType = content.nodeName;
|
|
414
|
+
|
|
415
|
+
const filtered = filterWritableProps(blockType, edit.props ?? {});
|
|
416
|
+
if (filtered.rejected.length > 0) {
|
|
417
|
+
return propNotEditable(
|
|
418
|
+
edit.block_id,
|
|
419
|
+
blockType,
|
|
420
|
+
filtered.rejected,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (edit.text !== undefined && !isTextEditable(blockType)) {
|
|
425
|
+
return propNotEditable(edit.block_id, blockType, [
|
|
426
|
+
{
|
|
427
|
+
prop: 'text',
|
|
428
|
+
reason: `'${blockType}' blocks hold no editable text — set title or description instead`,
|
|
429
|
+
},
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
prepared.push({
|
|
434
|
+
blockId: edit.block_id,
|
|
435
|
+
props: filtered.allowed,
|
|
436
|
+
inlineNodes: parsedText.get(index),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return prepared;
|
|
441
|
+
},
|
|
442
|
+
apply: (doc, prepared) => {
|
|
443
|
+
const fragment = doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME);
|
|
444
|
+
const applied: string[] = [];
|
|
445
|
+
for (const edit of prepared) {
|
|
446
|
+
const located = locateBlock(fragment, edit.blockId);
|
|
447
|
+
if (!located) continue;
|
|
448
|
+
if (Object.keys(edit.props).length > 0) {
|
|
449
|
+
writeBlockProps(located.container, edit.props);
|
|
450
|
+
}
|
|
451
|
+
if (edit.inlineNodes) {
|
|
452
|
+
writeBlockInlineContent(located.container, edit.inlineNodes);
|
|
453
|
+
}
|
|
454
|
+
applied.push(edit.blockId);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
ok: true as const,
|
|
458
|
+
roomId: session.roomId,
|
|
459
|
+
edited: applied.length,
|
|
460
|
+
blockIds: applied,
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
return json(result);
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'edit_block',
|
|
470
|
+
description:
|
|
471
|
+
'Change existing blocks — their text, their props, or both. Accepts a ' +
|
|
472
|
+
'batch: every edit in one call is applied atomically, and if any edit ' +
|
|
473
|
+
'is refused none of them are applied. Prose blocks accept text and any ' +
|
|
474
|
+
"prop; custom IXO blocks accept only 'title' and 'description'.",
|
|
475
|
+
schema,
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function createDeleteBlockTool(options: ContentToolsOptions): PluginTool {
|
|
481
|
+
const schema = z.object({ block_id: blockIdSchema });
|
|
482
|
+
|
|
483
|
+
return pluginTool(
|
|
484
|
+
async (rawArgs) => {
|
|
485
|
+
const args = schema.parse(rawArgs);
|
|
486
|
+
|
|
487
|
+
const result = await withDocument(options, async (session) =>
|
|
488
|
+
applyDocumentEdit(session, {
|
|
489
|
+
plan: (doc) => {
|
|
490
|
+
const located = locateBlock(
|
|
491
|
+
doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME),
|
|
492
|
+
args.block_id,
|
|
493
|
+
);
|
|
494
|
+
if (!located) return blockNotFound(args.block_id);
|
|
495
|
+
const content = getContentElement(located.container);
|
|
496
|
+
return { blockType: content?.nodeName ?? 'unknown' };
|
|
497
|
+
},
|
|
498
|
+
apply: (doc, plan) => {
|
|
499
|
+
const removed = removeBlock(doc, args.block_id);
|
|
500
|
+
return {
|
|
501
|
+
ok: true as const,
|
|
502
|
+
roomId: session.roomId,
|
|
503
|
+
removed,
|
|
504
|
+
blockId: args.block_id,
|
|
505
|
+
blockType: plan.blockType,
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
return json(result);
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'delete_block',
|
|
515
|
+
description:
|
|
516
|
+
'Delete one block and everything nested inside it. Deletion cannot be ' +
|
|
517
|
+
'undone from here — confirm with the user before deleting content they ' +
|
|
518
|
+
'did not explicitly ask you to remove.',
|
|
519
|
+
schema,
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function createMoveBlockTool(options: ContentToolsOptions): PluginTool {
|
|
525
|
+
const schema = z.object({
|
|
526
|
+
block_id: blockIdSchema,
|
|
527
|
+
reference_block_id: z
|
|
528
|
+
.string()
|
|
529
|
+
.min(1)
|
|
530
|
+
.describe('The block to move it next to.'),
|
|
531
|
+
placement: z
|
|
532
|
+
.enum(['before', 'after'])
|
|
533
|
+
.describe('Put the block before or after the reference block.'),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
return pluginTool(
|
|
537
|
+
async (rawArgs) => {
|
|
538
|
+
const args = schema.parse(rawArgs);
|
|
539
|
+
|
|
540
|
+
const result = await withDocument(options, async (session) =>
|
|
541
|
+
applyDocumentEdit(session, {
|
|
542
|
+
plan: (doc) => {
|
|
543
|
+
const fragment = doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME);
|
|
544
|
+
if (!locateBlock(fragment, args.block_id)) {
|
|
545
|
+
return blockNotFound(args.block_id);
|
|
546
|
+
}
|
|
547
|
+
if (!locateBlock(fragment, args.reference_block_id)) {
|
|
548
|
+
return blockNotFound(args.reference_block_id);
|
|
549
|
+
}
|
|
550
|
+
if (args.block_id === args.reference_block_id) {
|
|
551
|
+
return editorError('A block cannot be moved relative to itself.');
|
|
552
|
+
}
|
|
553
|
+
if (
|
|
554
|
+
isDescendantBlock(doc, args.block_id, args.reference_block_id)
|
|
555
|
+
) {
|
|
556
|
+
return editorError(
|
|
557
|
+
'A block cannot be moved inside its own child blocks.',
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return {};
|
|
561
|
+
},
|
|
562
|
+
apply: (doc) => {
|
|
563
|
+
const error = moveBlock(
|
|
564
|
+
doc,
|
|
565
|
+
args.block_id,
|
|
566
|
+
args.reference_block_id,
|
|
567
|
+
args.placement,
|
|
568
|
+
);
|
|
569
|
+
// `plan` already proved both blocks exist and are not nested, so
|
|
570
|
+
// this is a defensive branch: report it rather than claim success.
|
|
571
|
+
if (error) {
|
|
572
|
+
return editorError(
|
|
573
|
+
`Could not move the block (${error}).`,
|
|
574
|
+
session.roomId,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
ok: true as const,
|
|
579
|
+
roomId: session.roomId,
|
|
580
|
+
blockId: args.block_id,
|
|
581
|
+
placement: args.placement,
|
|
582
|
+
referenceBlockId: args.reference_block_id,
|
|
583
|
+
};
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
return json(result);
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: 'move_block',
|
|
592
|
+
description:
|
|
593
|
+
'Reorder the document by moving one block next to another. The block ' +
|
|
594
|
+
'keeps its id, text, props, and child blocks.',
|
|
595
|
+
schema,
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function createReplaceTextTool(options: ContentToolsOptions): PluginTool {
|
|
601
|
+
const schema = z.object({
|
|
602
|
+
find: z.string().min(1).describe('Exact text to find.'),
|
|
603
|
+
replace: z.string().describe('Replacement text. Pass "" to delete it.'),
|
|
604
|
+
case_sensitive: z
|
|
605
|
+
.boolean()
|
|
606
|
+
.optional()
|
|
607
|
+
.describe('Match case exactly (default true).'),
|
|
608
|
+
replace_all: z
|
|
609
|
+
.boolean()
|
|
610
|
+
.optional()
|
|
611
|
+
.describe('Replace every occurrence (default true).'),
|
|
612
|
+
block_id: z
|
|
613
|
+
.string()
|
|
614
|
+
.optional()
|
|
615
|
+
.describe('Restrict the replacement to a single block.'),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
return pluginTool(
|
|
619
|
+
async (rawArgs) => {
|
|
620
|
+
const args = schema.parse(rawArgs);
|
|
621
|
+
|
|
622
|
+
const result = await withDocument(options, async (session) =>
|
|
623
|
+
applyDocumentEdit(session, {
|
|
624
|
+
plan: (doc) => {
|
|
625
|
+
if (args.block_id) {
|
|
626
|
+
const located = locateBlock(
|
|
627
|
+
doc.getXmlFragment(DOCUMENT_FRAGMENT_NAME),
|
|
628
|
+
args.block_id,
|
|
629
|
+
);
|
|
630
|
+
if (!located) return blockNotFound(args.block_id);
|
|
631
|
+
}
|
|
632
|
+
return {};
|
|
633
|
+
},
|
|
634
|
+
apply: (doc) => {
|
|
635
|
+
const outcome = replaceTextInDocument(doc, {
|
|
636
|
+
find: args.find,
|
|
637
|
+
replace: args.replace,
|
|
638
|
+
caseSensitive: args.case_sensitive ?? true,
|
|
639
|
+
replaceAll: args.replace_all ?? true,
|
|
640
|
+
...(args.block_id && { blockId: args.block_id }),
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
ok: true as const,
|
|
644
|
+
roomId: session.roomId,
|
|
645
|
+
replacements: outcome.replacements,
|
|
646
|
+
blockIds: outcome.blockIds,
|
|
647
|
+
...(outcome.replacements === 0 && {
|
|
648
|
+
note:
|
|
649
|
+
'No occurrences found — the document was not changed. Read ' +
|
|
650
|
+
'the document to check the exact wording before retrying.',
|
|
651
|
+
}),
|
|
652
|
+
};
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
return json(result);
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: 'replace_text',
|
|
661
|
+
description:
|
|
662
|
+
'Replace a phrase wherever it appears in the document text, ' +
|
|
663
|
+
'preserving the surrounding formatting. Prefer this over edit_block ' +
|
|
664
|
+
'for small wording changes: it does not rewrite whole blocks.',
|
|
665
|
+
schema,
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The eight content tools, bound to one document. `isEditorFailure` is
|
|
672
|
+
* re-exported through the tools' JSON payloads: every tool returns either
|
|
673
|
+
* `{ ok: true, … }` or a typed failure the agent can act on.
|
|
674
|
+
*/
|
|
675
|
+
export function createContentTools(options: ContentToolsOptions): PluginTool[] {
|
|
676
|
+
return [
|
|
677
|
+
createReadDocumentTool(options),
|
|
678
|
+
createReadBlockTool(options),
|
|
679
|
+
createSearchDocumentTool(options),
|
|
680
|
+
createInsertContentTool(options),
|
|
681
|
+
createEditBlockTool(options),
|
|
682
|
+
createDeleteBlockTool(options),
|
|
683
|
+
createMoveBlockTool(options),
|
|
684
|
+
createReplaceTextTool(options),
|
|
685
|
+
];
|
|
686
|
+
}
|
|
Binary file
|