@elytracms/studio 0.0.56 → 0.0.57

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.
Files changed (55) hide show
  1. package/convex/auth.ts +7 -0
  2. package/convex/users.ts +41 -14
  3. package/dist/components/ai/ai-chat-panel-lazy.js +3 -3
  4. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  5. package/dist/components/ai/ai-chat-panel.d.ts +0 -9
  6. package/dist/components/ai/ai-chat-panel.js +161 -60
  7. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  8. package/dist/components/ai/chat-ui.d.ts +62 -10
  9. package/dist/components/ai/chat-ui.js +130 -23
  10. package/dist/components/ai/chat-ui.js.map +1 -1
  11. package/dist/lib/ai/anthropic-provider.d.ts +0 -12
  12. package/dist/lib/ai/anthropic-provider.js +3 -11
  13. package/dist/lib/ai/anthropic-provider.js.map +1 -1
  14. package/dist/lib/ai/chat-context.d.ts +58 -0
  15. package/dist/lib/ai/chat-context.js +71 -0
  16. package/dist/lib/ai/chat-context.js.map +1 -0
  17. package/dist/lib/ai/chat-loop.d.ts +70 -0
  18. package/dist/lib/ai/chat-loop.js +46 -0
  19. package/dist/lib/ai/chat-loop.js.map +1 -0
  20. package/dist/lib/ai/chat-state.d.ts +70 -57
  21. package/dist/lib/ai/chat-state.js +49 -90
  22. package/dist/lib/ai/chat-state.js.map +1 -1
  23. package/dist/lib/ai/fake-provider.d.ts +84 -6
  24. package/dist/lib/ai/fake-provider.js +193 -8
  25. package/dist/lib/ai/fake-provider.js.map +1 -1
  26. package/dist/lib/ai/gateway-provider.d.ts +30 -0
  27. package/dist/lib/ai/gateway-provider.js +73 -0
  28. package/dist/lib/ai/gateway-provider.js.map +1 -0
  29. package/dist/lib/ai/index.d.ts +3 -1
  30. package/dist/lib/ai/index.js +3 -1
  31. package/dist/lib/ai/index.js.map +1 -1
  32. package/dist/lib/ai/node-handler.d.ts +13 -4
  33. package/dist/lib/ai/node-handler.js +33 -3
  34. package/dist/lib/ai/node-handler.js.map +1 -1
  35. package/dist/lib/ai/route-provider.d.ts +35 -10
  36. package/dist/lib/ai/route-provider.js +13 -6
  37. package/dist/lib/ai/route-provider.js.map +1 -1
  38. package/dist/lib/ai/route.d.ts +50 -21
  39. package/dist/lib/ai/route.js +69 -40
  40. package/dist/lib/ai/route.js.map +1 -1
  41. package/dist/lib/ai/tools.d.ts +169 -39
  42. package/dist/lib/ai/tools.js +308 -62
  43. package/dist/lib/ai/tools.js.map +1 -1
  44. package/dist/lib/ai/types.d.ts +11 -16
  45. package/dist/lib/ai/types.js.map +1 -1
  46. package/dist/lib/auth/convex-auth-adapter.d.ts +9 -1
  47. package/dist/lib/auth/convex-auth-adapter.js +28 -2
  48. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  49. package/dist/lib/i18n/messages.en.js +9 -0
  50. package/dist/lib/i18n/messages.en.js.map +1 -1
  51. package/dist/ui/badge.d.ts +1 -1
  52. package/package.json +4 -3
  53. package/dist/lib/ai/create-flow.d.ts +0 -132
  54. package/dist/lib/ai/create-flow.js +0 -280
  55. package/dist/lib/ai/create-flow.js.map +0 -1
