@elytracms/studio 0.0.56 → 0.0.58
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/convex/auth.ts +7 -0
- package/convex/users.ts +41 -14
- package/dist/components/ai/ai-chat-panel-lazy.js +3 -3
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.d.ts +0 -9
- package/dist/components/ai/ai-chat-panel.js +199 -60
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.d.ts +62 -10
- package/dist/components/ai/chat-ui.js +139 -23
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +30 -7
- package/dist/components/ai/proposal-card.js +81 -21
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/cms/detail-editor.js +12 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +12 -1
- package/dist/components/cms/document-diff-view.js +119 -42
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/rich-text-preview.d.ts +15 -0
- package/dist/components/cms/rich-text-preview.js +81 -0
- package/dist/components/cms/rich-text-preview.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +26 -0
- package/dist/components/cms/version-compare-text.js +60 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/lib/ai/anthropic-provider.d.ts +0 -12
- package/dist/lib/ai/anthropic-provider.js +3 -11
- package/dist/lib/ai/anthropic-provider.js.map +1 -1
- package/dist/lib/ai/apply-content.js +52 -10
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +74 -0
- package/dist/lib/ai/chat-context.js +100 -0
- package/dist/lib/ai/chat-context.js.map +1 -0
- package/dist/lib/ai/chat-loop.d.ts +70 -0
- package/dist/lib/ai/chat-loop.js +46 -0
- package/dist/lib/ai/chat-loop.js.map +1 -0
- package/dist/lib/ai/chat-state.d.ts +73 -57
- package/dist/lib/ai/chat-state.js +49 -90
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +139 -6
- package/dist/lib/ai/fake-provider.js +261 -8
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/gateway-provider.d.ts +30 -0
- package/dist/lib/ai/gateway-provider.js +73 -0
- package/dist/lib/ai/gateway-provider.js.map +1 -0
- package/dist/lib/ai/index.d.ts +5 -1
- package/dist/lib/ai/index.js +5 -1
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/node-handler.d.ts +13 -4
- package/dist/lib/ai/node-handler.js +33 -3
- package/dist/lib/ai/node-handler.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +27 -0
- package/dist/lib/ai/rich-text-input.js +25 -0
- package/dist/lib/ai/rich-text-input.js.map +1 -0
- package/dist/lib/ai/route-provider.d.ts +35 -10
- package/dist/lib/ai/route-provider.js +13 -6
- package/dist/lib/ai/route-provider.js.map +1 -1
- package/dist/lib/ai/route.d.ts +50 -21
- package/dist/lib/ai/route.js +69 -40
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +12 -1
- package/dist/lib/ai/shape.js +46 -20
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +242 -37
- package/dist/lib/ai/tools.js +548 -63
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +31 -16
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +68 -0
- package/dist/lib/ai/update-fields.js +296 -0
- package/dist/lib/ai/update-fields.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +53 -0
- package/dist/lib/ai-current-document.js +54 -0
- package/dist/lib/ai-current-document.js.map +1 -0
- package/dist/lib/auth/convex-auth-adapter.d.ts +9 -1
- package/dist/lib/auth/convex-auth-adapter.js +28 -2
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/i18n/messages.en.js +19 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +4 -3
- package/dist/lib/ai/create-flow.d.ts +0 -132
- package/dist/lib/ai/create-flow.js +0 -280
- package/dist/lib/ai/create-flow.js.map +0 -1
package/dist/lib/ai/tools.js
CHANGED
|
@@ -1,74 +1,559 @@
|
|
|
1
|
-
/** The schema name the planner call carries — how a FakeProvider tells plan vs. author. */
|
|
2
|
-
export const PLANNER_SCHEMA_NAME = 'create_flow_action';
|
|
3
1
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The narrow tool set the assistant works against (EC-199, reshaped by EC-334).
|
|
3
|
+
*
|
|
4
|
+
* Per the ratified AD, tools execute CLIENT-side against the operations layer with
|
|
5
|
+
* the user's own session. This module carries the three pieces of that contract:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link chatToolDefinitions} — the SDK-native tool DEFINITIONS the route's
|
|
8
|
+
* chat branch declares in `streamText`, deliberately WITHOUT `execute`, so the
|
|
9
|
+
* SDK forwards every call to the client (the route stays a zero-data-access
|
|
10
|
+
* proxy). Plain `{ description, inputSchema }` objects with zod schemas —
|
|
11
|
+
* structurally the SDK's `FunctionTool`, with no SDK import (bundle discipline).
|
|
12
|
+
* 2. {@link ChatToolReaders} — the operations-backed read interface the studio
|
|
13
|
+
* binds client-side (search / list / get). Reads only — the engine never gains
|
|
14
|
+
* a write path; apply is a separate, human-gated draft write.
|
|
15
|
+
* 3. {@link createChatToolExecutor} — the client executor map `useChat`'s
|
|
16
|
+
* `onToolCall` dispatches into. Readers pass through; the terminal tools run
|
|
17
|
+
* the deterministic engines (`applyContentWithShape` / `translateDocument` /
|
|
18
|
+
* `buildUpdateProposal`) and return a COMPACT proposal summary as the tool
|
|
19
|
+
* output — the full proposal goes to the review UI via
|
|
20
|
+
* `onProposal(toolCallId, …)`, never to the model.
|
|
21
|
+
*
|
|
22
|
+
* EC-378: `updateFields` is the edit-flow terminal tool — the model authors
|
|
23
|
+
* per-field changes directly (it has `getDocument` output + the conversation as
|
|
24
|
+
* context); the executor reads the CURRENT document, builds a validated update
|
|
25
|
+
* proposal (`buildUpdateProposal`), and hands it to the review sink. Its target
|
|
26
|
+
* (and `getDocument`'s) may be omitted to mean the document currently open in
|
|
27
|
+
* the editor — resolved via the {@link ChatToolExecutorOptions.currentDocument}
|
|
28
|
+
* seam ({@link resolveDocumentTarget}); the context block the panel injects is
|
|
29
|
+
* ADVISORY, the resolution here is what makes "this document" deterministic.
|
|
30
|
+
*
|
|
31
|
+
* The EC-199 planner enum/schema (`plannerSchema` / `parsePlannerAction`) retired
|
|
32
|
+
* with the hand-rolled orchestrator (EC-334 slice 2).
|
|
7
33
|
*/
|
|
8
|
-
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
36
|
+
import { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text';
|
|
37
|
+
import { applyContentWithShape } from './apply-content.js';
|
|
38
|
+
import { translateDocument } from './translate-document.js';
|
|
39
|
+
import { buildUpdateProposal } from './update-fields.js';
|
|
40
|
+
import { aiProvenance } from './provenance.js';
|
|
41
|
+
/** Per-search result cap (protects the wire — results become model context). */
|
|
42
|
+
export const DEFAULT_SEARCH_LIMIT = 20;
|
|
43
|
+
/**
|
|
44
|
+
* The tool set the native chat branch declares (EC-334). No `execute` on any entry:
|
|
45
|
+
* the SDK forwards each call to the client, which executes reads against the
|
|
46
|
+
* operations layer with the user's session and posts the result back; the terminal
|
|
47
|
+
* tools (`createDocument` / `translateDocument`) run the deterministic engines
|
|
48
|
+
* client-side and return a compact proposal summary as the tool output. The route
|
|
49
|
+
* itself never executes anything.
|
|
50
|
+
*
|
|
51
|
+
* Collections are NOT enum-leashed here — the route has zero data access, so it has
|
|
52
|
+
* no catalog; the catalog reaches the model through the request `context` (system
|
|
53
|
+
* prompt), and every id a proposal references must come from a tool RESULT
|
|
54
|
+
* (client-validated; `applyContentWithShape` still enum-leashes relation ids).
|
|
55
|
+
*/
|
|
56
|
+
export const chatToolDefinitions = {
|
|
57
|
+
searchDocuments: {
|
|
58
|
+
description: 'Search documents in a collection. Use it to find the REAL ids a relation may ' +
|
|
59
|
+
'reference, or to locate an existing document — never invent an id. Returns ' +
|
|
60
|
+
'{ id, title } summaries.',
|
|
61
|
+
inputSchema: z.object({
|
|
62
|
+
collection: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe('Target collection id (from the catalog in the system context).'),
|
|
65
|
+
query: z.string().optional().describe('Optional search text (title match).'),
|
|
66
|
+
limit: z.number().int().positive().optional().describe('Max results.'),
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
listAssets: {
|
|
70
|
+
description: 'List or search assets (images, files). Use it to find REAL asset ids — never ' +
|
|
71
|
+
'invent one. Returns { id, title } summaries.',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
query: z.string().optional().describe('Optional search text.'),
|
|
74
|
+
limit: z.number().int().positive().optional().describe('Max results.'),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
getDocument: {
|
|
78
|
+
description: 'Fetch one full document (values + localized values + block ids) by collection and ' +
|
|
79
|
+
'id. Omit both to read the document currently open in the editor. Rich-text field ' +
|
|
80
|
+
'values are returned as markdown strings where faithfully expressible.',
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
collection: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Collection id. Omit to target the currently open document.'),
|
|
86
|
+
id: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Document id (from a searchDocuments result). Omit to target the currently open document.'),
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
createDocument: {
|
|
93
|
+
description: 'Author a NEW document in a collection as a reviewable draft proposal. Terminal ' +
|
|
94
|
+
'step of a creation request — call it once you have gathered the real ids you ' +
|
|
95
|
+
'need. Returns a compact proposal summary.',
|
|
96
|
+
inputSchema: z.object({
|
|
97
|
+
collection: z.string().describe('Target collection id.'),
|
|
98
|
+
instruction: z.string().describe('What to write, refining the user request.'),
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
updateFields: {
|
|
102
|
+
description: 'Change specific fields of an EXISTING document as a reviewable proposal — THE ' +
|
|
103
|
+
'tool for every edit request (change / fix / remove / replace / rewrite wording, ' +
|
|
104
|
+
'values, or a page-builder element). Call getDocument first to see the current ' +
|
|
105
|
+
'values (and block ids), then pass ONLY the fields that change. A block prop ' +
|
|
106
|
+
'(e.g. a Hero headline) is addressed as field + blockId + prop. Omit collection ' +
|
|
107
|
+
'and id to target the document currently open in the editor. A rich-text field ' +
|
|
108
|
+
'takes the FULL new value as a MARKDOWN string (headings, lists, links, bold/italic, ' +
|
|
109
|
+
'blockquotes — no images, code, tables, or HTML).',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
collection: z
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Collection id. Omit to target the currently open document.'),
|
|
115
|
+
id: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Document id. Omit to target the currently open document.'),
|
|
119
|
+
changes: z
|
|
120
|
+
.array(z.object({
|
|
121
|
+
field: z.string().describe('Field name from the collection schema.'),
|
|
122
|
+
locale: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Locale of a LOCALIZED field (defaults to the editor's working locale)."),
|
|
126
|
+
blockId: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('For a blocks field: the id of the block to change (from getDocument).'),
|
|
130
|
+
prop: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe('With blockId: the component prop to change.'),
|
|
134
|
+
value: z
|
|
135
|
+
.unknown()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('The new value (a rich-text field takes a markdown string). null clears the field/prop.'),
|
|
138
|
+
}))
|
|
139
|
+
.min(1)
|
|
140
|
+
.describe('The field changes — only what changes, never the whole document.'),
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
translateDocument: {
|
|
144
|
+
description: 'Fill the EMPTY locale values of an existing document (search for it first to ' +
|
|
145
|
+
'get its id). Only for translation requests — never for edits (use updateFields ' +
|
|
146
|
+
'to change existing content). Returns a compact proposal summary.',
|
|
147
|
+
inputSchema: z.object({
|
|
148
|
+
collection: z.string().describe('Collection id.'),
|
|
149
|
+
id: z.string().describe('Document id (from a searchDocuments result).'),
|
|
150
|
+
locale: z.string().optional().describe('Target locale (defaults to the active one).'),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a tool call's target document from its (optional) input coordinates
|
|
156
|
+
* and the current-document seam (EC-378). Deterministic:
|
|
157
|
+
*
|
|
158
|
+
* - both given → use them verbatim;
|
|
159
|
+
* - both omitted → the currently open document (error when none is open);
|
|
160
|
+
* - `collection` given with `id` omitted → the open document ONLY when the
|
|
161
|
+
* collections match — a named collection must never silently cross onto a
|
|
162
|
+
* different open document.
|
|
163
|
+
*
|
|
164
|
+
* Pure — tested directly; `getDocument` and `updateFields` share it.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveDocumentTarget(input, current) {
|
|
167
|
+
if (input.collection !== undefined && input.id !== undefined) {
|
|
168
|
+
return { ok: true, ref: { collection: input.collection, id: input.id } };
|
|
169
|
+
}
|
|
170
|
+
if (!current) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
errorText: 'No target document — pass collection and id, or ask the user to open the document in the editor.',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (input.collection !== undefined && input.collection !== current.collection) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
errorText: `The open document is in collection "${current.collection}", not "${input.collection}" — pass the document id explicitly.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, ref: { collection: current.collection, id: input.id ?? current.id } };
|
|
183
|
+
}
|
|
184
|
+
export function createChatCandidateStore() {
|
|
185
|
+
const searched = new Map();
|
|
186
|
+
const searchedAssets = new Map();
|
|
9
187
|
return {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
188
|
+
record(collection, results) {
|
|
189
|
+
const byId = searched.get(collection) ?? new Map();
|
|
190
|
+
for (const result of results)
|
|
191
|
+
byId.set(result.id, result);
|
|
192
|
+
searched.set(collection, byId);
|
|
193
|
+
},
|
|
194
|
+
recordAssets(results) {
|
|
195
|
+
for (const result of results)
|
|
196
|
+
searchedAssets.set(result.id, result);
|
|
197
|
+
},
|
|
198
|
+
candidatesFor(collection) {
|
|
199
|
+
const candidates = {};
|
|
200
|
+
for (const field of collection.fields) {
|
|
201
|
+
if (field.type !== 'relation')
|
|
202
|
+
continue;
|
|
203
|
+
const found = searched.get(field.target);
|
|
204
|
+
if (!found || found.size === 0)
|
|
205
|
+
continue;
|
|
206
|
+
candidates[field.name] = [...found.values()].map((r) => ({ id: r.id, ...(r.title ? { label: r.title } : {}) }));
|
|
207
|
+
}
|
|
208
|
+
return candidates;
|
|
209
|
+
},
|
|
210
|
+
assetCandidatesFor(collection) {
|
|
211
|
+
const candidates = {};
|
|
212
|
+
if (searchedAssets.size === 0)
|
|
213
|
+
return candidates;
|
|
214
|
+
const pool = [...searchedAssets.values()].map((a) => ({ id: a.id, ...(a.title ? { label: a.title } : {}) }));
|
|
215
|
+
for (const field of collection.fields) {
|
|
216
|
+
if (field.type !== 'asset')
|
|
217
|
+
continue;
|
|
218
|
+
candidates[field.name] = pool;
|
|
219
|
+
}
|
|
220
|
+
return candidates;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function issueSummary(issues) {
|
|
225
|
+
return issues
|
|
226
|
+
.slice(0, 5)
|
|
227
|
+
.map((issue) => issue.message)
|
|
228
|
+
.join('; ');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The MODEL-facing view of a document (EC-379): rich-text field values are
|
|
232
|
+
* projected to markdown strings — but ONLY when the engine's loss-guarded
|
|
233
|
+
* inverse can express them faithfully (subset-only content, round-trip proven).
|
|
234
|
+
* A document carrying embeds, internal `ref` links, or out-of-subset marks keeps
|
|
235
|
+
* its `{ version, doc }` envelope, so nothing the model later echoes back can
|
|
236
|
+
* silently lose structure. Applies to the `getDocument` TOOL OUTPUT only; the
|
|
237
|
+
* proposal/apply paths always read the raw stored document.
|
|
238
|
+
*/
|
|
239
|
+
export function projectRichTextAsMarkdown(document, collection) {
|
|
240
|
+
const richTextFields = collection.fields
|
|
241
|
+
.filter((field) => field.type === 'richText')
|
|
242
|
+
.map((field) => field.name);
|
|
243
|
+
if (richTextFields.length === 0)
|
|
244
|
+
return document;
|
|
245
|
+
const mapBucket = (bucket) => {
|
|
246
|
+
let next = bucket;
|
|
247
|
+
for (const name of richTextFields) {
|
|
248
|
+
const value = next[name];
|
|
249
|
+
if (value === undefined || value === null)
|
|
250
|
+
continue;
|
|
251
|
+
const parsed = parseRichTextValue(value);
|
|
252
|
+
if (!parsed.ok)
|
|
253
|
+
continue;
|
|
254
|
+
const markdown = richTextValueToMarkdownProse(parsed.value);
|
|
255
|
+
if (markdown === undefined)
|
|
256
|
+
continue; // not faithfully expressible → keep the envelope
|
|
257
|
+
if (next === bucket)
|
|
258
|
+
next = { ...bucket };
|
|
259
|
+
next[name] = markdown;
|
|
260
|
+
}
|
|
261
|
+
return next;
|
|
262
|
+
};
|
|
263
|
+
const values = mapBucket(document.values);
|
|
264
|
+
const localized = document.localized
|
|
265
|
+
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [locale, mapBucket(bucket)]))
|
|
266
|
+
: undefined;
|
|
267
|
+
return { ...document, values, ...(localized ? { localized } : {}) };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build the client executor map. Every entry validates its input against
|
|
271
|
+
* {@link chatToolDefinitions}' zod schema (the route validates UIMessages only
|
|
272
|
+
* structurally — deep input validation is the client's), executes with the
|
|
273
|
+
* user's session, and returns a JSON-safe outcome. EC-378's `updateFields`
|
|
274
|
+
* lands as one more case in this dispatch.
|
|
275
|
+
*/
|
|
276
|
+
export function createChatToolExecutor(options) {
|
|
277
|
+
const searchLimit = options.searchLimit ?? DEFAULT_SEARCH_LIMIT;
|
|
278
|
+
function collectionById(id) {
|
|
279
|
+
return options.collections.find((c) => c.id === id);
|
|
280
|
+
}
|
|
281
|
+
async function searchDocuments(input) {
|
|
282
|
+
if (!collectionById(input.collection)) {
|
|
283
|
+
return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
|
|
284
|
+
}
|
|
285
|
+
const limit = Math.min(input.limit ?? searchLimit, searchLimit);
|
|
286
|
+
const results = (await options.readers.searchDocuments({
|
|
287
|
+
collection: input.collection,
|
|
288
|
+
...(input.query ? { query: input.query } : {}),
|
|
289
|
+
limit,
|
|
290
|
+
})).slice(0, limit);
|
|
291
|
+
options.candidates.record(input.collection, results);
|
|
292
|
+
return { ok: true, output: results };
|
|
293
|
+
}
|
|
294
|
+
async function listAssets(input) {
|
|
295
|
+
const limit = Math.min(input.limit ?? searchLimit, searchLimit);
|
|
296
|
+
const results = (await options.readers.listAssets({
|
|
297
|
+
...(input.query ? { query: input.query } : {}),
|
|
298
|
+
limit,
|
|
299
|
+
})).slice(0, limit);
|
|
300
|
+
// EC-381 — leash asset fields to what the model actually searched, exactly as
|
|
301
|
+
// searchDocuments feeds the relation store.
|
|
302
|
+
options.candidates.recordAssets(results);
|
|
303
|
+
return { ok: true, output: results };
|
|
304
|
+
}
|
|
305
|
+
async function getDocument(input) {
|
|
306
|
+
if (!options.readers.getDocument) {
|
|
307
|
+
return { ok: false, errorText: 'Reading documents is not available here.' };
|
|
308
|
+
}
|
|
309
|
+
const target = resolveDocumentTarget(input, options.currentDocument?.());
|
|
310
|
+
if (!target.ok)
|
|
311
|
+
return { ok: false, errorText: target.errorText };
|
|
312
|
+
const document = await options.readers.getDocument(target.ref);
|
|
313
|
+
if (!document) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
errorText: `Document "${target.ref.collection}/${target.ref.id}" was not found.`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// EC-379: the model reads (and re-authors) rich text as markdown.
|
|
320
|
+
const collection = collectionById(target.ref.collection);
|
|
321
|
+
return { ok: true, output: collection ? projectRichTextAsMarkdown(document, collection) : document };
|
|
322
|
+
}
|
|
323
|
+
/** EC-378 — the edit-flow terminal tool: current doc + model-authored changes → validated proposal. */
|
|
324
|
+
async function updateFields(toolCallId, input) {
|
|
325
|
+
if (!options.readers.getDocument) {
|
|
326
|
+
return { ok: false, errorText: 'Reading documents is not available here.' };
|
|
327
|
+
}
|
|
328
|
+
const target = resolveDocumentTarget(input, options.currentDocument?.());
|
|
329
|
+
if (!target.ok)
|
|
330
|
+
return { ok: false, errorText: target.errorText };
|
|
331
|
+
const collection = collectionById(target.ref.collection);
|
|
332
|
+
if (!collection) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
errorText: `Unknown collection "${target.ref.collection}" — use a collection id from the catalog.`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const document = await options.readers.getDocument(target.ref);
|
|
339
|
+
if (!document) {
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
errorText: `Document "${target.ref.collection}/${target.ref.id}" was not found.`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const result = buildUpdateProposal({
|
|
346
|
+
registry: options.registry,
|
|
347
|
+
collection,
|
|
348
|
+
document,
|
|
349
|
+
changes: input.changes,
|
|
350
|
+
candidates: options.candidates.candidatesFor(collection),
|
|
351
|
+
assetCandidates: options.candidates.assetCandidatesFor(collection),
|
|
352
|
+
...(options.locale ? { locale: options.locale } : {}),
|
|
353
|
+
...(options.localeConfig ? { localeConfig: options.localeConfig } : {}),
|
|
354
|
+
...(options.validateRichTextEmbeds
|
|
355
|
+
? { validateRichTextEmbeds: options.validateRichTextEmbeds }
|
|
356
|
+
: {}),
|
|
357
|
+
...(options.validateComposition ? { validateComposition: options.validateComposition } : {}),
|
|
358
|
+
...(options.store ? { store: options.store } : {}),
|
|
359
|
+
});
|
|
360
|
+
if (!result.ok) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
errorText: `The proposed changes did not pass validation: ${issueSummary(result.issues)}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (result.kind === 'no-change') {
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
output: {
|
|
370
|
+
status: 'no-change',
|
|
371
|
+
note: 'The requested change matches the current values — there is nothing to update.',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
options.onProposal(toolCallId, {
|
|
376
|
+
origin: 'update',
|
|
377
|
+
collection: collection.id,
|
|
378
|
+
documentId: document.id,
|
|
379
|
+
patch: result.patch,
|
|
380
|
+
provenance: aiProvenance(options.provider),
|
|
381
|
+
attempts: 1,
|
|
382
|
+
issues: result.issues,
|
|
383
|
+
before: result.before,
|
|
384
|
+
});
|
|
385
|
+
// Compact summary only — the full patch goes to the review UI, not the model.
|
|
386
|
+
const titleValues = {
|
|
387
|
+
...result.patch.values,
|
|
388
|
+
...(options.locale ? result.patch.localized?.[options.locale] : {}),
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
ok: true,
|
|
392
|
+
output: {
|
|
393
|
+
status: 'proposed',
|
|
394
|
+
collection: collection.id,
|
|
395
|
+
documentId: document.id,
|
|
396
|
+
title: resolveDocumentTitle(collection, titleValues, document.id),
|
|
397
|
+
changedFields: result.changedFields,
|
|
398
|
+
note: 'Update proposal created; the editor reviews and applies it in the studio. Summarize the change briefly — do not repeat the content.',
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function createDocument(toolCallId, input) {
|
|
403
|
+
const collection = collectionById(input.collection);
|
|
404
|
+
if (!collection) {
|
|
405
|
+
return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
|
|
406
|
+
}
|
|
407
|
+
const documentId = options.mintDocumentId(collection.id);
|
|
408
|
+
const knowledge = options.knowledge?.();
|
|
409
|
+
const result = await applyContentWithShape({
|
|
410
|
+
provider: options.provider,
|
|
411
|
+
registry: options.registry,
|
|
412
|
+
collection,
|
|
413
|
+
documentId,
|
|
414
|
+
instruction: input.instruction,
|
|
415
|
+
...(knowledge ? { knowledge } : {}),
|
|
416
|
+
...(options.components ? { components: options.components } : {}),
|
|
417
|
+
candidates: options.candidates.candidatesFor(collection),
|
|
418
|
+
// EC-381 — asset/image fields are leashed to searched library assets.
|
|
419
|
+
assetCandidates: options.candidates.assetCandidatesFor(collection),
|
|
420
|
+
...(options.locale ? { locale: options.locale } : {}),
|
|
421
|
+
...(options.validateRichTextEmbeds
|
|
422
|
+
? { validateRichTextEmbeds: options.validateRichTextEmbeds }
|
|
423
|
+
: {}),
|
|
424
|
+
...(options.validateComposition ? { validateComposition: options.validateComposition } : {}),
|
|
425
|
+
...(options.store ? { store: options.store } : {}),
|
|
426
|
+
...(options.maxRepairAttempts !== undefined
|
|
427
|
+
? { maxRepairAttempts: options.maxRepairAttempts }
|
|
428
|
+
: {}),
|
|
429
|
+
});
|
|
430
|
+
if (!result.ok) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
errorText: `The drafted ${collection.id} did not pass validation: ${issueSummary(result.issues)}`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
options.onProposal(toolCallId, {
|
|
437
|
+
origin: 'create',
|
|
438
|
+
collection: collection.id,
|
|
439
|
+
documentId,
|
|
440
|
+
patch: result.patch,
|
|
441
|
+
provenance: result.provenance,
|
|
442
|
+
attempts: result.attempts,
|
|
443
|
+
issues: result.issues,
|
|
444
|
+
});
|
|
445
|
+
// Compact summary only — the full patch goes to the review UI, not the model.
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
output: {
|
|
449
|
+
status: 'proposed',
|
|
450
|
+
collection: collection.id,
|
|
451
|
+
documentId,
|
|
452
|
+
title: resolveDocumentTitle(collection, result.patch.values, documentId),
|
|
453
|
+
note: 'Draft proposal created; the editor reviews and applies it in the studio. Summarize briefly — do not repeat the content.',
|
|
20
454
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function translate(toolCallId, input) {
|
|
458
|
+
const collection = collectionById(input.collection);
|
|
459
|
+
if (!collection) {
|
|
460
|
+
return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
|
|
461
|
+
}
|
|
462
|
+
if (!options.readers.getDocument || !options.localeConfig) {
|
|
463
|
+
return { ok: false, errorText: 'Translation is not available here.' };
|
|
464
|
+
}
|
|
465
|
+
const document = await options.readers.getDocument({
|
|
466
|
+
collection: input.collection,
|
|
467
|
+
id: input.id,
|
|
468
|
+
});
|
|
469
|
+
if (!document) {
|
|
470
|
+
return { ok: false, errorText: `Document "${input.collection}/${input.id}" was not found.` };
|
|
471
|
+
}
|
|
472
|
+
const knowledge = options.knowledge?.();
|
|
473
|
+
const result = await translateDocument({
|
|
474
|
+
provider: options.provider,
|
|
475
|
+
collection,
|
|
476
|
+
document,
|
|
477
|
+
localeConfig: options.localeConfig,
|
|
478
|
+
...(input.locale ? { targetLocales: [input.locale] } : {}),
|
|
479
|
+
...(knowledge ? { knowledge } : {}),
|
|
480
|
+
});
|
|
481
|
+
if (result.total === 0) {
|
|
482
|
+
return {
|
|
483
|
+
ok: true,
|
|
484
|
+
output: {
|
|
485
|
+
status: 'nothing-to-translate',
|
|
486
|
+
note: 'Every locale value is already filled — there is nothing to translate.',
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (result.translated === 0) {
|
|
491
|
+
return { ok: false, errorText: 'The translation did not produce any content.' };
|
|
492
|
+
}
|
|
493
|
+
options.onProposal(toolCallId, {
|
|
494
|
+
origin: 'translate',
|
|
495
|
+
collection: collection.id,
|
|
496
|
+
documentId: document.id,
|
|
497
|
+
patch: {
|
|
498
|
+
collection: collection.id,
|
|
499
|
+
id: document.id,
|
|
500
|
+
values: result.document.values,
|
|
501
|
+
...(result.document.localized ? { localized: result.document.localized } : {}),
|
|
25
502
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
503
|
+
provenance: result.provenance,
|
|
504
|
+
attempts: 1,
|
|
505
|
+
issues: [],
|
|
506
|
+
before: {
|
|
507
|
+
values: document.values,
|
|
508
|
+
...(document.localized ? { localized: document.localized } : {}),
|
|
29
509
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
output: {
|
|
514
|
+
status: 'proposed',
|
|
515
|
+
collection: collection.id,
|
|
516
|
+
documentId: document.id,
|
|
517
|
+
translatedUnits: result.translated,
|
|
518
|
+
note: 'Translation proposal created; the editor reviews and applies it in the studio.',
|
|
33
519
|
},
|
|
34
|
-
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
async execute(call) {
|
|
524
|
+
const definition = chatToolDefinitions[call.toolName];
|
|
525
|
+
if (!definition) {
|
|
526
|
+
return { ok: false, errorText: `Unknown tool "${call.toolName}".` };
|
|
527
|
+
}
|
|
528
|
+
const parsed = definition.inputSchema.safeParse(call.input);
|
|
529
|
+
if (!parsed.success) {
|
|
530
|
+
return { ok: false, errorText: `Invalid ${call.toolName} input: ${parsed.error.message}` };
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
switch (call.toolName) {
|
|
534
|
+
case 'searchDocuments':
|
|
535
|
+
return await searchDocuments(parsed.data);
|
|
536
|
+
case 'listAssets':
|
|
537
|
+
return await listAssets(parsed.data);
|
|
538
|
+
case 'getDocument':
|
|
539
|
+
return await getDocument(parsed.data);
|
|
540
|
+
case 'createDocument':
|
|
541
|
+
return await createDocument(call.toolCallId, parsed.data);
|
|
542
|
+
case 'updateFields':
|
|
543
|
+
return await updateFields(call.toolCallId, parsed.data);
|
|
544
|
+
case 'translateDocument':
|
|
545
|
+
return await translate(call.toolCallId, parsed.data);
|
|
546
|
+
default:
|
|
547
|
+
return { ok: false, errorText: `Unknown tool "${call.toolName}".` };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
errorText: `The ${call.toolName} step failed: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
35
556
|
},
|
|
36
|
-
required: ['action'],
|
|
37
|
-
additionalProperties: false,
|
|
38
557
|
};
|
|
39
558
|
}
|
|
40
|
-
/** Coerce a raw structured-output object into a {@link PlannerAction}; `null` if unusable. */
|
|
41
|
-
export function parsePlannerAction(raw) {
|
|
42
|
-
if (typeof raw !== 'object' || raw === null)
|
|
43
|
-
return null;
|
|
44
|
-
const value = raw;
|
|
45
|
-
const action = value.action;
|
|
46
|
-
const str = (k) => typeof value[k] === 'string' && value[k].length > 0 ? value[k] : undefined;
|
|
47
|
-
switch (action) {
|
|
48
|
-
case 'searchDocuments': {
|
|
49
|
-
const collection = str('collection');
|
|
50
|
-
if (!collection)
|
|
51
|
-
return null;
|
|
52
|
-
return { action, collection, ...(str('query') ? { query: str('query') } : {}) };
|
|
53
|
-
}
|
|
54
|
-
case 'listAssets':
|
|
55
|
-
return { action, ...(str('query') ? { query: str('query') } : {}) };
|
|
56
|
-
case 'createDocument': {
|
|
57
|
-
const collection = str('collection');
|
|
58
|
-
if (!collection)
|
|
59
|
-
return null;
|
|
60
|
-
return { action, collection, ...(str('instruction') ? { instruction: str('instruction') } : {}) };
|
|
61
|
-
}
|
|
62
|
-
case 'translateDocument': {
|
|
63
|
-
const collection = str('collection');
|
|
64
|
-
if (!collection)
|
|
65
|
-
return null;
|
|
66
|
-
return { action, collection, ...(str('query') ? { query: str('query') } : {}) };
|
|
67
|
-
}
|
|
68
|
-
case 'respond':
|
|
69
|
-
return { action, message: str('message') ?? '' };
|
|
70
|
-
default:
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
559
|
//# sourceMappingURL=tools.js.map
|