@elytracms/studio 0.0.55 → 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.
- package/convex/auth.ts +152 -2
- 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 +161 -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 +130 -23
- package/dist/components/ai/chat-ui.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/chat-context.d.ts +58 -0
- package/dist/lib/ai/chat-context.js +71 -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 +70 -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 +84 -6
- package/dist/lib/ai/fake-provider.js +193 -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 +3 -1
- package/dist/lib/ai/index.js +3 -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/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/tools.d.ts +169 -39
- package/dist/lib/ai/tools.js +308 -62
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -16
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +37 -14
- package/dist/lib/auth/convex-auth-adapter.js +72 -16
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +9 -0
- 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.d.ts
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The narrow tool set the
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
41
|
-
*
|
|
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
|
-
/**
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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;
|
package/dist/lib/ai/tools.js
CHANGED
|
@@ -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
|
|
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
|
+
* 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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|