@@ -1,16 +1,35 @@
1
1
  /**
2
- * The narrow tool set the creation flow's model works against (EC-199).
2
+ * The narrow tool set the assistant works against (EC-199, reshaped by EC-334).
3
3
  *
4
4
  * Per the ratified AD, tools execute CLIENT-side against the operations layer with
5
- * the user's own session — this module only declares their SHAPES (the model-facing
6
- * planner schema) and the reader interface the studio binds to operations. The engine
7
- * never gains a write path: `searchDocuments` / `listAssets` are reads that gather the
8
- * real ids a proposal may reference (a relation id is otherwise structurally
9
- * impossible — see `buildTargetShape`), and `createDocument` is the terminal action
10
- * that runs `applyContentWithShape` and yields a reviewable proposal. `updateFields`
11
- * and `translate` are deliberately NOT here — they are later slices (EC-332 / post-v1).
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
+ * through the route's kept `generate` branch and return a COMPACT proposal
19
+ * summary as the tool output — the full proposal goes to the review UI via
20
+ * `onProposal(toolCallId, …)`, never to the model.
21
+ *
22
+ * EC-378 adds `updateFields` as one more definition + one more executor case —
23
+ * the map's shape leaves that slot open.
24
+ *
25
+ * The EC-199 planner enum/schema (`plannerSchema` / `parsePlannerAction`) retired
26
+ * with the hand-rolled orchestrator (EC-334 slice 2).
12
27
  */
13
- import type { CmsDocument, JsonSchema } from '@elytracms/core/cms-core';
28
+ import { z } from 'zod';
29
+ import type { CmsDocument, CollectionDef, CollectionRegistry, LocaleConfig, ValidateDocumentOptions } from '@elytracms/core/cms-core';
30
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
31
+ import type { ChatProposal } from './chat-state.js';
32
+ import type { AiProvider, RelationCandidates, ResolvedKnowledge } from './types.js';
14
33
  /** A document the model may reference as a relation target (from a `searchDocuments` read). */
