@elizaos/plugin-form 2.0.0-alpha.1 → 2.0.0-alpha.2
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/LICENSE +1 -1
- package/dist/index.d.ts +2 -175
- package/package.json +30 -23
- package/README.md +0 -846
- package/dist/actions/restore.d.ts +0 -62
- package/dist/actions/restore.d.ts.map +0 -1
- package/dist/builder.d.ts +0 -320
- package/dist/builder.d.ts.map +0 -1
- package/dist/builtins.d.ts +0 -128
- package/dist/builtins.d.ts.map +0 -1
- package/dist/defaults.d.ts +0 -95
- package/dist/defaults.d.ts.map +0 -1
- package/dist/evaluators/extractor.d.ts +0 -91
- package/dist/evaluators/extractor.d.ts.map +0 -1
- package/dist/extraction.d.ts +0 -105
- package/dist/extraction.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3306
- package/dist/index.js.map +0 -30
- package/dist/intent.d.ts +0 -116
- package/dist/intent.d.ts.map +0 -1
- package/dist/providers/context.d.ts +0 -69
- package/dist/providers/context.d.ts.map +0 -1
- package/dist/service.d.ts +0 -417
- package/dist/service.d.ts.map +0 -1
- package/dist/storage.d.ts +0 -228
- package/dist/storage.d.ts.map +0 -1
- package/dist/tasks/nudge.d.ts +0 -89
- package/dist/tasks/nudge.d.ts.map +0 -1
- package/dist/template.d.ts +0 -10
- package/dist/template.d.ts.map +0 -1
- package/dist/ttl.d.ts +0 -144
- package/dist/ttl.d.ts.map +0 -1
- package/dist/types.d.ts +0 -1214
- package/dist/types.d.ts.map +0 -1
- package/dist/validation.d.ts +0 -156
- package/dist/validation.d.ts.map +0 -1
package/README.md
DELETED
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
# @elizaos/plugin-form
|
|
2
|
-
|
|
3
|
-
**Guardrails for agent-guided user journeys.**
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
**Architecture & Design:** Odilitime
|
|
8
|
-
**Copyright © 2025 Odilitime.** Licensed under MIT.
|
|
9
|
-
See [Credits & Architecture](#credits--architecture) section below.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## The Insight
|
|
14
|
-
|
|
15
|
-
Agents are powerful but unpredictable. Without structure, they wander. They forget what they're collecting. They miss required information. They can't reliably guide users to outcomes.
|
|
16
|
-
|
|
17
|
-
**Forms are guardrails.** They give agents:
|
|
18
|
-
|
|
19
|
-
- **A path to follow** - structured steps toward an outcome
|
|
20
|
-
- **Conventions to honor** - required fields, validation rules, confirmation flows
|
|
21
|
-
- **Memory of progress** - what's been collected, what's missing, what needs confirmation
|
|
22
|
-
- **Recovery from interruption** - stash, restore, pick up where you left off
|
|
23
|
-
|
|
24
|
-
This plugin turns agents into **reliable process guides** that can shepherd users through registrations, orders, applications, onboarding flows - any structured journey to an outcome.
|
|
25
|
-
|
|
26
|
-
## How It Works
|
|
27
|
-
|
|
28
|
-
The user talks naturally. The agent stays on track:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
User: "I'm John Smith, 28 years old, and you can reach me at john@example.com"
|
|
32
|
-
Agent: "Got it! I have your name as John Smith, age 28, and email john@example.com.
|
|
33
|
-
I still need your username. What would you like to use?"
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The agent knows:
|
|
37
|
-
- ✅ What's been collected (name, age, email)
|
|
38
|
-
- ❌ What's still needed (username)
|
|
39
|
-
- 🎯 What to ask next
|
|
40
|
-
- 🛤️ How to get to the outcome (account creation)
|
|
41
|
-
|
|
42
|
-
## Use Cases: User Journeys
|
|
43
|
-
|
|
44
|
-
Forms enable any structured journey:
|
|
45
|
-
|
|
46
|
-
| Journey | Outcome | Guardrails Needed |
|
|
47
|
-
|---------|---------|-------------------|
|
|
48
|
-
| **User Registration** | Account created | Email, username, age verification |
|
|
49
|
-
| **Product Order** | Order placed | Product selection, billing, payment |
|
|
50
|
-
| **Support Ticket** | Issue logged | Category, description, contact info |
|
|
51
|
-
| **KYC/Onboarding** | User verified | Identity docs, address, compliance |
|
|
52
|
-
| **Booking/Reservation** | Appointment set | Date, time, preferences, contact |
|
|
53
|
-
| **Application** | Application submitted | Personal info, qualifications, documents |
|
|
54
|
-
| **Feedback Collection** | Insights captured | Rating, comments, follow-up consent |
|
|
55
|
-
|
|
56
|
-
Each journey has required stops. Forms ensure the agent visits them all.
|
|
57
|
-
|
|
58
|
-
## Features
|
|
59
|
-
|
|
60
|
-
- 🛤️ **Journey Guardrails** - Agents follow structured paths to outcomes
|
|
61
|
-
- 🗣️ **Natural Language** - Users talk naturally, agents extract structure
|
|
62
|
-
- 🔄 **Two-Tier Intent** - Fast English keywords + LLM fallback for any language
|
|
63
|
-
- ✨ **UX Magic** - Undo, skip, explain, example, progress, autofill
|
|
64
|
-
- ⏰ **Smart TTL** - Retention scales with user effort invested
|
|
65
|
-
- 🔌 **Extensible Types** - Register custom validators for your domain
|
|
66
|
-
- 📦 **Hooks System** - React to journey lifecycle events
|
|
67
|
-
- 💾 **Stash/Restore** - Pause journeys, resume later
|
|
68
|
-
|
|
69
|
-
## Installation
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
bun add @elizaos/plugin-form
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Quick Start
|
|
76
|
-
|
|
77
|
-
### 1. Add plugin to your agent
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
import { formPlugin } from '@elizaos/plugin-form';
|
|
81
|
-
|
|
82
|
-
const character = {
|
|
83
|
-
// ... your character config
|
|
84
|
-
plugins: [formPlugin],
|
|
85
|
-
};
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### 2. Define a form
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { Form, C, FormService } from '@elizaos/plugin-form';
|
|
92
|
-
|
|
93
|
-
const registrationForm = Form.create('registration')
|
|
94
|
-
.name('User Registration')
|
|
95
|
-
.description('Create your account')
|
|
96
|
-
.control(
|
|
97
|
-
C.email('email')
|
|
98
|
-
.required()
|
|
99
|
-
.ask('What email should we use for your account?')
|
|
100
|
-
.example('user@example.com')
|
|
101
|
-
)
|
|
102
|
-
.control(
|
|
103
|
-
C.text('name')
|
|
104
|
-
.required()
|
|
105
|
-
.ask("What's your name?")
|
|
106
|
-
)
|
|
107
|
-
.control(
|
|
108
|
-
C.number('age')
|
|
109
|
-
.min(13)
|
|
110
|
-
.ask('How old are you?')
|
|
111
|
-
)
|
|
112
|
-
.onSubmit('handle_registration')
|
|
113
|
-
.build();
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### 3. Register the form in your plugin
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import type { Plugin, IAgentRuntime } from '@elizaos/core';
|
|
120
|
-
import { FormService } from '@elizaos/plugin-form';
|
|
121
|
-
|
|
122
|
-
export const myPlugin: Plugin = {
|
|
123
|
-
name: 'my-plugin',
|
|
124
|
-
dependencies: ['form'], // Depend on form plugin
|
|
125
|
-
|
|
126
|
-
async init(runtime: IAgentRuntime) {
|
|
127
|
-
const formService = runtime.getService('FORM') as FormService;
|
|
128
|
-
formService.registerForm(registrationForm);
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### 4. Start a form session
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
// In an action or service:
|
|
137
|
-
const formService = runtime.getService('FORM') as FormService;
|
|
138
|
-
await formService.startSession('registration', entityId, roomId);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 5. Handle submissions
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
runtime.registerTaskWorker({
|
|
145
|
-
name: 'handle_registration',
|
|
146
|
-
validate: async () => true,
|
|
147
|
-
execute: async (runtime, options) => {
|
|
148
|
-
const { submission } = options;
|
|
149
|
-
const { email, name, age } = submission.values;
|
|
150
|
-
|
|
151
|
-
// Create user account, send welcome email, etc.
|
|
152
|
-
console.log(`New user: ${name} (${email}), age ${age}`);
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## User Experience
|
|
158
|
-
|
|
159
|
-
Once a form is active, users can interact naturally:
|
|
160
|
-
|
|
161
|
-
| User Says | What Happens |
|
|
162
|
-
|-----------|--------------|
|
|
163
|
-
| "I'm John, 25 years old" | Extracts name=John, age=25 |
|
|
164
|
-
| "john@example.com" | Extracts email |
|
|
165
|
-
| "done" / "submit" | Submits the form |
|
|
166
|
-
| "save for later" | Stashes form, can switch topics |
|
|
167
|
-
| "resume my form" | Restores stashed form |
|
|
168
|
-
| "cancel" / "nevermind" | Abandons form |
|
|
169
|
-
| "undo" / "go back" | Reverts last change |
|
|
170
|
-
| "skip" | Skips optional field |
|
|
171
|
-
| "why?" | Explains why field is needed |
|
|
172
|
-
| "example?" | Shows example value |
|
|
173
|
-
| "how far?" | Shows completion progress |
|
|
174
|
-
| "same as last time" | Applies autofill |
|
|
175
|
-
|
|
176
|
-
## API Reference
|
|
177
|
-
|
|
178
|
-
### FormBuilder
|
|
179
|
-
|
|
180
|
-
Create forms with the fluent builder API:
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
import { Form, C } from '@elizaos/plugin-form';
|
|
184
|
-
|
|
185
|
-
const form = Form.create('contact')
|
|
186
|
-
// Metadata
|
|
187
|
-
.name('Contact Form')
|
|
188
|
-
.description('Get in touch with us')
|
|
189
|
-
.version(1)
|
|
190
|
-
|
|
191
|
-
// Controls
|
|
192
|
-
.control(C.email('email').required())
|
|
193
|
-
.control(C.text('message').required().maxLength(1000))
|
|
194
|
-
.control(C.select('department', [
|
|
195
|
-
{ value: 'sales', label: 'Sales' },
|
|
196
|
-
{ value: 'support', label: 'Support' },
|
|
197
|
-
]))
|
|
198
|
-
|
|
199
|
-
// Permissions
|
|
200
|
-
.roles('user', 'admin')
|
|
201
|
-
.allowMultiple()
|
|
202
|
-
|
|
203
|
-
// UX options
|
|
204
|
-
.noUndo() // Disable undo
|
|
205
|
-
.noSkip() // Disable skip
|
|
206
|
-
.maxUndoSteps(3) // Limit undo history
|
|
207
|
-
|
|
208
|
-
// TTL (retention)
|
|
209
|
-
.ttl({ minDays: 7, maxDays: 30, effortMultiplier: 0.5 })
|
|
210
|
-
|
|
211
|
-
// Nudge (reminders)
|
|
212
|
-
.nudgeAfter(24) // Hours before nudge
|
|
213
|
-
.nudgeMessage('You have an unfinished contact form...')
|
|
214
|
-
|
|
215
|
-
// Hooks
|
|
216
|
-
.onStart('on_contact_start')
|
|
217
|
-
.onFieldChange('on_contact_field_change')
|
|
218
|
-
.onReady('on_contact_ready')
|
|
219
|
-
.onSubmit('handle_contact_submission')
|
|
220
|
-
.onCancel('on_contact_cancel')
|
|
221
|
-
|
|
222
|
-
// Debug
|
|
223
|
-
.debug() // Enable extraction logging
|
|
224
|
-
|
|
225
|
-
.build();
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### ControlBuilder
|
|
229
|
-
|
|
230
|
-
Create field controls:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { C } from '@elizaos/plugin-form';
|
|
234
|
-
|
|
235
|
-
// Basic types
|
|
236
|
-
C.text('name')
|
|
237
|
-
C.email('email')
|
|
238
|
-
C.number('age')
|
|
239
|
-
C.boolean('subscribe')
|
|
240
|
-
C.date('birthdate')
|
|
241
|
-
C.file('resume')
|
|
242
|
-
C.select('country', [{ value: 'us', label: 'United States' }])
|
|
243
|
-
|
|
244
|
-
// Validation
|
|
245
|
-
C.text('username')
|
|
246
|
-
.required()
|
|
247
|
-
.minLength(3)
|
|
248
|
-
.maxLength(20)
|
|
249
|
-
.pattern('^[a-z0-9_]+$')
|
|
250
|
-
|
|
251
|
-
C.number('quantity')
|
|
252
|
-
.min(1)
|
|
253
|
-
.max(100)
|
|
254
|
-
|
|
255
|
-
// Agent hints
|
|
256
|
-
C.text('company')
|
|
257
|
-
.label('Company Name')
|
|
258
|
-
.description('The company you work for')
|
|
259
|
-
.ask('What company do you work for?')
|
|
260
|
-
.hint('employer', 'organization', 'workplace')
|
|
261
|
-
.example('Acme Corp')
|
|
262
|
-
.confirmThreshold(0.9) // Require high confidence
|
|
263
|
-
|
|
264
|
-
// Behavior
|
|
265
|
-
C.text('password')
|
|
266
|
-
.required()
|
|
267
|
-
.sensitive() // Don't echo back
|
|
268
|
-
|
|
269
|
-
C.text('internalId')
|
|
270
|
-
.hidden() // Extract silently, never ask
|
|
271
|
-
|
|
272
|
-
C.text('role')
|
|
273
|
-
.readonly() // Can't change after set
|
|
274
|
-
|
|
275
|
-
// File uploads
|
|
276
|
-
C.file('document')
|
|
277
|
-
.accept(['application/pdf', 'image/*'])
|
|
278
|
-
.maxSize(5 * 1024 * 1024) // 5MB
|
|
279
|
-
.maxFiles(3)
|
|
280
|
-
|
|
281
|
-
// Access control
|
|
282
|
-
C.number('discount')
|
|
283
|
-
.roles('admin', 'sales')
|
|
284
|
-
|
|
285
|
-
// Conditional fields
|
|
286
|
-
C.text('state')
|
|
287
|
-
.dependsOn('country', 'equals', 'us')
|
|
288
|
-
|
|
289
|
-
// Database mapping
|
|
290
|
-
C.text('firstName')
|
|
291
|
-
.dbbind('first_name')
|
|
292
|
-
|
|
293
|
-
// UI hints (for future GUIs)
|
|
294
|
-
C.text('bio')
|
|
295
|
-
.section('Profile')
|
|
296
|
-
.order(1)
|
|
297
|
-
.placeholder('Tell us about yourself...')
|
|
298
|
-
.widget('textarea')
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### FormService
|
|
302
|
-
|
|
303
|
-
The main service for managing forms:
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
const formService = runtime.getService('FORM') as FormService;
|
|
307
|
-
|
|
308
|
-
// Form definitions
|
|
309
|
-
formService.registerForm(form);
|
|
310
|
-
formService.getForm('form-id');
|
|
311
|
-
formService.listForms();
|
|
312
|
-
|
|
313
|
-
// Sessions
|
|
314
|
-
await formService.startSession('form-id', entityId, roomId);
|
|
315
|
-
await formService.getActiveSession(entityId, roomId);
|
|
316
|
-
await formService.getStashedSessions(entityId);
|
|
317
|
-
|
|
318
|
-
// Field updates
|
|
319
|
-
await formService.updateField(sessionId, entityId, 'email', 'john@example.com', 0.95, 'extraction');
|
|
320
|
-
await formService.undoLastChange(sessionId, entityId);
|
|
321
|
-
await formService.skipField(sessionId, entityId, 'nickname');
|
|
322
|
-
|
|
323
|
-
// Lifecycle
|
|
324
|
-
await formService.submit(sessionId, entityId);
|
|
325
|
-
await formService.stash(sessionId, entityId);
|
|
326
|
-
await formService.restore(sessionId, entityId);
|
|
327
|
-
await formService.cancel(sessionId, entityId);
|
|
328
|
-
|
|
329
|
-
// Autofill
|
|
330
|
-
await formService.applyAutofill(session);
|
|
331
|
-
|
|
332
|
-
// Context
|
|
333
|
-
formService.getSessionContext(session);
|
|
334
|
-
|
|
335
|
-
// Custom types
|
|
336
|
-
formService.registerType('phone', {
|
|
337
|
-
validate: (value) => ({
|
|
338
|
-
valid: /^\+?[\d\s-()]+$/.test(String(value)),
|
|
339
|
-
error: 'Invalid phone number',
|
|
340
|
-
}),
|
|
341
|
-
extractionPrompt: 'a phone number (digits, spaces, dashes allowed)',
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Hooks
|
|
346
|
-
|
|
347
|
-
React to form lifecycle events by registering task workers:
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
// Called when session starts
|
|
351
|
-
runtime.registerTaskWorker({
|
|
352
|
-
name: 'on_registration_start',
|
|
353
|
-
execute: async (runtime, { session, form }) => {
|
|
354
|
-
// Log, initialize context, etc.
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// Called when any field changes
|
|
359
|
-
runtime.registerTaskWorker({
|
|
360
|
-
name: 'on_registration_field_change',
|
|
361
|
-
execute: async (runtime, { session, field, value, oldValue }) => {
|
|
362
|
-
// Validate cross-field dependencies, update derived values
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Called when all required fields are filled
|
|
367
|
-
runtime.registerTaskWorker({
|
|
368
|
-
name: 'on_registration_ready',
|
|
369
|
-
execute: async (runtime, { session }) => {
|
|
370
|
-
// Maybe auto-submit, or prepare preview
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// Called on successful submission
|
|
375
|
-
runtime.registerTaskWorker({
|
|
376
|
-
name: 'handle_registration',
|
|
377
|
-
execute: async (runtime, { session, submission }) => {
|
|
378
|
-
const { email, name, age } = submission.values;
|
|
379
|
-
// Create account, send email, etc.
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// Called when user cancels
|
|
384
|
-
runtime.registerTaskWorker({
|
|
385
|
-
name: 'on_registration_cancel',
|
|
386
|
-
execute: async (runtime, { session }) => {
|
|
387
|
-
// Cleanup, log abandonment, etc.
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// Called when session expires
|
|
392
|
-
runtime.registerTaskWorker({
|
|
393
|
-
name: 'on_registration_expire',
|
|
394
|
-
execute: async (runtime, { session }) => {
|
|
395
|
-
// Final cleanup
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
## Custom Field Types
|
|
401
|
-
|
|
402
|
-
Register custom validators for domain-specific types:
|
|
403
|
-
|
|
404
|
-
```typescript
|
|
405
|
-
import { registerTypeHandler } from '@elizaos/plugin-form';
|
|
406
|
-
|
|
407
|
-
// Solana wallet address
|
|
408
|
-
registerTypeHandler('solana_address', {
|
|
409
|
-
validate: (value) => {
|
|
410
|
-
const valid = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(String(value));
|
|
411
|
-
return { valid, error: valid ? undefined : 'Invalid Solana address' };
|
|
412
|
-
},
|
|
413
|
-
parse: (value) => value.trim(),
|
|
414
|
-
format: (value) => `${String(value).slice(0, 4)}...${String(value).slice(-4)}`,
|
|
415
|
-
extractionPrompt: 'a Solana wallet address (Base58 encoded, 32-44 characters)',
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Use in form
|
|
419
|
-
const form = Form.create('wallet')
|
|
420
|
-
.control(
|
|
421
|
-
C.field('walletAddress')
|
|
422
|
-
.type('solana_address')
|
|
423
|
-
.required()
|
|
424
|
-
.label('Wallet Address')
|
|
425
|
-
)
|
|
426
|
-
.build();
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
## Widget Registry (ControlType System)
|
|
430
|
-
|
|
431
|
-
The Widget Registry is a powerful system for creating complex, service-backed control types. It enables plugins to register custom controls that go beyond simple validation.
|
|
432
|
-
|
|
433
|
-
### Three Types of Controls
|
|
434
|
-
|
|
435
|
-
| Type | Description | Example |
|
|
436
|
-
|------|-------------|---------|
|
|
437
|
-
| **Simple** | Validate/parse/format only | text, email, phone |
|
|
438
|
-
| **Composite** | Has subcontrols that roll up | address (street, city, zip) |
|
|
439
|
-
| **External** | Requires async external confirmation | payment, signature |
|
|
440
|
-
|
|
441
|
-
### Registering a Simple Type
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
const formService = runtime.getService('FORM') as FormService;
|
|
445
|
-
|
|
446
|
-
formService.registerControlType({
|
|
447
|
-
id: 'phone',
|
|
448
|
-
validate: (value) => ({
|
|
449
|
-
valid: /^\+?[\d\s-()]+$/.test(String(value)),
|
|
450
|
-
error: 'Invalid phone number',
|
|
451
|
-
}),
|
|
452
|
-
parse: (value) => value.replace(/\s/g, ''),
|
|
453
|
-
format: (value) => String(value),
|
|
454
|
-
extractionPrompt: 'a phone number with optional country code',
|
|
455
|
-
});
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### Registering a Composite Type
|
|
459
|
-
|
|
460
|
-
Composite types have subcontrols that must all be filled:
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
formService.registerControlType({
|
|
464
|
-
id: 'address',
|
|
465
|
-
|
|
466
|
-
// Returns subcontrols for this type
|
|
467
|
-
getSubControls: (control, runtime) => [
|
|
468
|
-
{ key: 'street', type: 'text', label: 'Street', required: true },
|
|
469
|
-
{ key: 'city', type: 'text', label: 'City', required: true },
|
|
470
|
-
{ key: 'state', type: 'text', label: 'State', required: true },
|
|
471
|
-
{ key: 'zip', type: 'text', label: 'ZIP Code', required: true },
|
|
472
|
-
],
|
|
473
|
-
|
|
474
|
-
// Validate the combined address
|
|
475
|
-
validate: (value) => ({ valid: true }),
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// Use in form
|
|
479
|
-
const form = Form.create('shipping')
|
|
480
|
-
.control(C.field('address').type('address').required())
|
|
481
|
-
.build();
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
When users provide address parts, they're extracted as subfields:
|
|
485
|
-
```
|
|
486
|
-
User: "123 Main St, Springfield, IL 62701"
|
|
487
|
-
→ Extracts: address.street, address.city, address.state, address.zip
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
### Registering an External Type
|
|
491
|
-
|
|
492
|
-
External types require async confirmation from outside the conversation (blockchain transactions, signatures, etc.):
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
formService.registerControlType({
|
|
496
|
-
id: 'payment',
|
|
497
|
-
|
|
498
|
-
getSubControls: (control, runtime) => [
|
|
499
|
-
{ key: 'amount', type: 'number', label: 'Amount', required: true },
|
|
500
|
-
{ key: 'currency', type: 'select', label: 'Currency', required: true,
|
|
501
|
-
options: [{ value: 'SOL', label: 'Solana' }, { value: 'ETH', label: 'Ethereum' }]
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
|
|
505
|
-
// Called when all subcontrols are filled
|
|
506
|
-
activate: async (context) => {
|
|
507
|
-
const { session, subValues, runtime } = context;
|
|
508
|
-
const paymentService = runtime.getService('PAYMENT');
|
|
509
|
-
|
|
510
|
-
// Create pending payment, return instructions
|
|
511
|
-
return paymentService.createPendingPayment({
|
|
512
|
-
sessionId: session.id,
|
|
513
|
-
amount: subValues.amount,
|
|
514
|
-
currency: subValues.currency,
|
|
515
|
-
});
|
|
516
|
-
// Returns: { instructions: "Send 0.5 SOL to xyz...", reference: "abc123" }
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
// Called when user cancels
|
|
520
|
-
deactivate: async (context) => {
|
|
521
|
-
const paymentService = context.runtime.getService('PAYMENT');
|
|
522
|
-
await paymentService.cancelPending(context.session.fields.payment?.externalState?.reference);
|
|
523
|
-
},
|
|
524
|
-
|
|
525
|
-
validate: (value) => ({
|
|
526
|
-
valid: !!value?.confirmed,
|
|
527
|
-
error: 'Payment not confirmed',
|
|
528
|
-
}),
|
|
529
|
-
});
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
### External Confirmation Flow
|
|
533
|
-
|
|
534
|
-
External types follow an event-driven confirmation flow:
|
|
535
|
-
|
|
536
|
-
```
|
|
537
|
-
1. User fills subcontrols: "$50 in SOL"
|
|
538
|
-
2. Evaluator detects all subcontrols filled
|
|
539
|
-
3. Evaluator calls formService.activateExternalField()
|
|
540
|
-
4. Widget's activate() creates pending request, returns instructions
|
|
541
|
-
5. Agent shows instructions: "Send 0.5 SOL to xyz..."
|
|
542
|
-
6. (External) User sends blockchain transaction
|
|
543
|
-
7. PaymentService detects tx, calls formService.confirmExternalField()
|
|
544
|
-
8. Field status changes to 'filled'
|
|
545
|
-
9. Agent proceeds with form
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### Confirming External Fields
|
|
549
|
-
|
|
550
|
-
When your service detects the external action completed:
|
|
551
|
-
|
|
552
|
-
```typescript
|
|
553
|
-
// In your payment service
|
|
554
|
-
async handlePaymentReceived(reference: string, txData: any) {
|
|
555
|
-
const pending = this.getPendingByReference(reference);
|
|
556
|
-
|
|
557
|
-
const formService = this.runtime.getService('FORM') as FormService;
|
|
558
|
-
await formService.confirmExternalField(
|
|
559
|
-
pending.sessionId,
|
|
560
|
-
pending.entityId,
|
|
561
|
-
pending.field,
|
|
562
|
-
{ confirmed: true, txId: txData.signature },
|
|
563
|
-
{ blockchain: 'solana', confirmedAt: Date.now() }
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### Widget Events
|
|
569
|
-
|
|
570
|
-
The evaluator emits events as it processes messages:
|
|
571
|
-
|
|
572
|
-
| Event | When | Payload |
|
|
573
|
-
|-------|------|---------|
|
|
574
|
-
| `FORM_FIELD_EXTRACTED` | Simple field value extracted | sessionId, field, value, confidence |
|
|
575
|
-
| `FORM_SUBFIELD_UPDATED` | Composite subfield updated | sessionId, parentField, subField, value |
|
|
576
|
-
| `FORM_SUBCONTROLS_FILLED` | All subcontrols filled | sessionId, field, subValues |
|
|
577
|
-
| `FORM_EXTERNAL_ACTIVATED` | External type activated | sessionId, field, activation |
|
|
578
|
-
| `FORM_FIELD_CONFIRMED` | External field confirmed | sessionId, field, value, externalData |
|
|
579
|
-
| `FORM_FIELD_CANCELLED` | External field cancelled | sessionId, field, reason |
|
|
580
|
-
|
|
581
|
-
Widgets react to events, they don't parse messages.
|
|
582
|
-
|
|
583
|
-
### Built-in Types
|
|
584
|
-
|
|
585
|
-
These types are pre-registered and protected from override:
|
|
586
|
-
|
|
587
|
-
| Type | Validation | Extraction Prompt |
|
|
588
|
-
|------|------------|-------------------|
|
|
589
|
-
| `text` | Pattern, length | "a text string" |
|
|
590
|
-
| `number` | Min/max value | "a number" |
|
|
591
|
-
| `email` | Email format | "an email address" |
|
|
592
|
-
| `boolean` | Yes/no values | "yes/no or true/false" |
|
|
593
|
-
| `select` | Must match option | "one of the options" |
|
|
594
|
-
| `date` | Valid date | "a date in YYYY-MM-DD" |
|
|
595
|
-
| `file` | Size, type limits | "a file attachment" |
|
|
596
|
-
|
|
597
|
-
Override built-in types with caution:
|
|
598
|
-
|
|
599
|
-
```typescript
|
|
600
|
-
formService.registerControlType(myCustomText, { allowOverride: true });
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
## Architecture
|
|
604
|
-
|
|
605
|
-
```
|
|
606
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
607
|
-
│ User Message │
|
|
608
|
-
└────────────────────────────┬────────────────────────────────┘
|
|
609
|
-
│
|
|
610
|
-
▼
|
|
611
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
612
|
-
│ FORM_CONTEXT Provider │
|
|
613
|
-
│ • Runs BEFORE agent responds │
|
|
614
|
-
│ • Injects: progress, filled fields, next field, etc. │
|
|
615
|
-
│ • Shows pending external actions (payments, signatures) │
|
|
616
|
-
│ • Agent knows what to ask │
|
|
617
|
-
└────────────────────────────┬────────────────────────────────┘
|
|
618
|
-
│
|
|
619
|
-
▼
|
|
620
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
621
|
-
│ Agent (REPLY) │
|
|
622
|
-
│ • Uses form context to craft response │
|
|
623
|
-
│ • Asks for next field, confirms uncertain values │
|
|
624
|
-
│ • Shows external action instructions when pending │
|
|
625
|
-
└────────────────────────────┬────────────────────────────────┘
|
|
626
|
-
│
|
|
627
|
-
▼
|
|
628
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
629
|
-
│ form_evaluator │
|
|
630
|
-
│ • Runs AFTER each message │
|
|
631
|
-
│ • Tier 1: Fast English keyword matching │
|
|
632
|
-
│ • Tier 2: LLM extraction for complex/non-English │
|
|
633
|
-
│ • Handles subfields for composite types │
|
|
634
|
-
│ • Emits events (FORM_FIELD_EXTRACTED, etc.) │
|
|
635
|
-
│ • Triggers activation for external types │
|
|
636
|
-
│ • Updates session state │
|
|
637
|
-
└────────────────────────────┬────────────────────────────────┘
|
|
638
|
-
│
|
|
639
|
-
▼
|
|
640
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
641
|
-
│ FormService │
|
|
642
|
-
│ ┌──────────────────────────────────────────────────────┐ │
|
|
643
|
-
│ │ ControlType Registry │ │
|
|
644
|
-
│ │ • Built-in: text, number, email, boolean, etc. │ │
|
|
645
|
-
│ │ • Custom: phone, address, payment, signature │ │
|
|
646
|
-
│ │ • Simple → Composite → External types │ │
|
|
647
|
-
│ └──────────────────────────────────────────────────────┘ │
|
|
648
|
-
│ • Manages form definitions │
|
|
649
|
-
│ • Manages sessions (create, update, stash, restore) │
|
|
650
|
-
│ • Handles subfields for composite types │
|
|
651
|
-
│ • Activates/confirms/cancels external fields │
|
|
652
|
-
│ • Executes lifecycle hooks │
|
|
653
|
-
│ • Handles submissions │
|
|
654
|
-
└─────────────────────────────────────────────────────────────┘
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Widget Registry Flow (External Types)
|
|
658
|
-
|
|
659
|
-
```
|
|
660
|
-
┌──────────────┐ ┌────────────────┐ ┌──────────────────┐
|
|
661
|
-
│ User │ │ Evaluator │ │ FormService │
|
|
662
|
-
│ "$50 SOL" │───▶│ Extract values │───▶│ updateSubField() │
|
|
663
|
-
└──────────────┘ │ Emit events │ │ areSubFieldsFilled? │
|
|
664
|
-
└───────┬────────┘ └────────┬─────────┘
|
|
665
|
-
│ │ Yes
|
|
666
|
-
│ ┌───────▼────────┐
|
|
667
|
-
│ │ activateExternal│
|
|
668
|
-
│ │ Field() │
|
|
669
|
-
│ └───────┬─────────┘
|
|
670
|
-
│ │
|
|
671
|
-
┌───────▼──────────────────────▼─────────┐
|
|
672
|
-
│ ControlType.activate() │
|
|
673
|
-
│ • Creates pending request │
|
|
674
|
-
│ • Returns instructions + reference │
|
|
675
|
-
└───────────────────────────┬────────────┘
|
|
676
|
-
│
|
|
677
|
-
┌───────────────────────────▼────────────┐
|
|
678
|
-
│ Agent Shows Instructions │
|
|
679
|
-
│ "Send 0.5 SOL to xyz... ref: abc123" │
|
|
680
|
-
└────────────────────────────────────────┘
|
|
681
|
-
...
|
|
682
|
-
┌──────────────┐ ┌────────────────┐ ┌──────────────────┐
|
|
683
|
-
│ Blockchain │ │ PaymentService │ │ FormService │
|
|
684
|
-
│ TX detected │───▶│ Match reference│───▶│confirmExternal() │
|
|
685
|
-
└──────────────┘ └────────────────┘ └──────────────────┘
|
|
686
|
-
│
|
|
687
|
-
┌───────────────────────────▼────────────┐
|
|
688
|
-
│ Field marked 'filled' │
|
|
689
|
-
│ Form continues... │
|
|
690
|
-
└────────────────────────────────────────┘
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
## Smart TTL
|
|
694
|
-
|
|
695
|
-
Form sessions are retained based on user effort:
|
|
696
|
-
|
|
697
|
-
| Time Spent | Retention |
|
|
698
|
-
|------------|-----------|
|
|
699
|
-
| 0-5 min | 14 days (minimum) |
|
|
700
|
-
| 30 min | ~15 days |
|
|
701
|
-
| 2 hours | ~60 days |
|
|
702
|
-
| 4+ hours | 90 days (maximum) |
|
|
703
|
-
|
|
704
|
-
Configure per-form:
|
|
705
|
-
|
|
706
|
-
```typescript
|
|
707
|
-
Form.create('important-form')
|
|
708
|
-
.ttl({
|
|
709
|
-
minDays: 30, // Always keep at least 30 days
|
|
710
|
-
maxDays: 180, // Never more than 180 days
|
|
711
|
-
effortMultiplier: 1, // 1 day per minute of effort
|
|
712
|
-
})
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
## Debugging
|
|
716
|
-
|
|
717
|
-
Enable debug mode to see extraction reasoning:
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
Form.create('debug-form')
|
|
721
|
-
.debug() // Logs LLM extraction reasoning
|
|
722
|
-
.build();
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
---
|
|
726
|
-
|
|
727
|
-
## Credits & Architecture
|
|
728
|
-
|
|
729
|
-
### Design & Architecture
|
|
730
|
-
|
|
731
|
-
**Plugin-Form** was architected and designed by **Odilitime**.
|
|
732
|
-
|
|
733
|
-
#### Core Insight
|
|
734
|
-
|
|
735
|
-
**Forms as Agent Guardrails (2025)**
|
|
736
|
-
|
|
737
|
-
The foundational insight: forms aren't about data collection—they're about **keeping agents on track**. Without structure, agents wander, forget context, and fail to guide users to outcomes. This plugin provides the rails that make agents reliable process guides.
|
|
738
|
-
|
|
739
|
-
#### Core Innovations
|
|
740
|
-
|
|
741
|
-
**Agent-Native Form Architecture**
|
|
742
|
-
- Designed the evaluator-driven extraction pattern (vs action-per-field)
|
|
743
|
-
- Created the provider-based context injection for agent awareness
|
|
744
|
-
- Invented two-tier intent detection (fast English + LLM fallback)
|
|
745
|
-
- Result: Agents naturally guide users through journeys without explicit commands
|
|
746
|
-
|
|
747
|
-
**Conversational UX System**
|
|
748
|
-
- Designed natural language "UX magic" commands (undo, skip, explain, example, progress)
|
|
749
|
-
- Created confidence-based confirmation flow for uncertain extractions
|
|
750
|
-
- Invented the stash/restore pattern for journey interruption
|
|
751
|
-
- Result: Users interact naturally, agents stay on track
|
|
752
|
-
|
|
753
|
-
**Effort-Based TTL System**
|
|
754
|
-
- Designed smart retention that scales with user investment
|
|
755
|
-
- Created the effort tracking model (time spent, interaction count)
|
|
756
|
-
- Invented nudge system for stale journeys
|
|
757
|
-
- Result: Respects user work while managing storage
|
|
758
|
-
|
|
759
|
-
**Fluent Builder API**
|
|
760
|
-
- Designed type-safe form definition syntax
|
|
761
|
-
- Created the ControlBuilder/FormBuilder pattern
|
|
762
|
-
- Invented shorthand factories (C.email, C.number, etc.)
|
|
763
|
-
- Result: Forms are easy to define, hard to misconfigure
|
|
764
|
-
|
|
765
|
-
**Extensible Type System**
|
|
766
|
-
- Designed pluggable type handler registry
|
|
767
|
-
- Created validation/parsing/formatting/extraction pipeline per type
|
|
768
|
-
- Enabled domain-specific types (blockchain addresses, phone numbers)
|
|
769
|
-
- Result: Any domain can extend the form system
|
|
770
|
-
|
|
771
|
-
**Widget Registry (ControlType System)**
|
|
772
|
-
- Designed unified ControlType interface for simple, composite, and external types
|
|
773
|
-
- Created subfield extraction pattern for composite types
|
|
774
|
-
- Invented external type activation/confirmation flow for async processes
|
|
775
|
-
- Event-driven architecture: evaluator emits, widgets react
|
|
776
|
-
- Override protection for built-in types
|
|
777
|
-
- Result: Plugins can register complex, service-backed controls (payments, signatures)
|
|
778
|
-
|
|
779
|
-
#### Technical Architecture
|
|
780
|
-
|
|
781
|
-
- Evaluator + Provider pattern (not action-per-intent)
|
|
782
|
-
- Two-tier intent detection (regex fast path, LLM fallback)
|
|
783
|
-
- Component-based storage (room-scoped sessions)
|
|
784
|
-
- Confidence-scored extraction with confirmation flow
|
|
785
|
-
- Field-level history for undo functionality
|
|
786
|
-
- Hook system for consuming plugin integration
|
|
787
|
-
- Smart TTL with effort multipliers
|
|
788
|
-
- Nudge task worker for stale session recovery
|
|
789
|
-
- Widget registry with ControlType interface (simple → composite → external)
|
|
790
|
-
- Subfield state tracking for composite types
|
|
791
|
-
- External state machine (pending → confirmed/failed/expired)
|
|
792
|
-
- Event emission for reactive widget architecture
|
|
793
|
-
|
|
794
|
-
#### Philosophy & Principles
|
|
795
|
-
|
|
796
|
-
- "Forms are guardrails, not data collectors"
|
|
797
|
-
- "Agents follow paths, users reach outcomes"
|
|
798
|
-
- "Fast path first, LLM when needed"
|
|
799
|
-
- "Respect user effort in retention"
|
|
800
|
-
- "Extensibility through types, not code changes"
|
|
801
|
-
- "Provider gives context, agent handles conversation"
|
|
802
|
-
|
|
803
|
-
---
|
|
804
|
-
|
|
805
|
-
### Documentation & Knowledge Transfer
|
|
806
|
-
|
|
807
|
-
**All documentation authored by Odilitime:**
|
|
808
|
-
- Comprehensive README with architecture diagrams
|
|
809
|
-
- WHY comments throughout codebase explaining design decisions
|
|
810
|
-
- Module-level documentation on every file
|
|
811
|
-
- Four complete example plugins demonstrating patterns
|
|
812
|
-
- Type definitions with extensive JSDoc
|
|
813
|
-
|
|
814
|
-
**Code Quality:**
|
|
815
|
-
- Full TypeScript with strict typing
|
|
816
|
-
- Fluent builder API with method chaining
|
|
817
|
-
- Comprehensive defaults system
|
|
818
|
-
- Zero build errors
|
|
819
|
-
- Production-ready foundation
|
|
820
|
-
|
|
821
|
-
---
|
|
822
|
-
|
|
823
|
-
### License
|
|
824
|
-
|
|
825
|
-
Copyright © 2025 Odilitime
|
|
826
|
-
|
|
827
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
828
|
-
|
|
829
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
830
|
-
|
|
831
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
832
|
-
|
|
833
|
-
---
|
|
834
|
-
|
|
835
|
-
### Recognition
|
|
836
|
-
|
|
837
|
-
**If this plugin makes your agent-guided journeys reliable, consider:**
|
|
838
|
-
- ⭐ Starring the repository
|
|
839
|
-
- 🗣️ Sharing your implementation patterns
|
|
840
|
-
- 💬 Contributing improvements back to the community
|
|
841
|
-
- 🙏 Acknowledging Odilitime's architectural work
|
|
842
|
-
|
|
843
|
-
---
|
|
844
|
-
|
|
845
|
-
**Designed and built by Odilitime. Making agents reliable guides through user journeys.**
|
|
846
|
-
|