@nextsparkjs/plugin-langchain 0.1.0-beta.1

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 (67) hide show
  1. package/.env.example +41 -0
  2. package/api/observability/metrics/route.ts +110 -0
  3. package/api/observability/traces/[traceId]/route.ts +398 -0
  4. package/api/observability/traces/route.ts +205 -0
  5. package/api/sessions/route.ts +332 -0
  6. package/components/observability/CollapsibleJson.tsx +71 -0
  7. package/components/observability/CompactTimeline.tsx +75 -0
  8. package/components/observability/ConversationFlow.tsx +271 -0
  9. package/components/observability/DisabledMessage.tsx +21 -0
  10. package/components/observability/FiltersPanel.tsx +82 -0
  11. package/components/observability/ObservabilityDashboard.tsx +230 -0
  12. package/components/observability/SpansList.tsx +210 -0
  13. package/components/observability/TraceDetail.tsx +335 -0
  14. package/components/observability/TraceStatusBadge.tsx +39 -0
  15. package/components/observability/TracesTable.tsx +97 -0
  16. package/components/observability/index.ts +7 -0
  17. package/docs/01-getting-started/01-overview.md +196 -0
  18. package/docs/01-getting-started/02-installation.md +368 -0
  19. package/docs/01-getting-started/03-configuration.md +794 -0
  20. package/docs/02-core-concepts/01-architecture.md +566 -0
  21. package/docs/02-core-concepts/02-agents.md +597 -0
  22. package/docs/02-core-concepts/03-tools.md +689 -0
  23. package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
  24. package/docs/03-orchestration/02-legacy-react.md +650 -0
  25. package/docs/04-advanced/01-observability.md +645 -0
  26. package/docs/04-advanced/02-token-tracking.md +469 -0
  27. package/docs/04-advanced/03-streaming.md +476 -0
  28. package/docs/04-advanced/04-guardrails.md +597 -0
  29. package/docs/05-reference/01-api-reference.md +1403 -0
  30. package/docs/05-reference/02-customization.md +646 -0
  31. package/docs/05-reference/03-examples.md +881 -0
  32. package/docs/index.md +85 -0
  33. package/hooks/observability/useMetrics.ts +31 -0
  34. package/hooks/observability/useTraceDetail.ts +48 -0
  35. package/hooks/observability/useTraces.ts +59 -0
  36. package/lib/agent-factory.ts +354 -0
  37. package/lib/agent-helpers.ts +201 -0
  38. package/lib/db-memory-store.ts +417 -0
  39. package/lib/graph/index.ts +58 -0
  40. package/lib/graph/nodes/combiner.ts +399 -0
  41. package/lib/graph/nodes/router.ts +440 -0
  42. package/lib/graph/orchestrator-graph.ts +386 -0
  43. package/lib/graph/prompts/combiner.md +131 -0
  44. package/lib/graph/prompts/router.md +193 -0
  45. package/lib/graph/types.ts +365 -0
  46. package/lib/guardrails.ts +230 -0
  47. package/lib/index.ts +44 -0
  48. package/lib/logger.ts +70 -0
  49. package/lib/memory-store.ts +168 -0
  50. package/lib/message-serializer.ts +110 -0
  51. package/lib/prompt-renderer.ts +94 -0
  52. package/lib/providers.ts +226 -0
  53. package/lib/streaming.ts +232 -0
  54. package/lib/token-tracker.ts +298 -0
  55. package/lib/tools-builder.ts +192 -0
  56. package/lib/tracer-callbacks.ts +342 -0
  57. package/lib/tracer.ts +350 -0
  58. package/migrations/001_langchain_memory.sql +83 -0
  59. package/migrations/002_token_usage.sql +127 -0
  60. package/migrations/003_observability.sql +257 -0
  61. package/package.json +28 -0
  62. package/plugin.config.ts +170 -0
  63. package/presets/lib/langchain.config.ts.preset +142 -0
  64. package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
  65. package/presets/templates/sector7/ai-observability/page.tsx +54 -0
  66. package/types/langchain.types.ts +274 -0
  67. package/types/observability.types.ts +270 -0
