jat-feedback 3.7.1 → 4.0.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 CHANGED
@@ -35,6 +35,8 @@ The package includes the widget bundle, Supabase migration, and edge function
35
35
  | `agent-proxy` | No | `''` | URL path for the LLM proxy endpoint (e.g., `'/api/feedback/agent'`). Enables the Agent tab. |
36
36
  | `agent-model` | No | `'claude-sonnet-4-6'` | LLM model identifier passed to the proxy endpoint |
37
37
  | `agent-context` | No | `''` | Static app context injected into Agent system prompt (describe your app, key pages, nav) |
38
+ | `agent-role` | No | `''` | Role identifier for the current user (e.g. `'dispatcher'`, `'owner'`, `'tech'`). Injected as `[Role: X]` in the system prompt so the LLM can tailor tone, available tools, and safety defaults per role. |
39
+ | `approval-required-tools` | No | `''` | Comma-separated list of tool names that require user approval before executing (e.g. `'reassign_job,delete_user'`). Replaces the default set of four DOM-modifying built-ins. Omit to keep default gating. See [Approval-Required Tools](#approval-required-tools). |
38
40
 
39
41
  ## Agent Tab: LLM Proxy Endpoint
40
42
 
@@ -130,18 +132,34 @@ The Agent tab appears automatically when `agent-proxy` is set. Without it, only
130
132
 
131
133
  ### Page-Level Tool Registration
132
134
 
133
- Register custom tools that the agent can call during its execution loop. Tools run client-side with full access to your app's state, auth context, and data — the LLM decides when to call them and reasons over the results.
135
+ Register custom tools that the agent can call during its execution loop. Tools come in two flavors pick the one that fits the data and trust boundary:
136
+
137
+ - **Client tools** (default) run in the browser with full access to your app's state, auth context, and DOM. Use these for read-only page state (current user, current route, page data) and anything the user is already authenticated to do client-side.
138
+ - **Server tools** forward the call to your proxy at `{agent-proxy}/tool/execute` and execute server-side with your own authenticated context (e.g. SvelteKit `locals.currentOrg`). Use these for cross-tenant lookups, privileged writes, or anything that touches secrets — the widget never sees them.
139
+
140
+ The LLM doesn't care which kind a tool is — it sees the same name + description + parameters either way. The widget renders results identically.
134
141
 
135
142
  #### `registerTools()` API Reference
136
143
 
137
144
  ```typescript
138
- interface ToolDefinition {
145
+ type ToolDefinition = ClientToolDefinition | ServerToolDefinition;
146
+
147
+ interface ClientToolDefinition {
139
148
  name: string;
140
149
  description: string;
141
- parameters: Record<string, unknown>; // JSON Schema object
150
+ parameters: Record<string, unknown>; // JSON Schema object
151
+ executor?: 'client'; // optional, default
142
152
  handler: (args: Record<string, unknown>) => Promise<unknown>;
143
153
  }
144
154
 
155
+ interface ServerToolDefinition {
156
+ name: string;
157
+ description: string;
158
+ parameters: Record<string, unknown>; // JSON Schema object
159
+ executor: 'server'; // required
160
+ // No handler — execution happens at {agent-proxy}/tool/execute
161
+ }
162
+
145
163
  const widget = document.querySelector('jat-feedback');
146
164
  widget.registerTools(tools: ToolDefinition[]): void
147
165
  ```
@@ -151,13 +169,15 @@ widget.registerTools(tools: ToolDefinition[]): void
151
169
  | `name` | `string` | Unique tool name (snake_case). The LLM uses this to call the tool. |
152
170
  | `description` | `string` | Tells the LLM what the tool does and when to use it. |
153
171
  | `parameters` | `object` | JSON Schema describing the tool's arguments. Use `{ type: 'object', properties: {} }` for no-arg tools. |
154
- | `handler` | `async (args) => any` | Runs client-side when the LLM calls the tool. Receives parsed args, returns any JSON-serializable value. |
172
+ | `executor` | `'client' \| 'server'` | Where the tool runs. Omit (or `'client'`) for browser-side handlers. Set to `'server'` to forward execution to the proxy. |
173
+ | `handler` | `async (args) => any` | **Client tools only.** Runs in the browser when the LLM calls the tool. Receives parsed args, returns any JSON-serializable value. |
155
174
 
156
175
  **Behavior:**
157
176
  - Multiple `registerTools()` calls **accumulate** — tools are added, not replaced
158
177
  - Register tools before the user opens the Agent tab (typically in `onMount`)
159
- - If a handler throws, the error message is returned to the LLM so it can recover gracefully
160
- - Handlers have full access to the page's DOM, stores, and JS context
178
+ - If a client handler throws, the error message is returned to the LLM so it can recover gracefully
179
+ - Client handlers have full access to the page's DOM, stores, and JS context
180
+ - Server tools require an `agent-proxy` attribute — without it, the widget refuses to call them
161
181
 
162
182
  #### Example: Global Tools in SvelteKit
163
183
 
@@ -252,13 +272,188 @@ Add tools that only make sense on a specific page:
252
272
  </script>
253
273
  ```
254
274
 
275
+ #### Example: Server Tool
276
+
277
+ Server tools execute on your server with whatever org/auth context your proxy has. Declare them by name + description + parameter schema only — there's no client handler:
278
+
279
+ ```svelte
280
+ <script lang="ts">
281
+ import { onMount } from "svelte"
282
+
283
+ onMount(() => {
284
+ const widget = document.querySelector("jat-feedback")
285
+ if (!widget?.registerTools) return
286
+
287
+ widget.registerTools([
288
+ {
289
+ name: "lookup_customer",
290
+ description: "Find a customer by email. Returns id, name, plan, and most-recent order.",
291
+ parameters: {
292
+ type: "object",
293
+ properties: { email: { type: "string", format: "email" } },
294
+ required: ["email"],
295
+ },
296
+ executor: "server",
297
+ },
298
+ {
299
+ name: "reassign_job",
300
+ description: "Reassign a job to a different dispatcher. Destructive — confirm before calling.",
301
+ parameters: {
302
+ type: "object",
303
+ properties: {
304
+ job_id: { type: "string" },
305
+ new_assignee_id: { type: "string" },
306
+ },
307
+ required: ["job_id", "new_assignee_id"],
308
+ },
309
+ executor: "server",
310
+ },
311
+ ])
312
+ })
313
+ </script>
314
+ ```
315
+
316
+ ##### Proxy Contract: `POST {agent-proxy}/tool/execute`
317
+
318
+ The widget POSTs the tool call as JSON; the proxy executes the tool and responds in one of two shapes.
319
+
320
+ **Request:**
321
+
322
+ ```http
323
+ POST /api/feedback/agent/tool/execute
324
+ Content-Type: application/json
325
+
326
+ {
327
+ "tool": "lookup_customer",
328
+ "args": { "email": "ada@example.com" },
329
+ "model": "claude-sonnet-4-6"
330
+ }
331
+ ```
332
+
333
+ **Response — JSON (single result):**
334
+
335
+ ```http
336
+ HTTP/1.1 200 OK
337
+ Content-Type: application/json
338
+
339
+ { "result": { "id": "cust_123", "name": "Ada Lovelace", "plan": "pro" } }
340
+ ```
341
+
342
+ Or, on a refused/errored call:
343
+
344
+ ```json
345
+ { "error": "Customer not found in your organization" }
346
+ ```
347
+
348
+ **Response — SSE (streaming):**
349
+
350
+ ```http
351
+ HTTP/1.1 200 OK
352
+ Content-Type: text/event-stream
353
+
354
+ data: {"type":"chunk","text":"Looking up customer…\n"}
355
+
356
+ data: {"type":"chunk","text":"Found 1 match.\n"}
357
+
358
+ data: {"type":"result","value":{"id":"cust_123","name":"Ada Lovelace","plan":"pro"}}
359
+ ```
360
+
361
+ `chunk` events render live as a typing indicator inside the result message. The terminating `result` event delivers the structured value the LLM sees on its next step. An `error` event aborts the call and surfaces a user-facing error.
362
+
363
+ ##### SvelteKit Example: Wiring `/tool/execute`
364
+
365
+ ```ts
366
+ // src/routes/api/feedback/agent/tool/execute/+server.ts
367
+ import { json, error } from '@sveltejs/kit';
368
+ import type { RequestHandler } from './$types';
369
+
370
+ const TOOLS: Record<string, (args: any, locals: App.Locals) => Promise<unknown>> = {
371
+ async lookup_customer(args, locals) {
372
+ if (!locals.currentOrg) throw error(403, 'no org context');
373
+ const { data, error: dbErr } = await locals.supabase
374
+ .from('customers')
375
+ .select('id, name, plan')
376
+ .eq('org_id', locals.currentOrg.id)
377
+ .eq('email', args.email)
378
+ .maybeSingle();
379
+ if (dbErr) throw error(500, dbErr.message);
380
+ if (!data) return { error: 'Customer not found in your organization' };
381
+ return { result: data };
382
+ },
383
+ // …
384
+ };
385
+
386
+ export const POST: RequestHandler = async ({ request, locals }) => {
387
+ const { tool, args } = await request.json();
388
+ const handler = TOOLS[tool];
389
+ if (!handler) return json({ error: `Unknown tool: ${tool}` });
390
+ try {
391
+ const result = await handler(args, locals);
392
+ return json(result);
393
+ } catch (err) {
394
+ return json({ error: err instanceof Error ? err.message : 'Tool failed' });
395
+ }
396
+ };
397
+ ```
398
+
399
+ For SSE, set `Content-Type: text/event-stream`, return a `ReadableStream`, and emit `data: {…}\n\n` lines per event. Always finish with a `result` event so the LLM sees a structured value.
400
+
255
401
  #### How Tools Work
256
402
 
257
403
  Tools are integrated into the page-agent's action loop:
258
404
  - The LLM sees registered tools alongside built-in browser actions (click, type, scroll, etc.)
259
405
  - Each agent step picks one action — either a browser action or a registered tool
260
406
  - Tool results feed back to the LLM on the next step automatically
261
- - If a handler throws, the error message is returned to the LLM so it can recover gracefully
407
+ - If a client handler throws, the error message is returned to the LLM so it can recover gracefully
408
+ - Server tools forward to `{agent-proxy}/tool/execute`; the LLM sees only the final structured value (or `error` message)
409
+
410
+ ### Approval-Required Tools
411
+
412
+ By default, four DOM-modifying built-ins (`click_element_by_index`, `input_text`, `select_dropdown_option`, `execute_javascript`) require the user to click "Allow" before the agent executes them. v4 makes this set configurable.
413
+
414
+ **Extend the default set (add your server tools):**
415
+
416
+ ```svelte
417
+ <script>
418
+ import { onMount } from "svelte"
419
+
420
+ onMount(() => {
421
+ const widget = document.querySelector("jat-feedback")
422
+ if (!widget?.setApprovalRequiredTools) return
423
+ // Keep the default four DOM tools AND gate your custom tool
424
+ widget.setApprovalRequiredTools([
425
+ "click_element_by_index", "input_text", "select_dropdown_option",
426
+ "execute_javascript", "reassign_job"
427
+ ])
428
+ })
429
+ </script>
430
+ ```
431
+
432
+ **Replace the default set (only gate your tools):**
433
+
434
+ ```html
435
+ <jat-feedback approval-required-tools="reassign_job,delete_user" ...></jat-feedback>
436
+ ```
437
+
438
+ **Per-tool flag (no global list needed):**
439
+
440
+ ```typescript
441
+ widget.registerTools([{
442
+ name: "delete_user",
443
+ description: "Permanently delete a user account",
444
+ parameters: { type: "object", properties: { user_id: { type: "string" } }, required: ["user_id"] },
445
+ executor: "server",
446
+ requireApproval: true, // gates this tool regardless of the global list
447
+ }])
448
+ ```
449
+
450
+ **Disable approval prompts entirely:**
451
+
452
+ ```typescript
453
+ widget.setApprovalRequiredTools([]) // empty list + no per-tool flags = no prompts
454
+ ```
455
+
456
+ The exported `DEFAULT_APPROVAL_REQUIRED_TOOLS` constant lists the four built-in DOM tools — import it if you want to spread them into a custom list programmatically.
262
457
 
263
458
  ### Error Handling
264
459
 
@@ -862,6 +1057,49 @@ The `project_tasks` table columns used by the pipeline:
862
1057
  | `parent_id` | UUID | 3.0.0 | Self-referential parent for hierarchical tasks |
863
1058
  | `updated_at` | TIMESTAMPTZ | 3.0.0 | Auto-updated timestamp |
864
1059
 
1060
+ ## Upgrading from v3 to v4
1061
+
1062
+ v4 promotes `ToolDefinition` from a flat interface to a discriminated union. Existing client tools continue to work at runtime unchanged — this is a TypeScript-types-only breaking change.
1063
+
1064
+ ### TypeScript: update `ToolDefinition` annotations
1065
+
1066
+ ```typescript
1067
+ // v3 (old)
1068
+ import type { ToolDefinition } from "jat-feedback"
1069
+ const tools: ToolDefinition[] = [...]
1070
+
1071
+ // v4 (new) — same import, type is now ClientToolDefinition | ServerToolDefinition
1072
+ import type { ToolDefinition, ClientToolDefinition, ServerToolDefinition } from "jat-feedback"
1073
+
1074
+ // If you have generic arrays, use the union type:
1075
+ const tools: ToolDefinition[] = [...] // still works (union exported as ToolDefinition)
1076
+
1077
+ // Or be specific:
1078
+ const clientTools: ClientToolDefinition[] = [...]
1079
+ const serverTools: ServerToolDefinition[] = [...]
1080
+ ```
1081
+
1082
+ **Existing tool objects don't need changes.** A plain `{ name, description, parameters, handler }` object satisfies `ClientToolDefinition` — `executor` defaults to `'client'` when omitted.
1083
+
1084
+ ### New: add server tools (optional)
1085
+
1086
+ If you want tools that execute server-side with your auth context, see the [Server Tool](#example-server-tool) docs. No changes needed if you only use client tools.
1087
+
1088
+ ### New: configure approval gating (optional)
1089
+
1090
+ The default approval behavior (four DOM built-ins) is unchanged. If you register server tools that should also require confirmation, use `requireApproval: true` on the tool definition or `widget.setApprovalRequiredTools([...])`. See [Approval-Required Tools](#approval-required-tools).
1091
+
1092
+ ### Bump the version
1093
+
1094
+ ```bash
1095
+ # In each consuming project
1096
+ npm install jat-feedback@^4.0.0
1097
+ ```
1098
+
1099
+ No schema migrations required for v4 — all changes are widget JS only.
1100
+
1101
+ ---
1102
+
865
1103
  ## Upgrading
866
1104
 
867
1105
  npm never auto-updates — you need to explicitly upgrade and then handle each type of change manually.