duet-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,442 @@
1
+ # duet-kit
2
+
3
+ **Shared state for humans and AI.** A Zustand store that both can edit, with validation and an audit trail.
4
+
5
+ ## The Problem
6
+
7
+ You're building a UI where humans and AI edit the same state—a configuration wizard with AI suggestions, a form where AI auto-fills based on conversation, an editor where AI can make changes alongside you.
8
+
9
+ The AI needs:
10
+ - Schema context to know what fields exist
11
+ - Validation to prevent invalid edits
12
+ - A structured format to apply changes
13
+
14
+ You end up writing boilerplate: prompt templates, JSON parsing, validation logic, error handling. Every team does this differently. You can absolutely build this yourself—duet-kit just makes it simpler.
15
+
16
+ ## The Solution
17
+
18
+ **duet-kit** gives your Zustand store an LLM interface. One schema, two editors (human + AI), same validation rules. State changes are instant—no round-trip to a server.
19
+
20
+ ```typescript
21
+ import { createDuet, field, z } from 'duet-kit'
22
+
23
+ const useFormStore = createDuet('ContactForm', {
24
+ name: field(z.string().min(1), 'Full Name', ''),
25
+ email: field(z.string().email(), 'Email', ''),
26
+ company: field(z.string(), 'Company', ''),
27
+ })
28
+
29
+ // React UI uses it like any Zustand store
30
+ const { data, set } = useFormStore()
31
+
32
+ // LLM uses the attached bridge
33
+ useFormStore.llm.getContext() // schema + current values for prompt
34
+ useFormStore.llm.applyJSON('[{"op":"replace",...}]') // JSON Patch from LLM
35
+ useFormStore.llm.getFunctionSchema() // OpenAI function calling format
36
+ useFormStore.llm.history() // audit trail of all patches
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - **Zustand-based** — Works like any Zustand store. No new patterns to learn.
42
+ - **Zod validation** — Schema defines both types and constraints. LLM edits are validated.
43
+ - **JSON Patch (RFC 6902)** — Standard format for edits. Supports nested fields and LLM know it already.
44
+ - **Audit trail** — Every patch logged with timestamp, source, and result.
45
+ - **Drop-in ready** — `attachLLM()` adds capabilities to existing Zustand + Zod code.
46
+ - **TypeScript** — Full type inference from your schema.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ npm install duet-kit
52
+ ```
53
+
54
+ Peer dependencies: `react`, `zustand`, `zod`
55
+
56
+ ---
57
+
58
+ ## Quick Start
59
+
60
+ ```typescript
61
+ import { createDuet, field, z } from 'duet-kit'
62
+
63
+ // Define schema with nested fields
64
+ const useStore = createDuet('TripBudget', {
65
+ destination: field(z.string().min(1), 'Destination', 'Tokyo'),
66
+ days: field(z.number().min(1).max(365), 'Duration', 7),
67
+ accommodation: field(
68
+ z.object({
69
+ type: z.enum(['hotel', 'airbnb', 'hostel']),
70
+ budgetPerNight: z.number().min(0),
71
+ }),
72
+ 'Accommodation',
73
+ { type: 'hotel', budgetPerNight: 150 }
74
+ ),
75
+ }, { persist: 'trip-data' })
76
+
77
+ // React component
78
+ function TripForm() {
79
+ const { data, set } = useStore()
80
+
81
+ return (
82
+ <input
83
+ value={data.destination}
84
+ onChange={e => set('destination', e.target.value)}
85
+ />
86
+ )
87
+ }
88
+
89
+ // LLM integration
90
+ const result = useStore.llm.applyJSON(`[
91
+ { "op": "replace", "path": "/destination", "value": "Paris" },
92
+ { "op": "replace", "path": "/accommodation/type", "value": "airbnb" }
93
+ ]`)
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Already Using Zustand + Zod?
99
+
100
+ Add LLM capabilities without changing your existing code:
101
+
102
+ ```typescript
103
+ import { create } from 'zustand'
104
+ import { z } from 'zod'
105
+ import { attachLLM } from 'duet-kit'
106
+
107
+ // Your existing store (unchanged)
108
+ const schema = z.object({
109
+ title: z.string(),
110
+ priority: z.number().min(1).max(5),
111
+ })
112
+
113
+ const useStore = create<z.infer<typeof schema>>()(() => ({
114
+ title: '',
115
+ priority: 1,
116
+ }))
117
+
118
+ // One line addition
119
+ const llm = attachLLM(useStore, schema, { name: 'Task' })
120
+
121
+ // Now available:
122
+ llm.getContext() // prompt context
123
+ llm.applyJSON(...) // apply LLM output
124
+ llm.getFunctionSchema() // OpenAI tools format
125
+ llm.history() // audit trail
126
+ ```
127
+
128
+ No migration. No rewrites. Your store and schema stay exactly as they are.
129
+
130
+ ---
131
+
132
+ ## API Reference
133
+
134
+ ### `createDuet(name, fields, options?)`
135
+
136
+ Creates a Zustand store with LLM bridge attached.
137
+
138
+ ```typescript
139
+ const useStore = createDuet('FormName', {
140
+ fieldName: field(zodSchema, 'Label', defaultValue),
141
+ }, {
142
+ persist: 'localStorage-key', // optional localStorage persistence
143
+ transformContext: (ctx) => `Custom instructions...\n\n${ctx}`, // customize LLM prompt
144
+ transformFunctionSchema: (schema) => ({ ...schema, description: 'Custom' }), // customize function schema
145
+ })
146
+ ```
147
+
148
+ **Options:**
149
+ | Option | Description |
150
+ |--------|-------------|
151
+ | `persist` | localStorage key for persistence (omit for in-memory only) |
152
+ | `transformContext` | Transform the default `getContext()` output before returning |
153
+ | `transformFunctionSchema` | Transform the default `getFunctionSchema()` output before returning |
154
+
155
+ Returns a Zustand hook with `.llm` and `.schema` properties.
156
+
157
+ ### `field(schema, label, defaultValue)`
158
+
159
+ ```typescript
160
+ field(z.string().min(1), 'Username', '')
161
+ field(z.number().min(0).max(100), 'Score', 0)
162
+ field(z.enum(['draft', 'published']), 'Status', 'draft')
163
+ field(z.boolean(), 'Active', true)
164
+
165
+ // Nested objects
166
+ field(z.object({
167
+ street: z.string(),
168
+ city: z.string(),
169
+ }), 'Address', { street: '', city: '' })
170
+ ```
171
+
172
+ ### `attachLLM(store, zodSchema, options?)`
173
+
174
+ For existing Zustand stores:
175
+
176
+ ```typescript
177
+ const llm = attachLLM(existingStore, existingSchema, {
178
+ name: 'SchemaName',
179
+ labels: { fieldName: 'Human Label' }
180
+ })
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Store API
186
+
187
+ ```typescript
188
+ const { data, set, setMany, reset } = useStore()
189
+
190
+ data.fieldName // read
191
+ set('fieldName', value) // write single (returns success boolean)
192
+ setMany({ a: 1, b: 2 }) // write multiple
193
+ reset() // restore defaults
194
+ ```
195
+
196
+ ---
197
+
198
+ ## LLM Bridge API
199
+
200
+ ### `getContext()`
201
+
202
+ Generates a prompt-ready string with schema and current values:
203
+
204
+ ```
205
+ Schema: TripBudget
206
+ Fields:
207
+ - destination (string, min: 1): Destination
208
+ - days (number, min: 1, max: 365): Duration
209
+ - accommodation (object): Accommodation
210
+
211
+ Current Values:
212
+ destination: "Tokyo" (Destination)
213
+ days: 7 (Duration)
214
+ accommodation: {"type":"hotel","budgetPerNight":150} (Accommodation)
215
+
216
+ To edit fields, respond with a JSON Patch array (RFC 6902):
217
+ [{ "op": "replace", "path": "/destination", "value": "Paris" }]
218
+ ```
219
+
220
+ ### `applyJSON(jsonString, source?)`
221
+
222
+ Parses and applies JSON Patch from LLM output:
223
+
224
+ ```typescript
225
+ // Flat field
226
+ const patch1 = '[{ "op": "replace", "path": "/budget", "value": 8000 }]'
227
+
228
+ // Nested field
229
+ const patch2 = '[{ "op": "replace", "path": "/accommodation/type", "value": "airbnb" }]'
230
+
231
+ const result = useStore.llm.applyJSON(patch1)
232
+ // or with source tracking
233
+ const result = useStore.llm.applyJSON(patch1, 'llm')
234
+
235
+ if (result.success) {
236
+ console.log(`Applied ${result.applied} operation(s)`)
237
+ } else {
238
+ console.error(result.error) // validation message
239
+ }
240
+ ```
241
+
242
+ Accepts both array format `[...]` and wrapped format `{ "patch": [...] }`.
243
+
244
+ ### `getFunctionSchema()`
245
+
246
+ Returns OpenAI/Anthropic function calling format:
247
+
248
+ ```typescript
249
+ const response = await openai.chat.completions.create({
250
+ model: 'gpt-4',
251
+ messages: [...],
252
+ tools: [{
253
+ type: 'function',
254
+ function: useStore.llm.getFunctionSchema()
255
+ }]
256
+ })
257
+ ```
258
+
259
+ ### `history()`
260
+
261
+ Returns the full audit trail of all patches:
262
+
263
+ ```typescript
264
+ const history = useStore.llm.history()
265
+ // [
266
+ // {
267
+ // id: "1",
268
+ // timestamp: 1703782800000,
269
+ // patch: [{ op: "replace", path: "/budget", value: 5000 }],
270
+ // source: "llm",
271
+ // result: { success: true, applied: 1 }
272
+ // }
273
+ // ]
274
+ ```
275
+
276
+ ### `clearHistory()`
277
+
278
+ Clears the patch history.
279
+
280
+ ### Other Methods
281
+
282
+ | Method | Description |
283
+ |--------|-------------|
284
+ | `getCompactContext()` | Shorter context for token-constrained prompts |
285
+ | `applyPatch(ops, source?)` | Apply `JsonPatchOp[]` directly (typed) |
286
+ | `getCurrentValues()` | Get current store state |
287
+
288
+ ---
289
+
290
+ ## Nested Fields
291
+
292
+ duet-kit supports nested objects using JSON Pointer paths:
293
+
294
+ ```typescript
295
+ const useStore = createDuet('CRMLead', {
296
+ contact: field(z.object({
297
+ name: z.string(),
298
+ email: z.string().email(),
299
+ phone: z.string(),
300
+ }), 'Contact', { name: '', email: '', phone: '' }),
301
+
302
+ company: field(z.object({
303
+ name: z.string(),
304
+ industry: z.enum(['tech', 'finance', 'healthcare']),
305
+ }), 'Company', { name: '', industry: 'tech' }),
306
+ })
307
+
308
+ // LLM can update nested fields directly
309
+ useStore.llm.applyJSON(`[
310
+ { "op": "replace", "path": "/contact/name", "value": "Sarah Chen" },
311
+ { "op": "replace", "path": "/contact/email", "value": "sarah@acme.com" },
312
+ { "op": "replace", "path": "/company/name", "value": "Acme Corp" }
313
+ ]`)
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Patch Log (Debugging)
319
+
320
+ Every patch is logged for debugging. When an LLM makes unexpected changes or validation fails, you can inspect what happened:
321
+
322
+ ```typescript
323
+ const history = useStore.llm.history()
324
+
325
+ // [
326
+ // {
327
+ // id: "1",
328
+ // timestamp: 1703782800000,
329
+ // patch: [{ op: "replace", path: "/budget", value: 5000 }],
330
+ // source: "llm",
331
+ // result: { success: true, applied: 1 }
332
+ // },
333
+ // {
334
+ // id: "2",
335
+ // timestamp: 1703782810000,
336
+ // patch: [{ op: "replace", path: "/budget", value: 99999999 }],
337
+ // source: "llm",
338
+ // result: { success: false, error: "Number must be at most 1000000" }
339
+ // }
340
+ // ]
341
+
342
+ // Tag the source for filtering
343
+ useStore.llm.applyPatch([...], 'user') // manual edit
344
+ useStore.llm.applyPatch([...], 'llm') // AI edit (default)
345
+ useStore.llm.applyPatch([...], 'system') // webhook/automation
346
+
347
+ useStore.llm.clearHistory() // reset
348
+ ```
349
+
350
+ This is a change log, not a full decision trace. It tells you *what* changed, not *why* (the input, reasoning, or context that led to the change). Useful for debugging and simple undo—not a replacement for proper audit infrastructure.
351
+
352
+ ---
353
+
354
+ ## Use Cases
355
+
356
+ **Configuration wizards**: User sets options, AI suggests related settings, both iterate until "Create" is clicked. Draft state lives in the client until committed.
357
+
358
+ **AI-assisted editors**: Human and AI editing together in real-time—proposals, quotes, documents. Like Google Docs, but one editor is an AI.
359
+
360
+ **Chat-driven UI**: "Change the budget to $5000" → AI parses intent → validated state update → UI reflects instantly.
361
+
362
+ **Agentic workflows**: AI agents that modify your app state autonomously, with validation guardrails and an audit trail of what changed.
363
+
364
+ ### When to use duet-kit
365
+
366
+ Use it when human and AI are **editing shared state together in a live session**—especially draft state that doesn't exist in your database yet.
367
+
368
+ If your architecture is "agent updates DB → client syncs later," you probably don't need this. duet-kit is for interactive, client-side state where both editors see changes immediately.
369
+
370
+ ---
371
+
372
+ ## TypeScript
373
+
374
+ Types are inferred from your schema:
375
+
376
+ ```typescript
377
+ const useStore = createDuet('User', {
378
+ name: field(z.string(), 'Name', ''),
379
+ age: field(z.number(), 'Age', 0),
380
+ })
381
+
382
+ const { data, set } = useStore()
383
+ data.name // string
384
+ data.age // number
385
+ set('name', 123) // TS error
386
+ ```
387
+
388
+ ---
389
+
390
+ ## Examples
391
+
392
+ See [`examples/`](./examples) for working demos:
393
+
394
+ ### Trip Planner
395
+ - Side-by-side human and AI editing the same state
396
+ - Nested fields (accommodation type, budget per night)
397
+ - Debug panel showing `.llm.getContext()`, `.llm.getFunctionSchema()`, `.llm.history()`
398
+
399
+ ### CRM Lead Entry
400
+ - AI extracts structured data from natural language input
401
+ - Voice input with Web Speech API (optional)
402
+ - OpenAI integration (optional—works with mock LLM too)
403
+ - Nested contact and company fields with validation
404
+ - Full audit trail of AI changes
405
+
406
+ ```bash
407
+ cd examples && npm install && npm run dev
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Architecture
413
+
414
+ ```
415
+ ┌──────────────────────────────────────────────────────┐
416
+ │ Your App │
417
+ │ │
418
+ │ React UI LLM / Voice / API │
419
+ │ │ │ │
420
+ │ │ set() │ applyJSON() │
421
+ │ ▼ ▼ │
422
+ │ ┌──────────────────────────────────────────────┐ │
423
+ │ │ useFormStore │ │
424
+ │ │ │ │
425
+ │ │ Zustand Zod LLM Bridge │ │
426
+ │ │ (state) (validation) (context + │ │
427
+ │ │ patch log) │ │
428
+ │ └──────────────────────────────────────────────┘ │
429
+ └──────────────────────────────────────────────────────┘
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Future
435
+
436
+ I'm hoping state management libraries like Zustand offer this natively in the future. Until then, duet-kit fills the gap.
437
+
438
+ ---
439
+
440
+ ## License
441
+
442
+ MIT
@@ -0,0 +1,48 @@
1
+ /**
2
+ * duet-kit - Drop-in LLM Bridge
3
+ *
4
+ * For users who already have Zustand and Zod in their codebase,
5
+ * this allows adding LLM capabilities without rewriting anything.
6
+ * Uses RFC 6902 JSON Patch format.
7
+ */
8
+ import { z } from 'zod';
9
+ import type { StoreApi, UseBoundStore } from 'zustand';
10
+ import type { LLMBridge, SchemaFields } from './types';
11
+ type ZodObjectSchema = z.ZodObject<z.ZodRawShape>;
12
+ interface AttachOptions {
13
+ /** Name for the schema (used in LLM context) */
14
+ name?: string;
15
+ /** Human-readable labels for fields */
16
+ labels?: Record<string, string>;
17
+ }
18
+ /**
19
+ * Attach an LLM bridge to an existing Zustand store + Zod schema.
20
+ * Uses RFC 6902 JSON Patch format for edits.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Your existing code (unchanged):
25
+ * const todoSchema = z.object({
26
+ * title: z.string(),
27
+ * completed: z.boolean(),
28
+ * priority: z.number().min(1).max(5),
29
+ * });
30
+ *
31
+ * const useTodoStore = create<z.infer<typeof todoSchema>>()((set) => ({
32
+ * title: '',
33
+ * completed: false,
34
+ * priority: 1,
35
+ * }));
36
+ *
37
+ * // Drop-in addition (one line):
38
+ * const todoLLM = attachLLM(useTodoStore, todoSchema);
39
+ *
40
+ * // Now you have full LLM capabilities:
41
+ * todoLLM.getContext() // Generate context for prompts
42
+ * todoLLM.applyJSON('...') // Apply JSON Patch
43
+ * todoLLM.getFunctionSchema() // OpenAI function calling
44
+ * ```
45
+ */
46
+ export declare function attachLLM<T extends ZodObjectSchema>(store: UseBoundStore<StoreApi<z.infer<T>>>, schema: T, options?: AttachOptions): LLMBridge<SchemaFields>;
47
+ export {};
48
+ //# sourceMappingURL=attach.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAA2B,SAAS,EAAa,YAAY,EAAgB,MAAM,SAAS,CAAC;AAEzG,KAAK,eAAe,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;AAElD,UAAU,aAAa;IACrB,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,eAAe,EACjD,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAC1C,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,aAAkB,GAC1B,SAAS,CAAC,YAAY,CAAC,CA8LzB"}
package/dist/attach.js ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * duet-kit - Drop-in LLM Bridge
3
+ *
4
+ * For users who already have Zustand and Zod in their codebase,
5
+ * this allows adding LLM capabilities without rewriting anything.
6
+ * Uses RFC 6902 JSON Patch format.
7
+ */
8
+ import { z } from 'zod';
9
+ /**
10
+ * Attach an LLM bridge to an existing Zustand store + Zod schema.
11
+ * Uses RFC 6902 JSON Patch format for edits.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Your existing code (unchanged):
16
+ * const todoSchema = z.object({
17
+ * title: z.string(),
18
+ * completed: z.boolean(),
19
+ * priority: z.number().min(1).max(5),
20
+ * });
21
+ *
22
+ * const useTodoStore = create<z.infer<typeof todoSchema>>()((set) => ({
23
+ * title: '',
24
+ * completed: false,
25
+ * priority: 1,
26
+ * }));
27
+ *
28
+ * // Drop-in addition (one line):
29
+ * const todoLLM = attachLLM(useTodoStore, todoSchema);
30
+ *
31
+ * // Now you have full LLM capabilities:
32
+ * todoLLM.getContext() // Generate context for prompts
33
+ * todoLLM.applyJSON('...') // Apply JSON Patch
34
+ * todoLLM.getFunctionSchema() // OpenAI function calling
35
+ * ```
36
+ */
37
+ export function attachLLM(store, schema, options = {}) {
38
+ const name = options.name || 'State';
39
+ const labels = options.labels || {};
40
+ const shape = schema.shape;
41
+ const fieldIds = Object.keys(shape);
42
+ // Validate a single field value
43
+ const validateField = (field, value) => {
44
+ const fieldSchema = shape[field];
45
+ if (!fieldSchema) {
46
+ return { success: false, error: `Unknown field: ${field}` };
47
+ }
48
+ const result = fieldSchema.safeParse(value);
49
+ if (!result.success) {
50
+ return { success: false, error: result.error.errors[0]?.message || 'Invalid value' };
51
+ }
52
+ return { success: true, data: result.data };
53
+ };
54
+ // Get field description from Zod schema
55
+ const getFieldDescription = (fieldId) => {
56
+ const fieldSchema = shape[fieldId];
57
+ const parts = [];
58
+ if (fieldSchema instanceof z.ZodString)
59
+ parts.push('string');
60
+ else if (fieldSchema instanceof z.ZodNumber)
61
+ parts.push('number');
62
+ else if (fieldSchema instanceof z.ZodBoolean)
63
+ parts.push('boolean');
64
+ else if (fieldSchema instanceof z.ZodEnum) {
65
+ const values = fieldSchema._def.values;
66
+ parts.push(`enum: ${values.join(' | ')}`);
67
+ }
68
+ if (fieldSchema instanceof z.ZodNumber) {
69
+ const checks = fieldSchema._def.checks || [];
70
+ for (const check of checks) {
71
+ if (check.kind === 'min')
72
+ parts.push(`min: ${check.value}`);
73
+ if (check.kind === 'max')
74
+ parts.push(`max: ${check.value}`);
75
+ }
76
+ }
77
+ return parts.length > 0 ? `(${parts.join(', ')})` : '';
78
+ };
79
+ // Get default value for a field (current value as fallback)
80
+ const getDefaultValue = (fieldId) => {
81
+ return store.getState()[fieldId];
82
+ };
83
+ // History log for audit trail
84
+ const patchHistory = [];
85
+ let historyId = 0;
86
+ return {
87
+ applyPatch(patch, source = 'llm') {
88
+ const updates = {};
89
+ for (const op of patch) {
90
+ const fieldName = op.path.replace(/^\//, '').split('/')[0];
91
+ if (!fieldIds.includes(fieldName)) {
92
+ const result = { success: false, error: `Unknown field: ${fieldName}` };
93
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
94
+ return result;
95
+ }
96
+ if (op.op === 'remove') {
97
+ updates[fieldName] = getDefaultValue(fieldName);
98
+ continue;
99
+ }
100
+ if (op.op === 'replace' || op.op === 'add') {
101
+ const result = validateField(fieldName, op.value);
102
+ if (!result.success) {
103
+ const editResult = { success: false, error: `Invalid value for ${fieldName}: ${result.error}` };
104
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
105
+ return editResult;
106
+ }
107
+ updates[fieldName] = result.data;
108
+ }
109
+ }
110
+ store.setState(updates);
111
+ const result = { success: true, applied: patch.length };
112
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
113
+ return result;
114
+ },
115
+ applyJSON(json, source = 'llm') {
116
+ try {
117
+ const parsed = JSON.parse(json);
118
+ if (Array.isArray(parsed)) {
119
+ return this.applyPatch(parsed, source);
120
+ }
121
+ if (parsed.patch && Array.isArray(parsed.patch)) {
122
+ return this.applyPatch(parsed.patch, source);
123
+ }
124
+ return { success: false, error: 'Expected JSON Patch array or { patch: [...] } format' };
125
+ }
126
+ catch (e) {
127
+ return { success: false, error: `JSON parse error: ${e.message}` };
128
+ }
129
+ },
130
+ history() {
131
+ return [...patchHistory];
132
+ },
133
+ clearHistory() {
134
+ patchHistory.length = 0;
135
+ historyId = 0;
136
+ },
137
+ getContext() {
138
+ const data = store.getState();
139
+ let context = `${name} Schema:\n`;
140
+ for (const id of fieldIds) {
141
+ const label = labels[id] || id;
142
+ const desc = getFieldDescription(id);
143
+ context += ` - ${id}: ${label} ${desc}\n`;
144
+ }
145
+ context += '\nCurrent Values:\n';
146
+ for (const id of fieldIds) {
147
+ const value = data[id];
148
+ const label = labels[id] || id;
149
+ context += ` ${id}: ${JSON.stringify(value)} (${label})\n`;
150
+ }
151
+ context += `
152
+ To edit fields, respond with a JSON Patch array (RFC 6902):
153
+ [{ "op": "replace", "path": "/fieldName", "value": newValue }]
154
+
155
+ Examples:
156
+ - Single edit: [{ "op": "replace", "path": "/${fieldIds[0] || 'field'}", "value": "newValue" }]
157
+ - Multiple: [{ "op": "replace", "path": "/a", "value": 1 }, { "op": "replace", "path": "/b", "value": 2 }]`;
158
+ return context;
159
+ },
160
+ getCompactContext() {
161
+ const data = store.getState();
162
+ const values = Object.entries(data)
163
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
164
+ .join(', ');
165
+ return `${name}: {${values}}\nJSON Patch: [{"op":"replace","path":"/field","value":x}]`;
166
+ },
167
+ getFunctionSchema() {
168
+ return {
169
+ name: `patch_${name.toLowerCase().replace(/\s+/g, '_')}`,
170
+ description: `Apply JSON Patch operations to ${name}. Uses RFC 6902 format.`,
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ patch: {
175
+ type: 'array',
176
+ description: 'JSON Patch operations (RFC 6902)',
177
+ items: {
178
+ type: 'object',
179
+ properties: {
180
+ op: {
181
+ type: 'string',
182
+ enum: ['replace', 'add', 'remove'],
183
+ description: 'Operation type',
184
+ },
185
+ path: {
186
+ type: 'string',
187
+ description: `JSON Pointer to field. Valid paths: ${fieldIds.map(f => `/${f}`).join(', ')}`,
188
+ },
189
+ value: {
190
+ description: 'New value (required for replace/add)',
191
+ },
192
+ },
193
+ required: ['op', 'path'],
194
+ },
195
+ },
196
+ },
197
+ required: ['patch'],
198
+ },
199
+ };
200
+ },
201
+ getCurrentValues() {
202
+ return store.getState();
203
+ },
204
+ };
205
+ }