@@ -0,0 +1,689 @@
1
+ # Tools
2
+
3
+ Tools are how agents interact with your data. This guide covers creating, configuring, and best practices for building agent tools.
4
+
5
+ ## What is a Tool?
6
+
7
+ A tool is a function that an agent can call to perform actions or retrieve data. Tools have:
8
+
9
+ 1. **Name**: Unique identifier for the tool
10
+ 2. **Description**: Explains when to use the tool (read by the LLM)
11
+ 3. **Schema**: Zod schema defining input parameters
12
+ 4. **Function**: Async function that executes the action
13
+
14
+ ```typescript
15
+ interface ToolDefinition<T extends z.ZodObject<any>> {
16
+ name: string
17
+ description: string
18
+ schema: T
19
+ func: (input: z.infer<T>) => Promise<string>
20
+ }
21
+ ```
22
+
23
+ ## Creating Tools
24
+
25
+ ### Basic Tool
26
+
27
+ ```typescript
28
+ import { z } from 'zod'
29
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
30
+
31
+ const listItemsTool: ToolDefinition<typeof schema> = {
32
+ name: 'list_items',
33
+ description: 'List all items for the current user',
34
+ schema: z.object({
35
+ limit: z.number().optional().default(20).describe('Max items to return'),
36
+ }),
37
+ func: async ({ limit }) => {
38
+ const items = await ItemService.list(userId, { limit })
39
+ return JSON.stringify(items)
40
+ },
41
+ }
42
+ ```
43
+
44
+ ### Tool Factory Pattern
45
+
46
+ The recommended pattern is to create a factory function that receives context:
47
+
48
+ ```typescript
49
+ import { z } from 'zod'
50
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
51
+
52
+ interface ToolContext {
53
+ userId: string
54
+ teamId: string
55
+ }
56
+
57
+ export function createItemTools(context: ToolContext): ToolDefinition<any>[] {
58
+ const { userId, teamId } = context
59
+
60
+ return [
61
+ {
62
+ name: 'list_items',
63
+ description: 'List all items for the current user',
64
+ schema: z.object({
65
+ limit: z.number().optional().default(20),
66
+ }),
67
+ func: async ({ limit }) => {
68
+ const items = await ItemService.list(userId, { limit })
69
+ return JSON.stringify(items)
70
+ },
71
+ },
72
+ {
73
+ name: 'create_item',
74
+ description: 'Create a new item',
75
+ schema: z.object({
76
+ name: z.string().describe('Item name'),
77
+ description: z.string().optional().describe('Item description'),
78
+ }),
79
+ func: async (data) => {
80
+ const item = await ItemService.create(userId, { ...data, teamId })
81
+ return JSON.stringify(item)
82
+ },
83
+ },
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## Zod Schema Best Practices
89
+
90
+ ### Use Descriptive Fields
91
+
92
+ The `.describe()` method helps the LLM understand parameters:
93
+
94
+ ```typescript
95
+ schema: z.object({
96
+ // Good - describes the field purpose
97
+ query: z.string().describe('Search term to match against item names'),
98
+
99
+ // Good - explains valid values
100
+ status: z.enum(['active', 'completed', 'archived'])
101
+ .describe('Filter by item status'),
102
+
103
+ // Good - explains defaults
104
+ limit: z.number().optional().default(20)
105
+ .describe('Maximum items to return (default: 20)'),
106
+ })
107
+ ```
108
+
109
+ ### Required vs Optional Fields
110
+
111
+ ```typescript
112
+ schema: z.object({
113
+ // Required - must be provided
114
+ name: z.string().min(1).describe('Item name (required)'),
115
+
116
+ // Optional with default
117
+ priority: z.enum(['low', 'medium', 'high'])
118
+ .optional()
119
+ .default('medium')
120
+ .describe('Priority level'),
121
+
122
+ // Optional, truly optional
123
+ notes: z.string().optional().describe('Additional notes'),
124
+ })
125
+ ```
126
+
127
+ ### Common Schema Patterns
128
+
129
+ ```typescript
130
+ // Pagination
131
+ z.object({
132
+ limit: z.number().optional().default(20),
133
+ offset: z.number().optional().default(0),
134
+ })
135
+
136
+ // Search
137
+ z.object({
138
+ query: z.string().min(1).describe('Search query'),
139
+ limit: z.number().optional().default(10),
140
+ })
141
+
142
+ // Entity ID
143
+ z.object({
144
+ entityId: z.string().describe('The unique identifier of the entity'),
145
+ })
146
+
147
+ // Date handling
148
+ z.object({
149
+ dueDate: z.string().optional().describe('Due date in ISO format (YYYY-MM-DD)'),
150
+ })
151
+
152
+ // Arrays
153
+ z.object({
154
+ tags: z.array(z.string()).optional().describe('List of tags'),
155
+ days: z.array(z.enum(['mon', 'tue', 'wed', 'thu', 'fri']))
156
+ .optional()
157
+ .describe('Days of the week'),
158
+ })
159
+ ```
160
+
161
+ ## Real-World Examples
162
+
163
+ ### Task Tools
164
+
165
+ ```typescript
166
+ // tools/tasks.ts
167
+ import { z } from 'zod'
168
+ import { TasksService } from '@/themes/default/entities/tasks/tasks.service'
169
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
170
+
171
+ interface TaskToolContext {
172
+ userId: string
173
+ teamId: string
174
+ }
175
+
176
+ export function createTaskTools(context: TaskToolContext): ToolDefinition<any>[] {
177
+ const { userId, teamId } = context
178
+
179
+ return [
180
+ {
181
+ name: 'list_tasks',
182
+ description: 'List tasks with optional filtering by status or priority.',
183
+ schema: z.object({
184
+ status: z.enum(['todo', 'in-progress', 'review', 'done', 'blocked'])
185
+ .optional()
186
+ .describe('Filter by task status'),
187
+ priority: z.enum(['low', 'medium', 'high', 'urgent'])
188
+ .optional()
189
+ .describe('Filter by priority level'),
190
+ }),
191
+ func: async (params) => {
192
+ try {
193
+ const result = await TasksService.list(userId, params)
194
+ return JSON.stringify(result.tasks.map(t => ({
195
+ id: t.id,
196
+ title: t.title,
197
+ status: t.status,
198
+ priority: t.priority,
199
+ dueDate: t.dueDate,
200
+ })), null, 2)
201
+ } catch (error) {
202
+ return `Error listing tasks: ${error instanceof Error ? error.message : 'Unknown'}`
203
+ }
204
+ },
205
+ },
206
+ {
207
+ name: 'search_tasks',
208
+ description: 'Search tasks by keyword in title or description.',
209
+ schema: z.object({
210
+ query: z.string().describe('Search term'),
211
+ }),
212
+ func: async ({ query }) => {
213
+ try {
214
+ const result = await TasksService.list(userId, {})
215
+ const filtered = result.tasks.filter(t =>
216
+ t.title.toLowerCase().includes(query.toLowerCase()) ||
217
+ t.description?.toLowerCase().includes(query.toLowerCase())
218
+ )
219
+ return JSON.stringify(filtered.map(t => ({
220
+ id: t.id,
221
+ title: t.title,
222
+ status: t.status,
223
+ })), null, 2)
224
+ } catch (error) {
225
+ return `Error searching tasks: ${error instanceof Error ? error.message : 'Unknown'}`
226
+ }
227
+ },
228
+ },
229
+ {
230
+ name: 'get_task_details',
231
+ description: 'Get full details of a specific task by ID.',
232
+ schema: z.object({
233
+ taskId: z.string().describe('The task ID to retrieve'),
234
+ }),
235
+ func: async ({ taskId }) => {
236
+ try {
237
+ const task = await TasksService.getById(taskId, userId)
238
+ if (!task) {
239
+ return JSON.stringify({ error: 'Task not found' })
240
+ }
241
+ return JSON.stringify(task, null, 2)
242
+ } catch (error) {
243
+ return `Error getting task: ${error instanceof Error ? error.message : 'Unknown'}`
244
+ }
245
+ },
246
+ },
247
+ {
248
+ name: 'create_task',
249
+ description: 'Create a new task with title and optional details.',
250
+ schema: z.object({
251
+ title: z.string().min(1).describe('Task title (required)'),
252
+ description: z.string().optional().describe('Task description'),
253
+ priority: z.enum(['low', 'medium', 'high'])
254
+ .optional()
255
+ .default('medium')
256
+ .describe('Priority level'),
257
+ dueDate: z.string().optional().describe('Due date (ISO format)'),
258
+ }),
259
+ func: async (data) => {
260
+ try {
261
+ const task = await TasksService.create(userId, {
262
+ ...data,
263
+ teamId,
264
+ status: 'todo',
265
+ })
266
+ return JSON.stringify({
267
+ success: true,
268
+ task,
269
+ message: `Task created: ${task.title}`,
270
+ link: `/dashboard/tasks/${task.id}`,
271
+ }, null, 2)
272
+ } catch (error) {
273
+ return `Error creating task: ${error instanceof Error ? error.message : 'Unknown'}`
274
+ }
275
+ },
276
+ },
277
+ {
278
+ name: 'update_task',
279
+ description: 'Update an existing task. Only specify fields you want to change.',
280
+ schema: z.object({
281
+ taskId: z.string().describe('The task ID to update'),
282
+ title: z.string().optional().describe('New title'),
283
+ description: z.string().optional().describe('New description'),
284
+ status: z.enum(['todo', 'in-progress', 'review', 'done', 'blocked']).optional(),
285
+ priority: z.enum(['low', 'medium', 'high']).optional(),
286
+ dueDate: z.string().optional(),
287
+ }),
288
+ func: async ({ taskId, ...updates }) => {
289
+ try {
290
+ const task = await TasksService.update(userId, taskId, updates)
291
+ return JSON.stringify({
292
+ success: true,
293
+ task,
294
+ message: `Task updated: ${task.title}`,
295
+ link: `/dashboard/tasks/${task.id}`,
296
+ }, null, 2)
297
+ } catch (error) {
298
+ return `Error updating task: ${error instanceof Error ? error.message : 'Unknown'}`
299
+ }
300
+ },
301
+ },
302
+ ]
303
+ }
304
+ ```
305
+
306
+ ### Customer Tools
307
+
308
+ ```typescript
309
+ // tools/customers.ts
310
+ import { z } from 'zod'
311
+ import { CustomersService } from '@/themes/default/entities/customers/customers.service'
312
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
313
+
314
+ export function createCustomerTools(context: { userId: string; teamId: string }): ToolDefinition<any>[] {
315
+ const { userId, teamId } = context
316
+
317
+ return [
318
+ {
319
+ name: 'list_customers',
320
+ description: 'List all customers with optional pagination and sorting.',
321
+ schema: z.object({
322
+ limit: z.number().optional().default(20).describe('Max customers'),
323
+ offset: z.number().optional().default(0).describe('Pagination offset'),
324
+ orderBy: z.enum(['name', 'account', 'office', 'salesRep', 'createdAt'])
325
+ .optional()
326
+ .describe('Sort field'),
327
+ orderDir: z.enum(['asc', 'desc']).optional().describe('Sort direction'),
328
+ }),
329
+ func: async (params) => {
330
+ try {
331
+ const result = await CustomersService.list(userId, params)
332
+ return JSON.stringify({
333
+ customers: result.customers.map(c => ({
334
+ id: c.id,
335
+ name: c.name,
336
+ account: c.account,
337
+ office: c.office,
338
+ phone: c.phone,
339
+ })),
340
+ total: result.total,
341
+ }, null, 2)
342
+ } catch (error) {
343
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
344
+ }
345
+ },
346
+ },
347
+ {
348
+ name: 'search_customers',
349
+ description: 'Search customers by name, account, office, or sales rep.',
350
+ schema: z.object({
351
+ query: z.string().describe('Search term'),
352
+ limit: z.number().optional().default(10),
353
+ }),
354
+ func: async (params) => {
355
+ try {
356
+ const results = await CustomersService.search(userId, params)
357
+ return JSON.stringify(results.map(c => ({
358
+ id: c.id,
359
+ name: c.name,
360
+ account: c.account,
361
+ office: c.office,
362
+ phone: c.phone,
363
+ })), null, 2)
364
+ } catch (error) {
365
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
366
+ }
367
+ },
368
+ },
369
+ {
370
+ name: 'get_customer',
371
+ description: 'Get full details of a specific customer by ID.',
372
+ schema: z.object({
373
+ customerId: z.string().describe('The customer ID to retrieve'),
374
+ }),
375
+ func: async ({ customerId }) => {
376
+ try {
377
+ const customer = await CustomersService.getById(customerId, userId)
378
+ if (!customer) {
379
+ return JSON.stringify({ error: 'Customer not found' })
380
+ }
381
+ return JSON.stringify(customer, null, 2)
382
+ } catch (error) {
383
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
384
+ }
385
+ },
386
+ },
387
+ {
388
+ name: 'create_customer',
389
+ description: 'Create a new customer. Name, account, and office are required.',
390
+ schema: z.object({
391
+ name: z.string().describe('Customer or company name'),
392
+ account: z.number().describe('Unique account number'),
393
+ office: z.string().describe('Office location or branch'),
394
+ phone: z.string().optional().describe('Contact phone number'),
395
+ salesRep: z.string().optional().describe('Assigned sales representative'),
396
+ visitDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
397
+ .optional()
398
+ .describe('Days for in-person visits'),
399
+ contactDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
400
+ .optional()
401
+ .describe('Days for phone/email contact'),
402
+ }),
403
+ func: async (data) => {
404
+ try {
405
+ const customer = await CustomersService.create(userId, {
406
+ ...data,
407
+ teamId,
408
+ })
409
+ return JSON.stringify({
410
+ success: true,
411
+ customer,
412
+ link: `/dashboard/customers/${customer.id}`,
413
+ }, null, 2)
414
+ } catch (error) {
415
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
416
+ }
417
+ },
418
+ },
419
+ {
420
+ name: 'update_customer',
421
+ description: 'Update a customer. Only specify fields to change.',
422
+ schema: z.object({
423
+ customerId: z.string().describe('The customer ID to update'),
424
+ name: z.string().optional(),
425
+ phone: z.string().optional(),
426
+ office: z.string().optional(),
427
+ salesRep: z.string().optional(),
428
+ visitDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
429
+ .optional()
430
+ .describe('New visit days'),
431
+ contactDays: z.array(z.enum(['lun', 'mar', 'mie', 'jue', 'vie']))
432
+ .optional()
433
+ .describe('New contact days'),
434
+ }),
435
+ func: async ({ customerId, ...updates }) => {
436
+ try {
437
+ const customer = await CustomersService.update(userId, customerId, updates)
438
+ return JSON.stringify({
439
+ success: true,
440
+ customer,
441
+ link: `/dashboard/customers/${customer.id}`,
442
+ }, null, 2)
443
+ } catch (error) {
444
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
445
+ }
446
+ },
447
+ },
448
+ {
449
+ name: 'delete_customer',
450
+ description: 'Delete a customer permanently. This action cannot be undone.',
451
+ schema: z.object({
452
+ customerId: z.string().describe('The customer ID to delete'),
453
+ }),
454
+ func: async ({ customerId }) => {
455
+ try {
456
+ const success = await CustomersService.delete(userId, customerId)
457
+ return JSON.stringify({
458
+ success,
459
+ message: success ? 'Customer deleted successfully' : 'Failed to delete customer',
460
+ })
461
+ } catch (error) {
462
+ return `Error: ${error instanceof Error ? error.message : 'Unknown'}`
463
+ }
464
+ },
465
+ },
466
+ ]
467
+ }
468
+ ```
469
+
470
+ ## Orchestrator Tools
471
+
472
+ Orchestrator tools are special - they route requests to other agents:
473
+
474
+ ```typescript
475
+ // tools/orchestrator.ts
476
+ import { z } from 'zod'
477
+ import type { ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
478
+
479
+ export interface RoutingResult {
480
+ agent: 'task' | 'customer' | 'page'
481
+ message: string
482
+ }
483
+
484
+ export interface ClarificationResult {
485
+ action: 'clarify'
486
+ question: string
487
+ options: Array<{ label: string; description: string }>
488
+ }
489
+
490
+ export function createOrchestratorTools(): ToolDefinition<any>[] {
491
+ return [
492
+ {
493
+ name: 'route_to_task',
494
+ description: 'Route to task agent for task-related requests.',
495
+ schema: z.object({
496
+ message: z.string().describe('The user message to forward'),
497
+ }),
498
+ func: async ({ message }) => {
499
+ return JSON.stringify({ agent: 'task', message } as RoutingResult)
500
+ },
501
+ },
502
+ {
503
+ name: 'route_to_customer',
504
+ description: 'Route to customer agent for customer-related requests.',
505
+ schema: z.object({
506
+ message: z.string().describe('The user message to forward'),
507
+ }),
508
+ func: async ({ message }) => {
509
+ return JSON.stringify({ agent: 'customer', message } as RoutingResult)
510
+ },
511
+ },
512
+ {
513
+ name: 'route_to_page',
514
+ description: 'Route to page agent for content/page requests.',
515
+ schema: z.object({
516
+ message: z.string().describe('The user message to forward'),
517
+ }),
518
+ func: async ({ message }) => {
519
+ return JSON.stringify({ agent: 'page', message } as RoutingResult)
520
+ },
521
+ },
522
+ {
523
+ name: 'ask_clarification',
524
+ description: 'Ask user for clarification when intent is unclear.',
525
+ schema: z.object({
526
+ question: z.string().describe('The clarifying question'),
527
+ options: z.array(z.object({
528
+ label: z.string(),
529
+ description: z.string(),
530
+ })).describe('Options for the user'),
531
+ }),
532
+ func: async ({ question, options }) => {
533
+ return JSON.stringify({
534
+ action: 'clarify',
535
+ question,
536
+ options,
537
+ } as ClarificationResult)
538
+ },
539
+ },
540
+ ]
541
+ }
542
+ ```
543
+
544
+ ## Building Tools with buildTools
545
+
546
+ The plugin provides a `buildTools` helper:
547
+
548
+ ```typescript
549
+ import { buildTools, type ToolDefinition } from '@/contents/plugins/langchain/lib/tools-builder'
550
+
551
+ const toolDefinitions: ToolDefinition<any>[] = [
552
+ { name: 'tool1', schema: z.object({}), ... },
553
+ { name: 'tool2', schema: z.object({}), ... },
554
+ ]
555
+
556
+ // Convert to LangChain DynamicStructuredTool instances
557
+ const langChainTools = buildTools(toolDefinitions)
558
+ ```
559
+
560
+ ## Tool Response Format
561
+
562
+ Tools should return JSON strings:
563
+
564
+ ```typescript
565
+ // Good - structured response
566
+ func: async () => {
567
+ const data = await Service.getData()
568
+ return JSON.stringify(data, null, 2)
569
+ }
570
+
571
+ // Good - with metadata
572
+ func: async () => {
573
+ const item = await Service.create(...)
574
+ return JSON.stringify({
575
+ success: true,
576
+ item,
577
+ message: 'Created successfully',
578
+ link: `/items/${item.id}`,
579
+ }, null, 2)
580
+ }
581
+
582
+ // Good - error handling
583
+ func: async () => {
584
+ try {
585
+ const data = await Service.getData()
586
+ return JSON.stringify(data)
587
+ } catch (error) {
588
+ return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
589
+ }
590
+ }
591
+ ```
592
+
593
+ ## Error Handling
594
+
595
+ Always handle errors gracefully:
596
+
597
+ ```typescript
598
+ {
599
+ name: 'risky_operation',
600
+ schema: z.object({ id: z.string() }),
601
+ func: async ({ id }) => {
602
+ try {
603
+ const result = await Service.riskyOperation(id)
604
+ return JSON.stringify({ success: true, result })
605
+ } catch (error) {
606
+ // Return error as string - agent can inform user
607
+ if (error instanceof NotFoundError) {
608
+ return JSON.stringify({ error: 'Item not found', id })
609
+ }
610
+ if (error instanceof PermissionError) {
611
+ return JSON.stringify({ error: 'Access denied' })
612
+ }
613
+ return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
614
+ }
615
+ },
616
+ }
617
+ ```
618
+
619
+ ## Security Considerations
620
+
621
+ ### Always Use Context
622
+
623
+ Tools receive context with user/team IDs - use them:
624
+
625
+ ```typescript
626
+ createTools: (context) => [
627
+ {
628
+ func: async (params) => {
629
+ // Use context.userId for RLS
630
+ const data = await Service.list(context.userId, params)
631
+ return JSON.stringify(data)
632
+ },
633
+ },
634
+ ]
635
+ ```
636
+
637
+ ### Never Expose Internal IDs
638
+
639
+ Return only necessary fields:
640
+
641
+ ```typescript
642
+ // Bad - exposes internal data
643
+ return JSON.stringify(fullDatabaseRecord)
644
+
645
+ // Good - curated response
646
+ return JSON.stringify({
647
+ id: record.id,
648
+ name: record.name,
649
+ // Exclude: internalId, secrets, etc.
650
+ })
651
+ ```
652
+
653
+ ### Validate Before Destructive Actions
654
+
655
+ ```typescript
656
+ {
657
+ name: 'delete_item',
658
+ description: 'Delete an item. This action cannot be undone.',
659
+ schema: z.object({
660
+ itemId: z.string(),
661
+ confirm: z.boolean().describe('Must be true to confirm deletion'),
662
+ }),
663
+ func: async ({ itemId, confirm }) => {
664
+ if (!confirm) {
665
+ return JSON.stringify({
666
+ error: 'Deletion not confirmed',
667
+ message: 'Set confirm=true to proceed',
668
+ })
669
+ }
670
+ await Service.delete(itemId)
671
+ return JSON.stringify({ success: true, deleted: itemId })
672
+ },
673
+ }
674
+ ```
675
+
676
+ ## Using Presets
677
+
678
+ The plugin includes a tools preset:
679
+
680
+ ```bash
681
+ cp contents/plugins/langchain/presets/lib/tools/entity-tools.ts.preset \
682
+ contents/themes/your-theme/lib/langchain/tools/my-entity.ts
683
+ ```
684
+
685
+ ## Next Steps
686
+
687
+ - [Set up graph orchestration](../03-orchestration/01-graph-orchestrator.md) (recommended)
688
+ - [Advanced customization](../05-reference/02-customization.md)
689
+ - [Configure observability](../04-advanced/01-observability.md)