15
34
  export interface DocumentSearchResult {
16
35
  id: string;
@@ -22,11 +41,11 @@ export interface AssetSearchResult {
22
41
  title?: string;
23
42
  }
24
43
  /**
25
- * The client-side, operations-backed readers the orchestrator calls. The studio binds
44
+ * The client-side, operations-backed readers the executor calls. The studio binds
26
45
  * these to the user's authenticated operations client; tests bind an in-memory fake.
27
46
  * Reads only — there is no write tool (apply is a separate, human-gated draft write).
28
47
  */
29
- export interface CreateFlowTools {
48
+ export interface ChatToolReaders {
30
49
  searchDocuments(input: {
31
50
  collection: string;
32
51
  query?: string;
@@ -37,41 +56,152 @@ export interface CreateFlowTools {
37
56
  limit?: number;
38
57
  }): Promise<AssetSearchResult[]>;
39
58
  /**
40
- * Fetch a full document (values + localized) for the translate flow (EC-332). Optional:
41
- * the creation flow never needs it, so a studio that only wires create can omit it.
59
+ * Fetch a full document (values + localized) — the translate flow and the model's
60
+ * `getDocument` tool. Optional: a studio that only wires create can omit it.
42
61
  */
43
62
  getDocument?(input: {
44
63
  collection: string;
45
64
  id: string;
46
65
  }): Promise<CmsDocument | null>;
47
66
  }
48
- /** One planned step the model returns — the discriminated tool/action union. */
49
- export type PlannerAction = {
50
- action: 'searchDocuments';
51
- collection: string;
52
- query?: string;
53
- } | {
54
- action: 'listAssets';
55
- query?: string;
56
- } | {
57
- action: 'createDocument';
58
- collection: string;
59
- instruction?: string;
60
- } | {
61
- action: 'translateDocument';
62
- collection: string;
63
- query?: string;
67
+ /** Per-search result cap (protects the wire — results become model context). */
68
+ export declare const DEFAULT_SEARCH_LIMIT = 20;
69
+ /**
70
+ * One model-facing tool definition for the route's `streamText` call. Structurally
71
+ * the SDK's `FunctionTool` WITHOUT `execute` (execution is client-side, always) —
72
+ * typed locally so this module needs no SDK import.
73
+ */
74
+ export interface ChatToolDefinition {
75
+ description: string;
76
+ /** Zod input schema (the AI SDK accepts zod schemas as `inputSchema` directly). */
77
+ inputSchema: z.ZodType;
78
+ }
79
+ /**
80
+ * The tool set the native chat branch declares (EC-334). No `execute` on any entry:
81
+ * the SDK forwards each call to the client, which executes reads against the
82
+ * operations layer with the user's session and posts the result back; the terminal
83
+ * tools (`createDocument` / `translateDocument`) run the deterministic engines
84
+ * client-side and return a compact proposal summary as the tool output. The route
85
+ * itself never executes anything.
86
+ *
87
+ * Collections are NOT enum-leashed here — the route has zero data access, so it has
88
+ * no catalog; the catalog reaches the model through the request `context` (system
89
+ * prompt), and every id a proposal references must come from a tool RESULT
90
+ * (client-validated; `applyContentWithShape` still enum-leashes relation ids).
91
+ */
92
+ export declare const chatToolDefinitions: {
93
+ searchDocuments: {
94
+ description: string;
95
+ inputSchema: z.ZodObject<{
96
+ collection: z.ZodString;
97
+ query: z.ZodOptional<z.ZodString>;
98
+ limit: z.ZodOptional<z.ZodNumber>;
99
+ }, z.core.$strip>;
100
+ };
101
+ listAssets: {
102
+ description: string;
103
+ inputSchema: z.ZodObject<{
104
+ query: z.ZodOptional<z.ZodString>;
105
+ limit: z.ZodOptional<z.ZodNumber>;
106
+ }, z.core.$strip>;
107
+ };
108
+ getDocument: {
109
+ description: string;
110
+ inputSchema: z.ZodObject<{
111
+ collection: z.ZodString;
112
+ id: z.ZodString;
113
+ }, z.core.$strip>;
114
+ };
115
+ createDocument: {
116
+ description: string;
117
+ inputSchema: z.ZodObject<{
118
+ collection: z.ZodString;
119
+ instruction: z.ZodString;
120
+ }, z.core.$strip>;
121
+ };
122
+ translateDocument: {
123
+ description: string;
124
+ inputSchema: z.ZodObject<{
125
+ collection: z.ZodString;
126
+ id: z.ZodString;
127
+ locale: z.ZodOptional<z.ZodString>;
128
+ }, z.core.$strip>;
129
+ };
130
+ };
131
+ /** One forwarded tool call, as `useChat`'s `onToolCall` receives it. */
132
+ export interface ChatToolCall {
133
+ toolCallId: string;
134
+ toolName: string;
135
+ input: unknown;
136
+ }
137
+ /**
138
+ * A tool execution outcome: an `output` posted back as the tool result, or an
139
+ * `errorText` posted as an `output-error` part (the model sees the failure and
140
+ * can react — never a thrown exception into the stream).
141
+ */
142
+ export type ChatToolOutcome = {
143
+ ok: true;
144
+ output: unknown;
64
145
  } | {
65
- action: 'respond';
66
- message: string;
146
+ ok: false;
147
+ errorText: string;
67
148
  };
68
- /** The schema name the planner call carries — how a FakeProvider tells plan vs. author. */
69
- export declare const PLANNER_SCHEMA_NAME = "create_flow_action";
70
149
  /**
71
- * The structured-output schema for ONE planning step. `action` is a closed enum and
72
- * `collection` is leashed to the known collection ids, so the model can neither invent
73
- * an action nor target a collection that does not exist.
150
+ * The per-conversation relation-candidate store: every `searchDocuments` result
151
+ * is recorded per collection, and `createDocument` may only reference the ids
152
+ * gathered here (the `applyContentWithShape` enum leash makes a hallucinated id
153
+ * structurally unrepresentable). Held by the panel per conversation — a new
154
+ * conversation (EC-380 clear) starts an empty store.
155
+ */
156
+ export interface ChatCandidateStore {
157
+ record(collection: string, results: readonly DocumentSearchResult[]): void;
158
+ /** Candidates for a target collection's relation fields, keyed by field name. */
159
+ candidatesFor(collection: CollectionDef): RelationCandidates;
160
+ }
161
+ export declare function createChatCandidateStore(): ChatCandidateStore;
162
+ export interface ChatToolExecutorOptions {
163
+ /** The (route) provider the terminal engines proxy their model calls through. */
164
+ provider: AiProvider;
165
+ registry: CollectionRegistry;
166
+ /** The collections the assistant may target (unknown ids fail loud). */
167
+ collections: readonly CollectionDef[];
168
+ /** Operations-backed readers (client-side, user's session). */
169
+ readers: ChatToolReaders;
170
+ /** Mint a fresh document id for a new draft (studio: the CMS store's minter). */
171
+ mintDocumentId: (collectionId: string) => string;
172
+ /** Per-conversation relation-candidate store (see {@link createChatCandidateStore}). */
173
+ candidates: ChatCandidateStore;
174
+ /**
175
+ * Full proposal sink, keyed by `toolCallId` — the review UI's entry point.
176
+ * The tool OUTPUT stays a compact summary; the patch never reaches the model.
177
+ */
178
+ onProposal: (toolCallId: string, proposal: ChatProposal) => void;
179
+ /** Current knowledge snapshot (thunk — the body thunk refreshes it per request). */
180
+ knowledge?: () => ResolvedKnowledge | undefined;
181
+ /** Component manifests — context for `allow`-leashed richText/blocks fields. */
182
+ components?: readonly ComponentManifest[];
183
+ /** Target locale for localized fields. */
184
+ locale?: string;
185
+ /** Locale config — required for `translateDocument`. */
186
+ localeConfig?: LocaleConfig;
187
+ /** Injected leashes / store for `applyContentWithShape`'s validation. */
188
+ validateRichTextEmbeds?: ValidateDocumentOptions['validateRichTextEmbeds'];
189
+ validateComposition?: ValidateDocumentOptions['validateComposition'];
190
+ store?: ValidateDocumentOptions['store'];
191
+ /** Max repair attempts inside `applyContentWithShape`. */
192
+ maxRepairAttempts?: number;
193
+ /** Per-search result cap (default {@link DEFAULT_SEARCH_LIMIT}). */
194
+ searchLimit?: number;
195
+ }
196
+ /** The executor `useChat`'s `onToolCall` dispatches into. */
197
+ export interface ChatToolExecutor {
198
+ execute(call: ChatToolCall): Promise<ChatToolOutcome>;
199
+ }
200
+ /**
201
+ * Build the client executor map. Every entry validates its input against
202
+ * {@link chatToolDefinitions}' zod schema (the route validates UIMessages only
203
+ * structurally — deep input validation is the client's), executes with the
204
+ * user's session, and returns a JSON-safe outcome. EC-378's `updateFields`
205
+ * lands as one more case in this dispatch.
74
206
  */
75
- export declare function plannerSchema(collectionIds: readonly string[]): JsonSchema;
76
- /** Coerce a raw structured-output object into a {@link PlannerAction}; `null` if unusable. */
77
- export declare function parsePlannerAction(raw: unknown): PlannerAction | null;
207
+ export declare function createChatToolExecutor(options: ChatToolExecutorOptions): ChatToolExecutor;
@@ -1,74 +1,320 @@
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 structured-output schema for ONE planning step. `action` is a closed enum and
5
- * `collection` is leashed to the known collection ids, so the model can neither invent
6
- * an action nor target a collection that does not exist.
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
+ * through the route's kept `generate` branch and return a COMPACT proposal
19
+ * summary as the tool output — the full proposal goes to the review UI via
20
+ * `onProposal(toolCallId, …)`, never to the model.
21
+ *
22
+ * EC-378 adds `updateFields` as one more definition + one more executor case —
23
+ * the map's shape leaves that slot open.
24
+ *
25
+ * The EC-199 planner enum/schema (`plannerSchema` / `parsePlannerAction`) retired
26
+ * with the hand-rolled orchestrator (EC-334 slice 2).
7
27
  */
8
- export function plannerSchema(collectionIds) {
28
+ import { z } from 'zod';
29
+ import { resolveDocumentTitle } from '@elytracms/core/cms-core';
30
+ import { applyContentWithShape } from './apply-content.js';
31
+ import { translateDocument } from './translate-document.js';
32
+ /** Per-search result cap (protects the wire — results become model context). */
33
+ export const DEFAULT_SEARCH_LIMIT = 20;
34
+ /**
35
+ * The tool set the native chat branch declares (EC-334). No `execute` on any entry:
36
+ * the SDK forwards each call to the client, which executes reads against the
37
+ * operations layer with the user's session and posts the result back; the terminal
38
+ * tools (`createDocument` / `translateDocument`) run the deterministic engines
39
+ * client-side and return a compact proposal summary as the tool output. The route
40
+ * itself never executes anything.
41
+ *
42
+ * Collections are NOT enum-leashed here — the route has zero data access, so it has
43
+ * no catalog; the catalog reaches the model through the request `context` (system
44
+ * prompt), and every id a proposal references must come from a tool RESULT
45
+ * (client-validated; `applyContentWithShape` still enum-leashes relation ids).
46
+ */
47
+ export const chatToolDefinitions = {
48
+ searchDocuments: {
49
+ description: 'Search documents in a collection. Use it to find the REAL ids a relation may ' +
50
+ 'reference, or to locate an existing document — never invent an id. Returns ' +
51
+ '{ id, title } summaries.',
52
+ inputSchema: z.object({
53
+ collection: z
54
+ .string()
55
+ .describe('Target collection id (from the catalog in the system context).'),
56
+ query: z.string().optional().describe('Optional search text (title match).'),
57
+ limit: z.number().int().positive().optional().describe('Max results.'),
58
+ }),
59
+ },
60
+ listAssets: {
61
+ description: 'List or search assets (images, files). Use it to find REAL asset ids — never ' +
62
+ 'invent one. Returns { id, title } summaries.',
63
+ inputSchema: z.object({
64
+ query: z.string().optional().describe('Optional search text.'),
65
+ limit: z.number().int().positive().optional().describe('Max results.'),
66
+ }),
67
+ },
68
+ getDocument: {
69
+ description: 'Fetch one full document (values + localized values) by collection and id.',
70
+ inputSchema: z.object({
71
+ collection: z.string().describe('Collection id.'),
72
+ id: z.string().describe('Document id (from a searchDocuments result).'),
73
+ }),
74
+ },
75
+ createDocument: {
76
+ description: 'Author a NEW document in a collection as a reviewable draft proposal. Terminal ' +
77
+ 'step of a creation request — call it once you have gathered the real ids you ' +
78
+ 'need. Returns a compact proposal summary.',
79
+ inputSchema: z.object({
80
+ collection: z.string().describe('Target collection id.'),
81
+ instruction: z.string().describe('What to write, refining the user request.'),
82
+ }),
83
+ },
84
+ translateDocument: {
85
+ description: 'Fill the empty locale values of an EXISTING document (search for it first to ' +
86
+ 'get its id). Returns a compact proposal summary.',
87
+ inputSchema: z.object({
88
+ collection: z.string().describe('Collection id.'),
89
+ id: z.string().describe('Document id (from a searchDocuments result).'),
90
+ locale: z.string().optional().describe('Target locale (defaults to the active one).'),
91
+ }),
92
+ },
93
+ };
94
+ export function createChatCandidateStore() {
95
+ const searched = new Map();
9
96
  return {
10
- type: 'object',
11
- properties: {
12
- action: {
13
- type: 'string',
14
- enum: ['searchDocuments', 'listAssets', 'createDocument', 'translateDocument', 'respond'],
15
- description: 'searchDocuments: find real ids for a relation target before creating. ' +
16
- 'listAssets: find real asset ids. createDocument: author the document once ' +
17
- 'you have what you need. translateDocument: fill the empty locale values of an ' +
18
- 'existing document (search for it first to get its id). ' +
19
- 'respond: reply without creating anything.',
20
- },
21
- collection: {
22
- type: 'string',
23
- ...(collectionIds.length > 0 ? { enum: [...collectionIds] } : {}),
24
- description: 'Target collection id (for searchDocuments / createDocument / translateDocument).',
25
- },
26
- query: {
27
- type: 'string',
28
- description: 'Optional search text (searchDocuments / listAssets / translateDocument).',
29
- },
30
- instruction: {
31
- type: 'string',
32
- description: 'What to write, refining the user request (createDocument).',
33
- },
34
- message: { type: 'string', description: 'Assistant reply text (respond).' },
97
+ record(collection, results) {
98
+ const byId = searched.get(collection) ?? new Map();
99
+ for (const result of results)
100
+ byId.set(result.id, result);
101
+ searched.set(collection, byId);
102
+ },
103
+ candidatesFor(collection) {
104
+ const candidates = {};
105
+ for (const field of collection.fields) {
106
+ if (field.type !== 'relation')
107
+ continue;
108
+ const found = searched.get(field.target);
109
+ if (!found || found.size === 0)
110
+ continue;
111
+ candidates[field.name] = [...found.values()].map((r) => ({ id: r.id, ...(r.title ? { label: r.title } : {}) }));
112
+ }
113
+ return candidates;
35
114
  },
36
- required: ['action'],
37
- additionalProperties: false,
38
115
  };
39
116
  }
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') } : {}) };
117
+ function issueSummary(issues) {
118
+ return issues
119
+ .slice(0, 5)
120
+ .map((issue) => issue.message)
121
+ .join('; ');
122
+ }
123
+ /**
124
+ * Build the client executor map. Every entry validates its input against
125
+ * {@link chatToolDefinitions}' zod schema (the route validates UIMessages only
126
+ * structurally — deep input validation is the client's), executes with the
127
+ * user's session, and returns a JSON-safe outcome. EC-378's `updateFields`
128
+ * lands as one more case in this dispatch.
129
+ */
130
+ export function createChatToolExecutor(options) {
131
+ const searchLimit = options.searchLimit ?? DEFAULT_SEARCH_LIMIT;
132
+ function collectionById(id) {
133
+ return options.collections.find((c) => c.id === id);
134
+ }
135
+ async function searchDocuments(input) {
136
+ if (!collectionById(input.collection)) {
137
+ return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
53
138
  }
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') } : {}) };
139
+ const limit = Math.min(input.limit ?? searchLimit, searchLimit);
140
+ const results = (await options.readers.searchDocuments({
141
+ collection: input.collection,
142
+ ...(input.query ? { query: input.query } : {}),
143
+ limit,
144
+ })).slice(0, limit);
145
+ options.candidates.record(input.collection, results);
146
+ return { ok: true, output: results };
147
+ }
148
+ async function listAssets(input) {
149
+ const limit = Math.min(input.limit ?? searchLimit, searchLimit);
150
+ const results = (await options.readers.listAssets({
151
+ ...(input.query ? { query: input.query } : {}),
152
+ limit,
153
+ })).slice(0, limit);
154
+ return { ok: true, output: results };
155
+ }
156
+ async function getDocument(input) {
157
+ if (!options.readers.getDocument) {
158
+ return { ok: false, errorText: 'Reading documents is not available here.' };
61
159
  }
62
- case 'translateDocument': {
63
- const collection = str('collection');
64
- if (!collection)
65
- return null;
66
- return { action, collection, ...(str('query') ? { query: str('query') } : {}) };
160
+ const document = await options.readers.getDocument(input);
161
+ if (!document) {
162
+ return { ok: false, errorText: `Document "${input.collection}/${input.id}" was not found.` };
67
163
  }
68
- case 'respond':
69
- return { action, message: str('message') ?? '' };
70
- default:
71
- return null;
164
+ return { ok: true, output: document };
72
165
  }
166
+ async function createDocument(toolCallId, input) {
167
+ const collection = collectionById(input.collection);
168
+ if (!collection) {
169
+ return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
170
+ }
171
+ const documentId = options.mintDocumentId(collection.id);
172
+ const knowledge = options.knowledge?.();
173
+ const result = await applyContentWithShape({
174
+ provider: options.provider,
175
+ registry: options.registry,
176
+ collection,
177
+ documentId,
178
+ instruction: input.instruction,
179
+ ...(knowledge ? { knowledge } : {}),
180
+ ...(options.components ? { components: options.components } : {}),
181
+ candidates: options.candidates.candidatesFor(collection),
182
+ ...(options.locale ? { locale: options.locale } : {}),
183
+ ...(options.validateRichTextEmbeds
184
+ ? { validateRichTextEmbeds: options.validateRichTextEmbeds }
185
+ : {}),
186
+ ...(options.validateComposition ? { validateComposition: options.validateComposition } : {}),
187
+ ...(options.store ? { store: options.store } : {}),
188
+ ...(options.maxRepairAttempts !== undefined
189
+ ? { maxRepairAttempts: options.maxRepairAttempts }
190
+ : {}),
191
+ });
192
+ if (!result.ok) {
193
+ return {
194
+ ok: false,
195
+ errorText: `The drafted ${collection.id} did not pass validation: ${issueSummary(result.issues)}`,
196
+ };
197
+ }
198
+ options.onProposal(toolCallId, {
199
+ origin: 'create',
200
+ collection: collection.id,
201
+ documentId,
202
+ patch: result.patch,
203
+ provenance: result.provenance,
204
+ attempts: result.attempts,
205
+ issues: result.issues,
206
+ });
207
+ // Compact summary only — the full patch goes to the review UI, not the model.
208
+ return {
209
+ ok: true,
210
+ output: {
211
+ status: 'proposed',
212
+ collection: collection.id,
213
+ documentId,
214
+ title: resolveDocumentTitle(collection, result.patch.values, documentId),
215
+ note: 'Draft proposal created; the editor reviews and applies it in the studio. Summarize briefly — do not repeat the content.',
216
+ },
217
+ };
218
+ }
219
+ async function translate(toolCallId, input) {
220
+ const collection = collectionById(input.collection);
221
+ if (!collection) {
222
+ return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
223
+ }
224
+ if (!options.readers.getDocument || !options.localeConfig) {
225
+ return { ok: false, errorText: 'Translation is not available here.' };
226
+ }
227
+ const document = await options.readers.getDocument({
228
+ collection: input.collection,
229
+ id: input.id,
230
+ });
231
+ if (!document) {
232
+ return { ok: false, errorText: `Document "${input.collection}/${input.id}" was not found.` };
233
+ }
234
+ const knowledge = options.knowledge?.();
235
+ const result = await translateDocument({
236
+ provider: options.provider,
237
+ collection,
238
+ document,
239
+ localeConfig: options.localeConfig,
240
+ ...(input.locale ? { targetLocales: [input.locale] } : {}),
241
+ ...(knowledge ? { knowledge } : {}),
242
+ });
243
+ if (result.total === 0) {
244
+ return {
245
+ ok: true,
246
+ output: {
247
+ status: 'nothing-to-translate',
248
+ note: 'Every locale value is already filled — there is nothing to translate.',
249
+ },
250
+ };
251
+ }
252
+ if (result.translated === 0) {
253
+ return { ok: false, errorText: 'The translation did not produce any content.' };
254
+ }
255
+ options.onProposal(toolCallId, {
256
+ origin: 'translate',
257
+ collection: collection.id,
258
+ documentId: document.id,
259
+ patch: {
260
+ collection: collection.id,
261
+ id: document.id,
262
+ values: result.document.values,
263
+ ...(result.document.localized ? { localized: result.document.localized } : {}),
264
+ },
265
+ provenance: result.provenance,
266
+ attempts: 1,
267
+ issues: [],
268
+ before: {
269
+ values: document.values,
270
+ ...(document.localized ? { localized: document.localized } : {}),
271
+ },
272
+ });
273
+ return {
274
+ ok: true,
275
+ output: {
276
+ status: 'proposed',
277
+ collection: collection.id,
278
+ documentId: document.id,
279
+ translatedUnits: result.translated,
280
+ note: 'Translation proposal created; the editor reviews and applies it in the studio.',
281
+ },
282
+ };
283
+ }
284
+ return {
285
+ async execute(call) {
286
+ const definition = chatToolDefinitions[call.toolName];
287
+ if (!definition) {
288
+ return { ok: false, errorText: `Unknown tool "${call.toolName}".` };
289
+ }
290
+ const parsed = definition.inputSchema.safeParse(call.input);
291
+ if (!parsed.success) {
292
+ return { ok: false, errorText: `Invalid ${call.toolName} input: ${parsed.error.message}` };
293
+ }
294
+ try {
295
+ switch (call.toolName) {
296
+ case 'searchDocuments':
297
+ return await searchDocuments(parsed.data);
298
+ case 'listAssets':
299
+ return await listAssets(parsed.data);
300
+ case 'getDocument':
301
+ return await getDocument(parsed.data);
302
+ case 'createDocument':
303
+ return await createDocument(call.toolCallId, parsed.data);
304
+ case 'translateDocument':
305
+ return await translate(call.toolCallId, parsed.data);
306
+ // EC-378: case 'updateFields' lands here (leashed field-update proposals).
307
+ default:
308
+ return { ok: false, errorText: `Unknown tool "${call.toolName}".` };
309
+ }
310
+ }
311
+ catch (error) {
312
+ return {
313
+ ok: false,
314
+ errorText: `The ${call.toolName} step failed: ${error instanceof Error ? error.message : 'unknown error'}`,
315
+ };
316
+ }
317
+ },
318
+ };
73
319
  }
74
320
  //# sourceMappingURL=tools.js.map