@elizaos/plugin-form 2.0.0-alpha.10 → 2.0.0-alpha.11

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/dist/index.js CHANGED
@@ -1,2661 +1,64 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
11
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
12
-
13
- // src/types.ts
14
- var FORM_CONTROL_DEFAULTS, FORM_DEFINITION_DEFAULTS, FORM_SESSION_COMPONENT = "form_session", FORM_SUBMISSION_COMPONENT = "form_submission", FORM_AUTOFILL_COMPONENT = "form_autofill";
15
- var init_types = __esm(() => {
16
- FORM_CONTROL_DEFAULTS = {
17
- type: "text",
18
- required: false,
19
- confirmThreshold: 0.8
20
- };
21
- FORM_DEFINITION_DEFAULTS = {
22
- version: 1,
23
- status: "active",
24
- ux: {
25
- allowUndo: true,
26
- allowSkip: true,
27
- maxUndoSteps: 5,
28
- showExamples: true,
29
- showExplanations: true,
30
- allowAutofill: true
31
- },
32
- ttl: {
33
- minDays: 14,
34
- maxDays: 90,
35
- effortMultiplier: 0.5
36
- },
37
- nudge: {
38
- enabled: true,
39
- afterInactiveHours: 48,
40
- maxNudges: 3
41
- },
42
- debug: false
43
- };
44
- });
45
-
46
- // src/builtins.ts
47
- function registerBuiltinTypes(registerFn) {
48
- for (const type of BUILTIN_TYPES) {
49
- registerFn(type);
50
- }
51
- }
52
- function getBuiltinType(id) {
53
- return BUILTIN_TYPE_MAP.get(id);
54
- }
55
- function isBuiltinType(id) {
56
- return BUILTIN_TYPE_MAP.has(id);
57
- }
58
- var EMAIL_REGEX, ISO_DATE_REGEX, textType, numberType, emailType, booleanType, selectType, dateType, fileType, BUILTIN_TYPES, BUILTIN_TYPE_MAP;
59
- var init_builtins = __esm(() => {
60
- EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
61
- ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
62
- textType = {
63
- id: "text",
64
- builtin: true,
65
- validate: (value, control) => {
66
- if (value === null || value === undefined) {
67
- return { valid: true };
68
- }
69
- const str = String(value);
70
- if (control.minLength !== undefined && str.length < control.minLength) {
71
- return {
72
- valid: false,
73
- error: `Must be at least ${control.minLength} characters`
74
- };
75
- }
76
- if (control.maxLength !== undefined && str.length > control.maxLength) {
77
- return {
78
- valid: false,
79
- error: `Must be at most ${control.maxLength} characters`
80
- };
81
- }
82
- if (control.pattern) {
83
- const regex = new RegExp(control.pattern);
84
- if (!regex.test(str)) {
85
- return { valid: false, error: "Invalid format" };
86
- }
87
- }
88
- if (control.enum && !control.enum.includes(str)) {
89
- return {
90
- valid: false,
91
- error: `Must be one of: ${control.enum.join(", ")}`
92
- };
93
- }
94
- return { valid: true };
95
- },
96
- parse: (value) => String(value).trim(),
97
- format: (value) => String(value ?? ""),
98
- extractionPrompt: "a text string"
99
- };
100
- numberType = {
101
- id: "number",
102
- builtin: true,
103
- validate: (value, control) => {
104
- if (value === null || value === undefined || value === "") {
105
- return { valid: true };
106
- }
107
- const num = typeof value === "number" ? value : parseFloat(String(value));
108
- if (Number.isNaN(num)) {
109
- return { valid: false, error: "Must be a valid number" };
110
- }
111
- if (control.min !== undefined && num < control.min) {
112
- return { valid: false, error: `Must be at least ${control.min}` };
113
- }
114
- if (control.max !== undefined && num > control.max) {
115
- return { valid: false, error: `Must be at most ${control.max}` };
116
- }
117
- return { valid: true };
118
- },
119
- parse: (value) => {
120
- const cleaned = value.replace(/[,$\s]/g, "");
121
- return parseFloat(cleaned);
122
- },
123
- format: (value) => {
124
- if (value === null || value === undefined)
125
- return "";
126
- const num = typeof value === "number" ? value : parseFloat(String(value));
127
- if (Number.isNaN(num))
128
- return String(value);
129
- return num.toLocaleString();
130
- },
131
- extractionPrompt: "a number (integer or decimal)"
132
- };
133
- emailType = {
134
- id: "email",
135
- builtin: true,
136
- validate: (value) => {
137
- if (value === null || value === undefined || value === "") {
138
- return { valid: true };
139
- }
140
- const str = String(value).trim().toLowerCase();
141
- if (!EMAIL_REGEX.test(str)) {
142
- return { valid: false, error: "Invalid email format" };
143
- }
144
- return { valid: true };
145
- },
146
- parse: (value) => value.trim().toLowerCase(),
147
- format: (value) => String(value ?? "").toLowerCase(),
148
- extractionPrompt: "an email address (e.g., user@example.com)"
149
- };
150
- booleanType = {
151
- id: "boolean",
152
- builtin: true,
153
- validate: (value) => {
154
- if (value === null || value === undefined) {
155
- return { valid: true };
156
- }
157
- if (typeof value === "boolean") {
158
- return { valid: true };
159
- }
160
- const str = String(value).toLowerCase();
161
- const validValues = ["true", "false", "yes", "no", "1", "0", "on", "off"];
162
- if (!validValues.includes(str)) {
163
- return { valid: false, error: "Must be yes/no or true/false" };
164
- }
165
- return { valid: true };
166
- },
167
- parse: (value) => {
168
- const str = value.toLowerCase();
169
- return ["true", "yes", "1", "on"].includes(str);
170
- },
171
- format: (value) => {
172
- if (value === true)
173
- return "Yes";
174
- if (value === false)
175
- return "No";
176
- return String(value ?? "");
177
- },
178
- extractionPrompt: "a yes/no or true/false value"
179
- };
180
- selectType = {
181
- id: "select",
182
- builtin: true,
183
- validate: (value, control) => {
184
- if (value === null || value === undefined || value === "") {
185
- return { valid: true };
186
- }
187
- const str = String(value);
188
- if (control.options) {
189
- const validValues = control.options.map((o) => o.value);
190
- if (!validValues.includes(str)) {
191
- const labels = control.options.map((o) => o.label).join(", ");
192
- return { valid: false, error: `Must be one of: ${labels}` };
193
- }
194
- }
195
- if (control.enum && !control.options) {
196
- if (!control.enum.includes(str)) {
197
- return {
198
- valid: false,
199
- error: `Must be one of: ${control.enum.join(", ")}`
200
- };
201
- }
202
- }
203
- return { valid: true };
204
- },
205
- parse: (value) => value.trim(),
206
- format: (value) => String(value ?? ""),
207
- extractionPrompt: "one of the available options"
208
- };
209
- dateType = {
210
- id: "date",
211
- builtin: true,
212
- validate: (value) => {
213
- if (value === null || value === undefined || value === "") {
214
- return { valid: true };
215
- }
216
- const str = String(value);
217
- if (!ISO_DATE_REGEX.test(str)) {
218
- return { valid: false, error: "Must be in YYYY-MM-DD format" };
219
- }
220
- const date = new Date(str);
221
- if (Number.isNaN(date.getTime())) {
222
- return { valid: false, error: "Invalid date" };
223
- }
224
- return { valid: true };
225
- },
226
- parse: (value) => {
227
- const date = new Date(value);
228
- if (!Number.isNaN(date.getTime())) {
229
- return date.toISOString().split("T")[0];
230
- }
231
- return value.trim();
232
- },
233
- format: (value) => {
234
- if (!value)
235
- return "";
236
- const date = new Date(String(value));
237
- if (Number.isNaN(date.getTime()))
238
- return String(value);
239
- return date.toLocaleDateString();
240
- },
241
- extractionPrompt: "a date (preferably in YYYY-MM-DD format)"
242
- };
243
- fileType = {
244
- id: "file",
245
- builtin: true,
246
- validate: (value, _control) => {
247
- if (value === null || value === undefined) {
248
- return { valid: true };
249
- }
250
- if (typeof value === "object") {
251
- return { valid: true };
252
- }
253
- return { valid: false, error: "Invalid file data" };
254
- },
255
- format: (value) => {
256
- if (!value)
257
- return "";
258
- if (Array.isArray(value)) {
259
- return `${value.length} file(s)`;
260
- }
261
- if (typeof value === "object" && value !== null && "name" in value) {
262
- return String(value.name);
263
- }
264
- return "File attached";
265
- },
266
- extractionPrompt: "a file attachment (upload required)"
267
- };
268
- BUILTIN_TYPES = [
269
- textType,
270
- numberType,
271
- emailType,
272
- booleanType,
273
- selectType,
274
- dateType,
275
- fileType
276
- ];
277
- BUILTIN_TYPE_MAP = new Map(BUILTIN_TYPES.map((t) => [t.id, t]));
278
- });
279
-
280
- // src/validation.ts
281
- function registerTypeHandler(type, handler) {
282
- typeHandlers.set(type, handler);
283
- }
284
- function getTypeHandler(type) {
285
- return typeHandlers.get(type);
286
- }
287
- function clearTypeHandlers() {
288
- typeHandlers.clear();
289
- }
290
- function validateField(value, control) {
291
- if (control.required) {
292
- if (value === undefined || value === null || value === "") {
293
- return {
294
- valid: false,
295
- error: `${control.label || control.key} is required`
296
- };
297
- }
298
- }
299
- if (value === undefined || value === null || value === "") {
300
- return { valid: true };
301
- }
302
- const handler = typeHandlers.get(control.type);
303
- if (handler?.validate) {
304
- const result = handler.validate(value, control);
305
- if (!result.valid) {
306
- return result;
307
- }
308
- }
309
- switch (control.type) {
310
- case "email":
311
- return validateEmail(value, control);
312
- case "number":
313
- return validateNumber(value, control);
314
- case "boolean":
315
- return validateBoolean(value, control);
316
- case "date":
317
- return validateDate(value, control);
318
- case "select":
319
- return validateSelect(value, control);
320
- case "file":
321
- return validateFile(value, control);
322
- default:
323
- return validateText(value, control);
324
- }
325
- }
326
- function validateText(value, control) {
327
- const strValue = String(value);
328
- if (control.pattern) {
329
- const regex = new RegExp(control.pattern);
330
- if (!regex.test(strValue)) {
331
- return {
332
- valid: false,
333
- error: `${control.label || control.key} has invalid format`
334
- };
335
- }
336
- }
337
- if (control.minLength !== undefined && strValue.length < control.minLength) {
338
- return {
339
- valid: false,
340
- error: `${control.label || control.key} must be at least ${control.minLength} characters`
341
- };
342
- }
343
- if (control.maxLength !== undefined && strValue.length > control.maxLength) {
344
- return {
345
- valid: false,
346
- error: `${control.label || control.key} must be at most ${control.maxLength} characters`
347
- };
348
- }
349
- if (control.enum && control.enum.length > 0) {
350
- if (!control.enum.includes(strValue)) {
351
- return {
352
- valid: false,
353
- error: `${control.label || control.key} must be one of: ${control.enum.join(", ")}`
354
- };
355
- }
356
- }
357
- return { valid: true };
358
- }
359
- function validateEmail(value, control) {
360
- const strValue = String(value);
361
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
362
- if (!emailRegex.test(strValue)) {
363
- return {
364
- valid: false,
365
- error: `${control.label || control.key} must be a valid email address`
366
- };
367
- }
368
- return validateText(value, control);
369
- }
370
- function validateNumber(value, control) {
371
- const numValue = typeof value === "number" ? value : parseFloat(String(value).replace(/[,$]/g, ""));
372
- if (Number.isNaN(numValue)) {
373
- return {
374
- valid: false,
375
- error: `${control.label || control.key} must be a number`
376
- };
377
- }
378
- if (control.min !== undefined && numValue < control.min) {
379
- return {
380
- valid: false,
381
- error: `${control.label || control.key} must be at least ${control.min}`
382
- };
383
- }
384
- if (control.max !== undefined && numValue > control.max) {
385
- return {
386
- valid: false,
387
- error: `${control.label || control.key} must be at most ${control.max}`
388
- };
389
- }
390
- return { valid: true };
391
- }
392
- function validateBoolean(value, _control) {
393
- if (typeof value === "boolean") {
394
- return { valid: true };
395
- }
396
- const strValue = String(value).toLowerCase();
397
- const truthy = ["true", "yes", "1", "on"];
398
- const falsy = ["false", "no", "0", "off"];
399
- if (truthy.includes(strValue) || falsy.includes(strValue)) {
400
- return { valid: true };
401
- }
402
- return { valid: false, error: "Must be true or false" };
403
- }
404
- function validateDate(value, control) {
405
- let dateValue;
406
- if (value instanceof Date) {
407
- dateValue = value;
408
- } else if (typeof value === "string" || typeof value === "number") {
409
- dateValue = new Date(value);
410
- } else {
411
- return {
412
- valid: false,
413
- error: `${control.label || control.key} must be a valid date`
414
- };
415
- }
416
- if (Number.isNaN(dateValue.getTime())) {
417
- return {
418
- valid: false,
419
- error: `${control.label || control.key} must be a valid date`
420
- };
421
- }
422
- if (control.min !== undefined && dateValue.getTime() < control.min) {
423
- return {
424
- valid: false,
425
- error: `${control.label || control.key} is too early`
426
- };
427
- }
428
- if (control.max !== undefined && dateValue.getTime() > control.max) {
429
- return {
430
- valid: false,
431
- error: `${control.label || control.key} is too late`
432
- };
433
- }
434
- return { valid: true };
435
- }
436
- function validateSelect(value, control) {
437
- if (!control.options || control.options.length === 0) {
438
- return { valid: true };
439
- }
440
- const strValue = String(value);
441
- const validValues = control.options.map((opt) => opt.value);
442
- if (!validValues.includes(strValue)) {
443
- return {
444
- valid: false,
445
- error: `${control.label || control.key} must be one of the available options`
446
- };
447
- }
448
- return { valid: true };
449
- }
450
- function validateFile(value, control) {
451
- if (!control.file) {
452
- return { valid: true };
453
- }
454
- const files = Array.isArray(value) ? value : [value];
455
- if (control.file.maxFiles && files.length > control.file.maxFiles) {
456
- return {
457
- valid: false,
458
- error: `Maximum ${control.file.maxFiles} files allowed`
459
- };
460
- }
461
- for (const file of files) {
462
- if (!file || typeof file !== "object")
463
- continue;
464
- const fileObj = file;
465
- if (control.file.maxSize && fileObj.size && fileObj.size > control.file.maxSize) {
466
- return {
467
- valid: false,
468
- error: `File size exceeds maximum of ${formatBytes(control.file.maxSize)}`
469
- };
470
- }
471
- if (control.file.accept && fileObj.mimeType) {
472
- const accepted = control.file.accept.some((pattern) => matchesMimeType(fileObj.mimeType, pattern));
473
- if (!accepted) {
474
- return {
475
- valid: false,
476
- error: `File type ${fileObj.mimeType} is not accepted`
477
- };
478
- }
479
- }
480
- }
481
- return { valid: true };
482
- }
483
- function matchesMimeType(mimeType, pattern) {
484
- if (pattern === "*/*")
485
- return true;
486
- if (pattern.endsWith("/*")) {
487
- const prefix = pattern.slice(0, -1);
488
- return mimeType.startsWith(prefix);
489
- }
490
- return mimeType === pattern;
491
- }
492
- function formatBytes(bytes) {
493
- if (bytes < 1024)
494
- return `${bytes} B`;
495
- if (bytes < 1024 * 1024)
496
- return `${(bytes / 1024).toFixed(1)} KB`;
497
- if (bytes < 1024 * 1024 * 1024)
498
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
499
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
500
- }
501
- function parseValue(value, control) {
502
- const handler = typeHandlers.get(control.type);
503
- if (handler?.parse) {
504
- return handler.parse(value);
505
- }
506
- switch (control.type) {
507
- case "number":
508
- return parseFloat(value.replace(/[,$]/g, ""));
509
- case "boolean": {
510
- const lower = value.toLowerCase();
511
- return ["true", "yes", "1", "on"].includes(lower);
512
- }
513
- case "date": {
514
- const timestamp = Date.parse(value);
515
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : value;
516
- }
517
- default:
518
- return value;
519
- }
520
- }
521
- function formatValue(value, control) {
522
- if (value === undefined || value === null)
523
- return "";
524
- const handler = typeHandlers.get(control.type);
525
- if (handler?.format) {
526
- return handler.format(value);
527
- }
528
- if (control.sensitive) {
529
- const strVal = String(value);
530
- if (strVal.length > 8) {
531
- return `${strVal.slice(0, 4)}...${strVal.slice(-4)}`;
532
- }
533
- return "****";
534
- }
535
- switch (control.type) {
536
- case "number":
537
- return typeof value === "number" ? value.toLocaleString() : String(value);
538
- case "boolean":
539
- return value ? "Yes" : "No";
540
- case "date":
541
- return value instanceof Date ? value.toLocaleDateString() : String(value);
542
- case "select":
543
- if (control.options) {
544
- const option = control.options.find((opt) => opt.value === String(value));
545
- if (option)
546
- return option.label;
547
- }
548
- return String(value);
549
- case "file":
550
- if (Array.isArray(value)) {
551
- return value.map((f) => f.name || "file").join(", ");
552
- }
553
- return value.name || "file";
554
- default:
555
- return String(value);
556
- }
557
- }
558
- var typeHandlers;
559
- var init_validation = __esm(() => {
560
- typeHandlers = new Map;
561
- });
562
-
563
- // src/intent.ts
564
- function quickIntentDetect(text) {
565
- const lower = text.toLowerCase().trim();
566
- if (lower.length < 2) {
567
- return null;
568
- }
569
- if (/\b(resume|continue|pick up where|go back to|get back to)\b/.test(lower)) {
570
- return "restore";
571
- }
572
- if (/\b(submit|done|finish|send it|that'?s all|i'?m done|complete|all set)\b/.test(lower)) {
573
- return "submit";
574
- }
575
- if (/\b(save|stash|later|hold on|pause|save for later|come back|save this)\b/.test(lower)) {
576
- if (!/\b(save and submit|save and send)\b/.test(lower)) {
577
- return "stash";
578
- }
579
- }
580
- if (/\b(cancel|abort|nevermind|never mind|forget it|stop|quit|exit)\b/.test(lower)) {
581
- return "cancel";
582
- }
583
- if (/\b(undo|go back|wait no|change that|oops|that'?s wrong|wrong|not right)\b/.test(lower)) {
584
- return "undo";
585
- }
586
- if (/\b(skip|pass|don'?t know|next one|next|don'?t have|no idea)\b/.test(lower)) {
587
- if (!/\bskip to\b/.test(lower)) {
588
- return "skip";
589
- }
590
- }
591
- if (/\b(why|what'?s that for|explain|what do you mean|what is|purpose|reason)\b\??$/i.test(lower)) {
592
- return "explain";
593
- }
594
- if (/^why\??$/i.test(lower)) {
595
- return "explain";
596
- }
597
- if (/\b(example|like what|show me|such as|for instance|sample)\b\??$/i.test(lower)) {
598
- return "example";
599
- }
600
- if (/^(example|e\.?g\.?)\??$/i.test(lower)) {
601
- return "example";
602
- }
603
- if (/\b(how far|how many left|progress|status|how much more|where are we)\b/.test(lower)) {
604
- return "progress";
605
- }
606
- if (/\b(same as|last time|use my usual|like before|previous|from before)\b/.test(lower)) {
607
- return "autofill";
608
- }
609
- return null;
610
- }
611
- function isLifecycleIntent(intent) {
612
- return ["submit", "stash", "restore", "cancel"].includes(intent);
613
- }
614
- function isUXIntent(intent) {
615
- return ["undo", "skip", "explain", "example", "progress", "autofill"].includes(intent);
616
- }
617
- function hasDataToExtract(intent) {
618
- return intent === "fill_form" || intent === "other";
619
- }
620
-
621
- // node_modules/uuid/dist-node/stringify.js
622
- function unsafeStringify(arr, offset = 0) {
623
- return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
624
- }
625
- var byteToHex;
626
- var init_stringify = __esm(() => {
627
- byteToHex = [];
628
- for (let i = 0;i < 256; ++i) {
629
- byteToHex.push((i + 256).toString(16).slice(1));
630
- }
631
- });
632
-
633
- // node_modules/uuid/dist-node/rng.js
634
- import { randomFillSync } from "node:crypto";
635
- function rng() {
636
- if (poolPtr > rnds8Pool.length - 16) {
637
- randomFillSync(rnds8Pool);
638
- poolPtr = 0;
639
- }
640
- return rnds8Pool.slice(poolPtr, poolPtr += 16);
641
- }
642
- var rnds8Pool, poolPtr;
643
- var init_rng = __esm(() => {
644
- rnds8Pool = new Uint8Array(256);
645
- poolPtr = rnds8Pool.length;
646
- });
647
-
648
- // node_modules/uuid/dist-node/native.js
649
- import { randomUUID } from "node:crypto";
650
- var native_default;
651
- var init_native = __esm(() => {
652
- native_default = { randomUUID };
653
- });
654
-
655
- // node_modules/uuid/dist-node/v4.js
656
- function _v4(options, buf, offset) {
657
- options = options || {};
658
- const rnds = options.random ?? options.rng?.() ?? rng();
659
- if (rnds.length < 16) {
660
- throw new Error("Random bytes length must be >= 16");
661
- }
662
- rnds[6] = rnds[6] & 15 | 64;
663
- rnds[8] = rnds[8] & 63 | 128;
664
- if (buf) {
665
- offset = offset || 0;
666
- if (offset < 0 || offset + 16 > buf.length) {
667
- throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
668
- }
669
- for (let i = 0;i < 16; ++i) {
670
- buf[offset + i] = rnds[i];
671
- }
672
- return buf;
673
- }
674
- return unsafeStringify(rnds);
675
- }
676
- function v4(options, buf, offset) {
677
- if (native_default.randomUUID && !buf && !options) {
678
- return native_default.randomUUID();
679
- }
680
- return _v4(options, buf, offset);
681
- }
682
- var v4_default;
683
- var init_v4 = __esm(() => {
684
- init_native();
685
- init_rng();
686
- init_stringify();
687
- v4_default = v4;
688
- });
689
-
690
- // node_modules/uuid/dist-node/index.js
691
- var init_dist_node = __esm(() => {
692
- init_v4();
693
- });
694
-
695
- // src/storage.ts
696
- async function getActiveSession(runtime, entityId, roomId) {
697
- const component = await runtime.getComponent(entityId, `${FORM_SESSION_COMPONENT}:${roomId}`);
698
- if (!component?.data || !isFormSession(component.data))
699
- return null;
700
- const session = component.data;
701
- if (session.status === "active" || session.status === "ready") {
702
- return session;
703
- }
704
- return null;
705
- }
706
- async function getAllActiveSessions(runtime, entityId) {
707
- const components = await runtime.getComponents(entityId);
708
- const sessions = [];
709
- for (const component of components) {
710
- if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
711
- if (component.data && isFormSession(component.data)) {
712
- const session = component.data;
713
- if (session.status === "active" || session.status === "ready") {
714
- sessions.push(session);
715
- }
716
- }
717
- }
718
- }
719
- return sessions;
720
- }
721
- async function getStashedSessions(runtime, entityId) {
722
- const components = await runtime.getComponents(entityId);
723
- const sessions = [];
724
- for (const component of components) {
725
- if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
726
- if (component.data && isFormSession(component.data)) {
727
- const session = component.data;
728
- if (session.status === "stashed") {
729
- sessions.push(session);
730
- }
731
- }
732
- }
733
- }
734
- return sessions;
735
- }
736
- async function getSessionById(runtime, entityId, sessionId) {
737
- const components = await runtime.getComponents(entityId);
738
- for (const component of components) {
739
- if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
740
- if (component.data && isFormSession(component.data)) {
741
- const session = component.data;
742
- if (session.id === sessionId) {
743
- return session;
744
- }
745
- }
746
- }
747
- }
748
- return null;
749
- }
750
- async function saveSession(runtime, session) {
751
- const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
752
- const existing = await runtime.getComponent(session.entityId, componentType);
753
- const context = await resolveComponentContext(runtime, session.roomId);
754
- const resolvedWorldId = existing?.worldId ?? context.worldId;
755
- const component = {
756
- id: existing?.id || v4_default(),
757
- entityId: session.entityId,
758
- agentId: runtime.agentId,
759
- roomId: session.roomId,
760
- worldId: resolvedWorldId,
761
- sourceEntityId: runtime.agentId,
762
- type: componentType,
763
- createdAt: existing?.createdAt || Date.now(),
764
- data: JSON.parse(JSON.stringify(session))
765
- };
766
- if (existing) {
767
- await runtime.updateComponent(component);
768
- } else {
769
- await runtime.createComponent(component);
770
- }
771
- }
772
- async function deleteSession(runtime, session) {
773
- const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
774
- const existing = await runtime.getComponent(session.entityId, componentType);
775
- if (existing) {
776
- await runtime.deleteComponent(existing.id);
777
- }
778
- }
779
- async function saveSubmission(runtime, submission) {
780
- const componentType = `${FORM_SUBMISSION_COMPONENT}:${submission.formId}:${submission.id}`;
781
- const context = await resolveComponentContext(runtime);
782
- const component = {
783
- id: v4_default(),
784
- entityId: submission.entityId,
785
- agentId: runtime.agentId,
786
- roomId: context.roomId,
787
- worldId: context.worldId,
788
- sourceEntityId: runtime.agentId,
789
- type: componentType,
790
- createdAt: submission.submittedAt,
791
- data: JSON.parse(JSON.stringify(submission))
792
- };
793
- await runtime.createComponent(component);
794
- }
795
- async function getSubmissions(runtime, entityId, formId) {
796
- const components = await runtime.getComponents(entityId);
797
- const submissions = [];
798
- const prefix = formId ? `${FORM_SUBMISSION_COMPONENT}:${formId}:` : `${FORM_SUBMISSION_COMPONENT}:`;
799
- for (const component of components) {
800
- if (component.type.startsWith(prefix)) {
801
- if (component.data && isFormSubmission(component.data)) {
802
- submissions.push(component.data);
803
- }
804
- }
805
- }
806
- submissions.sort((a, b) => b.submittedAt - a.submittedAt);
807
- return submissions;
808
- }
809
- async function getAutofillData(runtime, entityId, formId) {
810
- const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
811
- const component = await runtime.getComponent(entityId, componentType);
812
- if (!component?.data || !isFormAutofillData(component.data))
813
- return null;
814
- return component.data;
815
- }
816
- async function saveAutofillData(runtime, entityId, formId, values) {
817
- const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
818
- const existing = await runtime.getComponent(entityId, componentType);
819
- const context = await resolveComponentContext(runtime);
820
- const resolvedWorldId = existing?.worldId ?? context.worldId;
821
- const data = {
822
- formId,
823
- values,
824
- updatedAt: Date.now()
825
- };
826
- const component = {
827
- id: existing?.id || v4_default(),
828
- entityId,
829
- agentId: runtime.agentId,
830
- roomId: context.roomId,
831
- worldId: resolvedWorldId,
832
- sourceEntityId: runtime.agentId,
833
- type: componentType,
834
- createdAt: existing?.createdAt || Date.now(),
835
- data: JSON.parse(JSON.stringify(data))
836
- };
837
- if (existing) {
838
- await runtime.updateComponent(component);
839
- } else {
840
- await runtime.createComponent(component);
841
- }
842
- }
843
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), resolveComponentContext = async (runtime, roomId) => {
844
- if (roomId) {
845
- const room = await runtime.getRoom(roomId);
846
- return { roomId, worldId: room?.worldId ?? runtime.agentId };
847
- }
848
- return { roomId: runtime.agentId, worldId: runtime.agentId };
849
- }, isFormSession = (data) => {
850
- if (!isRecord(data))
851
- return false;
852
- return typeof data.id === "string" && typeof data.formId === "string" && typeof data.entityId === "string" && typeof data.roomId === "string";
853
- }, isFormSubmission = (data) => {
854
- if (!isRecord(data))
855
- return false;
856
- return typeof data.id === "string" && typeof data.formId === "string" && typeof data.sessionId === "string" && typeof data.entityId === "string";
857
- }, isFormAutofillData = (data) => {
858
- if (!isRecord(data))
859
- return false;
860
- return typeof data.formId === "string" && typeof data.updatedAt === "number" && typeof data.values === "object";
861
- };
862
- var init_storage = __esm(() => {
863
- init_dist_node();
864
- init_types();
865
- });
866
-
867
- // src/template.ts
868
- function buildTemplateValues(session) {
869
- const values = {};
870
- for (const [key, state] of Object.entries(session.fields)) {
871
- const value = state.value;
872
- if (typeof value === "string") {
873
- values[key] = value;
874
- } else if (typeof value === "number" || typeof value === "boolean") {
875
- values[key] = String(value);
876
- }
877
- }
878
- const context = session.context;
879
- if (context && typeof context === "object" && !Array.isArray(context)) {
880
- for (const [key, value] of Object.entries(context)) {
881
- if (typeof value === "string") {
882
- values[key] = value;
883
- } else if (typeof value === "number" || typeof value === "boolean") {
884
- values[key] = String(value);
885
- }
886
- }
887
- }
888
- return values;
889
- }
890
- function renderTemplate(template, values) {
891
- if (!template) {
892
- return template;
893
- }
894
- return template.replace(TEMPLATE_PATTERN, (match, key) => {
895
- const replacement = values[key];
896
- return replacement !== undefined ? replacement : match;
897
- });
898
- }
899
- function resolveControlTemplates(control, values) {
900
- const resolvedOptions = control.options?.map((option) => ({
901
- ...option,
902
- label: renderTemplate(option.label, values) ?? option.label,
903
- description: renderTemplate(option.description, values)
904
- }));
905
- const resolvedFields = control.fields?.map((field) => resolveControlTemplates(field, values));
906
- return {
907
- ...control,
908
- label: renderTemplate(control.label, values) ?? control.label,
909
- description: renderTemplate(control.description, values),
910
- askPrompt: renderTemplate(control.askPrompt, values),
911
- example: renderTemplate(control.example, values),
912
- extractHints: control.extractHints?.map((hint) => renderTemplate(hint, values) ?? hint),
913
- options: resolvedOptions,
914
- fields: resolvedFields ?? control.fields
915
- };
916
- }
917
- var TEMPLATE_PATTERN;
918
- var init_template = __esm(() => {
919
- TEMPLATE_PATTERN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
920
- });
921
-
922
- // src/extraction.ts
923
- import { ModelType, parseKeyValueXml } from "@elizaos/core";
924
- async function llmIntentAndExtract(runtime, text, form, controls, templateValues) {
925
- const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
926
- const fieldsDescription = resolvedControls.filter((c) => !c.hidden).map((c) => {
927
- const handler = getTypeHandler(c.type);
928
- const typeHint = handler?.extractionPrompt || c.type;
929
- const hints = c.extractHints?.join(", ") || "";
930
- const options = c.options?.map((o) => o.value).join(", ") || "";
931
- return `- ${c.key} (${c.label}): ${c.description || typeHint}${hints ? ` [hints: ${hints}]` : ""}${options ? ` [options: ${options}]` : ""}`;
932
- }).join(`
933
- `);
934
- const prompt = `You are extracting structured data from a user's natural language message.
935
-
936
- FORM: ${form.name}
937
- ${form.description ? `DESCRIPTION: ${form.description}` : ""}
938
-
939
- FIELDS TO EXTRACT:
940
- ${fieldsDescription}
941
-
942
- USER MESSAGE:
943
- "${text}"
944
-
945
- INSTRUCTIONS:
946
- 1. Determine the user's intent:
947
- - fill_form: They are providing information for form fields
948
- - submit: They want to submit/complete the form ("done", "submit", "finish", "that's all")
949
- - stash: They want to save for later ("save for later", "pause", "hold on")
950
- - restore: They want to resume a saved form ("resume", "continue", "pick up where")
951
- - cancel: They want to cancel ("cancel", "abort", "nevermind", "forget it")
952
- - undo: They want to undo last change ("undo", "go back", "wait no")
953
- - skip: They want to skip current field ("skip", "pass", "don't know")
954
- - explain: They want explanation ("why?", "what's that for?")
955
- - example: They want an example ("example?", "like what?")
956
- - progress: They want progress update ("how far?", "status")
957
- - autofill: They want to use saved values ("same as last time")
958
- - other: None of the above
959
-
960
- 2. For fill_form intent, extract all field values mentioned.
961
- - For each extracted value, provide a confidence score (0.0-1.0)
962
- - Note if this appears to be a correction to a previous value
963
-
964
- Respond in this exact XML format:
965
- <response>
966
- <intent>fill_form|submit|stash|restore|cancel|undo|skip|explain|example|progress|autofill|other</intent>
967
- <extractions>
968
- <field>
969
- <key>field_key</key>
970
- <value>extracted_value</value>
971
- <confidence>0.0-1.0</confidence>
972
- <reasoning>why this value was extracted</reasoning>
973
- <is_correction>true|false</is_correction>
974
- </field>
975
- <!-- more fields if applicable -->
976
- </extractions>
977
- </response>`;
978
- try {
979
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
980
- prompt,
981
- temperature: 0.1
982
- });
983
- const parsed = parseExtractionResponse(response);
984
- for (const extraction of parsed.extractions) {
985
- const control = resolvedControls.find((c) => c.key === extraction.field);
986
- if (control) {
987
- if (typeof extraction.value === "string") {
988
- extraction.value = parseValue(extraction.value, control);
989
- }
990
- const validation = validateField(extraction.value, control);
991
- if (!validation.valid) {
992
- extraction.confidence = Math.min(extraction.confidence, 0.3);
993
- extraction.reasoning = `${extraction.reasoning || ""} (Validation failed: ${validation.error})`;
994
- }
995
- }
996
- }
997
- if (form.debug) {
998
- runtime.logger.debug("[FormExtraction] LLM extraction result:", JSON.stringify(parsed));
999
- }
1000
- return parsed;
1001
- } catch (error) {
1002
- runtime.logger.error("[FormExtraction] LLM extraction failed:", String(error));
1003
- return { intent: "other", extractions: [] };
1004
- }
1005
- }
1006
- function parseExtractionResponse(response) {
1007
- const result = {
1008
- intent: "other",
1009
- extractions: []
1010
- };
1011
- try {
1012
- const parsed = parseKeyValueXml(response);
1013
- if (parsed) {
1014
- const intentStr = parsed.intent?.toLowerCase() ?? "other";
1015
- result.intent = isValidIntent(intentStr) ? intentStr : "other";
1016
- if (parsed.extractions) {
1017
- const fields = Array.isArray(parsed.extractions) ? parsed.extractions : parsed.extractions.field ? Array.isArray(parsed.extractions.field) ? parsed.extractions.field : [parsed.extractions.field] : [];
1018
- for (const field of fields) {
1019
- if (field?.key) {
1020
- const extraction = {
1021
- field: String(field.key),
1022
- value: field.value ?? null,
1023
- confidence: parseFloat(String(field.confidence ?? "")) || 0.5,
1024
- reasoning: field.reasoning ? String(field.reasoning) : undefined,
1025
- isCorrection: field.is_correction === "true" || field.is_correction === true
1026
- };
1027
- result.extractions.push(extraction);
1028
- }
1029
- }
1030
- }
1031
- }
1032
- } catch (_error) {
1033
- const intentMatch = response.match(/<intent>([^<]+)<\/intent>/);
1034
- if (intentMatch) {
1035
- const intentStr = intentMatch[1].toLowerCase().trim();
1036
- result.intent = isValidIntent(intentStr) ? intentStr : "other";
1037
- }
1038
- const fieldMatches = response.matchAll(/<field>\s*<key>([^<]+)<\/key>\s*<value>([^<]*)<\/value>\s*<confidence>([^<]+)<\/confidence>/g);
1039
- for (const match of fieldMatches) {
1040
- result.extractions.push({
1041
- field: match[1].trim(),
1042
- value: match[2].trim(),
1043
- confidence: parseFloat(match[3]) || 0.5
1044
- });
1045
- }
1046
- }
1047
- return result;
1048
- }
1049
- function isValidIntent(str) {
1050
- const validIntents = [
1051
- "fill_form",
1052
- "submit",
1053
- "stash",
1054
- "restore",
1055
- "cancel",
1056
- "undo",
1057
- "skip",
1058
- "explain",
1059
- "example",
1060
- "progress",
1061
- "autofill",
1062
- "other"
1063
- ];
1064
- return validIntents.includes(str);
1065
- }
1066
- async function extractSingleField(runtime, text, control, debug, templateValues) {
1067
- const resolvedControl = templateValues ? resolveControlTemplates(control, templateValues) : control;
1068
- const handler = getTypeHandler(resolvedControl.type);
1069
- const typeHint = handler?.extractionPrompt || resolvedControl.type;
1070
- const prompt = `Extract the ${resolvedControl.label} (${typeHint}) from this message:
1071
-
1072
- "${text}"
1073
-
1074
- ${resolvedControl.description ? `Context: ${resolvedControl.description}` : ""}
1075
- ${resolvedControl.extractHints?.length ? `Look for: ${resolvedControl.extractHints.join(", ")}` : ""}
1076
- ${resolvedControl.options?.length ? `Valid options: ${resolvedControl.options.map((o) => o.value).join(", ")}` : ""}
1077
- ${resolvedControl.example ? `Example: ${resolvedControl.example}` : ""}
1078
-
1079
- Respond in XML:
1080
- <response>
1081
- <found>true|false</found>
1082
- <value>extracted_value or empty if not found</value>
1083
- <confidence>0.0-1.0</confidence>
1084
- <reasoning>brief explanation</reasoning>
1085
- </response>`;
1086
- try {
1087
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
1088
- prompt,
1089
- temperature: 0.1
1090
- });
1091
- const parsed = parseKeyValueXml(response);
1092
- const found = parsed?.found === true || parsed?.found === "true";
1093
- if (found) {
1094
- let value = parsed.value;
1095
- if (typeof value === "string") {
1096
- value = parseValue(value, resolvedControl);
1097
- }
1098
- const confidence = typeof parsed?.confidence === "number" ? parsed.confidence : parseFloat(String(parsed?.confidence ?? ""));
1099
- const result = {
1100
- field: resolvedControl.key,
1101
- value: value ?? null,
1102
- confidence: Number.isFinite(confidence) ? confidence : 0.5,
1103
- reasoning: parsed.reasoning ? String(parsed.reasoning) : undefined
1104
- };
1105
- if (debug) {
1106
- runtime.logger.debug("[FormExtraction] Single field extraction:", JSON.stringify(result));
1107
- }
1108
- return result;
1109
- }
1110
- return null;
1111
- } catch (error) {
1112
- runtime.logger.error("[FormExtraction] Single field extraction failed:", String(error));
1113
- return null;
1114
- }
1115
- }
1116
- async function detectCorrection(runtime, text, currentValues, controls, templateValues) {
1117
- const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
1118
- const currentValuesStr = resolvedControls.filter((c) => currentValues[c.key] !== undefined).map((c) => `- ${c.label}: ${currentValues[c.key]}`).join(`
1119
- `);
1120
- if (!currentValuesStr) {
1121
- return [];
1122
- }
1123
- const prompt = `Is the user correcting any of these previously provided values?
1124
-
1125
- Current values:
1126
- ${currentValuesStr}
1127
-
1128
- User message:
1129
- "${text}"
1130
-
1131
- If they are correcting a value, extract the new value. Otherwise respond with no corrections.
1132
-
1133
- Respond in XML:
1134
- <response>
1135
- <has_correction>true|false</has_correction>
1136
- <corrections>
1137
- <correction>
1138
- <field>field_label</field>
1139
- <old_value>previous value</old_value>
1140
- <new_value>corrected value</new_value>
1141
- <confidence>0.0-1.0</confidence>
1142
- </correction>
1143
- </corrections>
1144
- </response>`;
1145
- try {
1146
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
1147
- prompt,
1148
- temperature: 0.1
1149
- });
1150
- const parsed = parseKeyValueXml(response);
1151
- const hasCorrection = parsed?.has_correction === true || parsed?.has_correction === "true";
1152
- if (parsed && hasCorrection && parsed.corrections) {
1153
- const corrections = [];
1154
- const correctionList = Array.isArray(parsed.corrections) ? parsed.corrections : parsed.corrections.correction ? Array.isArray(parsed.corrections.correction) ? parsed.corrections.correction : [parsed.corrections.correction] : [];
1155
- for (const correction of correctionList) {
1156
- const fieldName = correction.field ? String(correction.field) : "";
1157
- const control = resolvedControls.find((c) => c.label.toLowerCase() === fieldName.toLowerCase() || c.key.toLowerCase() === fieldName.toLowerCase());
1158
- if (control) {
1159
- let value = correction.new_value;
1160
- if (typeof value === "string") {
1161
- value = parseValue(value, control);
1162
- }
1163
- const confidence = typeof correction.confidence === "number" ? correction.confidence : parseFloat(String(correction.confidence ?? ""));
1164
- const extraction = {
1165
- field: control.key,
1166
- value: value ?? null,
1167
- confidence: Number.isFinite(confidence) ? confidence : 0.8,
1168
- isCorrection: true
1169
- };
1170
- corrections.push(extraction);
1171
- }
1172
- }
1173
- return corrections;
1174
- }
1175
- return [];
1176
- } catch (error) {
1177
- runtime.logger.error("[FormExtraction] Correction detection failed:", String(error));
1178
- return [];
1179
- }
1180
- }
1181
- var init_extraction = __esm(() => {
1182
- init_template();
1183
- init_validation();
1184
- });
1185
-
1186
- // src/service.ts
1187
- var exports_service = {};
1188
- __export(exports_service, {
1189
- FormService: () => FormService
1190
- });
1191
- import {
1192
- logger,
1193
- Service
1194
- } from "@elizaos/core";
1195
- function prettify3(key) {
1196
- return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1197
- }
1198
- var FormService;
1199
- var init_service = __esm(() => {
1200
- init_dist_node();
1201
- init_builtins();
1202
- init_storage();
1203
- init_types();
1204
- init_validation();
1205
- FormService = class FormService extends Service {
1206
- static serviceType = "FORM";
1207
- capabilityDescription = "Manages conversational forms for data collection";
1208
- forms = new Map;
1209
- controlTypes = new Map;
1210
- static async start(runtime) {
1211
- const service = new FormService(runtime);
1212
- registerBuiltinTypes((type, options) => service.registerControlType(type, options));
1213
- logger.info("[FormService] Started with built-in types");
1214
- return service;
1215
- }
1216
- async stop() {
1217
- logger.info("[FormService] Stopped");
1218
- }
1219
- registerForm(definition) {
1220
- const form = {
1221
- ...definition,
1222
- version: definition.version ?? FORM_DEFINITION_DEFAULTS.version,
1223
- status: definition.status ?? FORM_DEFINITION_DEFAULTS.status,
1224
- ux: { ...FORM_DEFINITION_DEFAULTS.ux, ...definition.ux },
1225
- ttl: { ...FORM_DEFINITION_DEFAULTS.ttl, ...definition.ttl },
1226
- nudge: { ...FORM_DEFINITION_DEFAULTS.nudge, ...definition.nudge },
1227
- debug: definition.debug ?? FORM_DEFINITION_DEFAULTS.debug,
1228
- controls: definition.controls.map((control) => ({
1229
- ...control,
1230
- type: control.type || FORM_CONTROL_DEFAULTS.type,
1231
- required: control.required ?? FORM_CONTROL_DEFAULTS.required,
1232
- confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
1233
- label: control.label || prettify3(control.key)
1234
- }))
1235
- };
1236
- this.forms.set(form.id, form);
1237
- logger.debug(`[FormService] Registered form: ${form.id}`);
1238
- }
1239
- getForm(formId) {
1240
- return this.forms.get(formId);
1241
- }
1242
- listForms() {
1243
- return Array.from(this.forms.values());
1244
- }
1245
- registerControlType(type, options) {
1246
- const existing = this.controlTypes.get(type.id);
1247
- if (existing) {
1248
- if (existing.builtin && !options?.allowOverride) {
1249
- logger.warn(`[FormService] Cannot override builtin type '${type.id}' without allowOverride: true`);
1250
- return;
1251
- }
1252
- logger.warn(`[FormService] Overriding control type: ${type.id}`);
1253
- }
1254
- this.controlTypes.set(type.id, type);
1255
- logger.debug(`[FormService] Registered control type: ${type.id}`);
1256
- }
1257
- getControlType(typeId) {
1258
- return this.controlTypes.get(typeId);
1259
- }
1260
- listControlTypes() {
1261
- return Array.from(this.controlTypes.values());
1262
- }
1263
- isCompositeType(typeId) {
1264
- const type = this.controlTypes.get(typeId);
1265
- return !!type?.getSubControls;
1266
- }
1267
- isExternalType(typeId) {
1268
- const type = this.controlTypes.get(typeId);
1269
- return !!type?.activate;
1270
- }
1271
- getSubControls(control) {
1272
- const type = this.controlTypes.get(control.type);
1273
- if (!type?.getSubControls) {
1274
- return [];
1275
- }
1276
- return type.getSubControls(control, this.runtime);
1277
- }
1278
- async startSession(formId, entityId, roomId, options) {
1279
- const form = this.getForm(formId);
1280
- if (!form) {
1281
- throw new Error(`Form not found: ${formId}`);
1282
- }
1283
- const existing = await getActiveSession(this.runtime, entityId, roomId);
1284
- if (existing) {
1285
- throw new Error(`Active session already exists for this user/room: ${existing.id}`);
1286
- }
1287
- const now = Date.now();
1288
- const fields = {};
1289
- for (const control of form.controls) {
1290
- if (options?.initialValues?.[control.key] !== undefined) {
1291
- fields[control.key] = {
1292
- status: "filled",
1293
- value: options.initialValues[control.key],
1294
- source: "manual",
1295
- updatedAt: now
1296
- };
1297
- } else if (control.defaultValue !== undefined) {
1298
- fields[control.key] = {
1299
- status: "filled",
1300
- value: control.defaultValue,
1301
- source: "default",
1302
- updatedAt: now
1303
- };
1304
- } else {
1305
- fields[control.key] = { status: "empty" };
1306
- }
1307
- }
1308
- const ttlDays = form.ttl?.minDays ?? 14;
1309
- const expiresAt = now + ttlDays * 24 * 60 * 60 * 1000;
1310
- const session = {
1311
- id: v4_default(),
1312
- formId,
1313
- formVersion: form.version,
1314
- entityId,
1315
- roomId,
1316
- status: "active",
1317
- fields,
1318
- history: [],
1319
- context: options?.context,
1320
- locale: options?.locale,
1321
- effort: {
1322
- interactionCount: 0,
1323
- timeSpentMs: 0,
1324
- firstInteractionAt: now,
1325
- lastInteractionAt: now
1326
- },
1327
- expiresAt,
1328
- createdAt: now,
1329
- updatedAt: now
1330
- };
1331
- await saveSession(this.runtime, session);
1332
- if (form.hooks?.onStart) {
1333
- await this.executeHook(session, "onStart");
1334
- }
1335
- logger.debug(`[FormService] Started session ${session.id} for form ${formId}`);
1336
- return session;
1337
- }
1338
- async getActiveSession(entityId, roomId) {
1339
- return getActiveSession(this.runtime, entityId, roomId);
1340
- }
1341
- async getAllActiveSessions(entityId) {
1342
- return getAllActiveSessions(this.runtime, entityId);
1343
- }
1344
- async getStashedSessions(entityId) {
1345
- return getStashedSessions(this.runtime, entityId);
1346
- }
1347
- async saveSession(session) {
1348
- session.updatedAt = Date.now();
1349
- await saveSession(this.runtime, session);
1350
- }
1351
- async updateField(sessionId, entityId, field, value, confidence, source, messageId) {
1352
- const session = await getSessionById(this.runtime, entityId, sessionId);
1353
- if (!session) {
1354
- throw new Error(`Session not found: ${sessionId}`);
1355
- }
1356
- const form = this.getForm(session.formId);
1357
- if (!form) {
1358
- throw new Error(`Form not found: ${session.formId}`);
1359
- }
1360
- const control = form.controls.find((c) => c.key === field);
1361
- if (!control) {
1362
- throw new Error(`Field not found: ${field}`);
1363
- }
1364
- const oldValue = session.fields[field]?.value;
1365
- const validation = validateField(value, control);
1366
- let status;
1367
- if (!validation.valid) {
1368
- status = "invalid";
1369
- } else if (confidence < (control.confirmThreshold ?? 0.8)) {
1370
- status = "uncertain";
1371
- } else {
1372
- status = "filled";
1373
- }
1374
- const now = Date.now();
1375
- if (oldValue !== undefined) {
1376
- const historyEntry = {
1377
- field,
1378
- oldValue,
1379
- newValue: value,
1380
- timestamp: now
1381
- };
1382
- session.history.push(historyEntry);
1383
- const maxUndo = form.ux?.maxUndoSteps ?? 5;
1384
- if (session.history.length > maxUndo) {
1385
- session.history = session.history.slice(-maxUndo);
1386
- }
1387
- }
1388
- session.fields[field] = {
1389
- status,
1390
- value,
1391
- confidence,
1392
- source,
1393
- messageId,
1394
- updatedAt: now,
1395
- error: !validation.valid ? validation.error : undefined
1396
- };
1397
- session.effort.interactionCount++;
1398
- session.effort.lastInteractionAt = now;
1399
- session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
1400
- session.expiresAt = this.calculateTTL(session);
1401
- const allRequiredFilled = this.checkAllRequiredFilled(session, form);
1402
- if (allRequiredFilled && session.status === "active") {
1403
- session.status = "ready";
1404
- if (form.hooks?.onReady) {
1405
- await this.executeHook(session, "onReady");
1406
- }
1407
- }
1408
- session.updatedAt = now;
1409
- await saveSession(this.runtime, session);
1410
- if (form.hooks?.onFieldChange) {
1411
- const hookPayload = { field, value };
1412
- if (oldValue !== undefined) {
1413
- hookPayload.oldValue = oldValue;
1414
- }
1415
- await this.executeHook(session, "onFieldChange", hookPayload);
1416
- }
1417
- }
1418
- async undoLastChange(sessionId, entityId) {
1419
- const session = await getSessionById(this.runtime, entityId, sessionId);
1420
- if (!session) {
1421
- throw new Error(`Session not found: ${sessionId}`);
1422
- }
1423
- const form = this.getForm(session.formId);
1424
- if (!form?.ux?.allowUndo) {
1425
- return null;
1426
- }
1427
- const lastChange = session.history.pop();
1428
- if (!lastChange) {
1429
- return null;
1430
- }
1431
- if (lastChange.oldValue !== undefined) {
1432
- session.fields[lastChange.field] = {
1433
- status: "filled",
1434
- value: lastChange.oldValue,
1435
- source: "correction",
1436
- updatedAt: Date.now()
1437
- };
1438
- } else {
1439
- session.fields[lastChange.field] = { status: "empty" };
1440
- }
1441
- session.updatedAt = Date.now();
1442
- await saveSession(this.runtime, session);
1443
- return { field: lastChange.field, restoredValue: lastChange.oldValue };
1444
- }
1445
- async skipField(sessionId, entityId, field) {
1446
- const session = await getSessionById(this.runtime, entityId, sessionId);
1447
- if (!session) {
1448
- throw new Error(`Session not found: ${sessionId}`);
1449
- }
1450
- const form = this.getForm(session.formId);
1451
- if (!form?.ux?.allowSkip) {
1452
- return false;
1453
- }
1454
- const control = form.controls.find((c) => c.key === field);
1455
- if (!control) {
1456
- return false;
1457
- }
1458
- if (control.required) {
1459
- return false;
1460
- }
1461
- session.fields[field] = {
1462
- status: "skipped",
1463
- updatedAt: Date.now()
1464
- };
1465
- session.updatedAt = Date.now();
1466
- await saveSession(this.runtime, session);
1467
- return true;
1468
- }
1469
- async confirmField(sessionId, entityId, field, accepted) {
1470
- const session = await getSessionById(this.runtime, entityId, sessionId);
1471
- if (!session) {
1472
- throw new Error(`Session not found: ${sessionId}`);
1473
- }
1474
- const fieldState = session.fields[field];
1475
- if (!fieldState || fieldState.status !== "uncertain") {
1476
- return;
1477
- }
1478
- const now = Date.now();
1479
- if (accepted) {
1480
- fieldState.status = "filled";
1481
- fieldState.confirmedAt = now;
1482
- } else {
1483
- fieldState.status = "empty";
1484
- fieldState.value = undefined;
1485
- fieldState.confidence = undefined;
1486
- }
1487
- fieldState.updatedAt = now;
1488
- session.updatedAt = now;
1489
- await saveSession(this.runtime, session);
1490
- }
1491
- async updateSubField(sessionId, entityId, parentField, subField, value, confidence, messageId) {
1492
- const session = await getSessionById(this.runtime, entityId, sessionId);
1493
- if (!session) {
1494
- throw new Error(`Session not found: ${sessionId}`);
1495
- }
1496
- const form = this.getForm(session.formId);
1497
- if (!form) {
1498
- throw new Error(`Form not found: ${session.formId}`);
1499
- }
1500
- const parentControl = form.controls.find((c) => c.key === parentField);
1501
- if (!parentControl) {
1502
- throw new Error(`Parent field not found: ${parentField}`);
1503
- }
1504
- const controlType = this.getControlType(parentControl.type);
1505
- if (!controlType?.getSubControls) {
1506
- throw new Error(`Control type '${parentControl.type}' is not a composite type`);
1507
- }
1508
- const subControls = controlType.getSubControls(parentControl, this.runtime);
1509
- const subControl = subControls.find((c) => c.key === subField);
1510
- if (!subControl) {
1511
- throw new Error(`Subfield not found: ${subField} in ${parentField}`);
1512
- }
1513
- const now = Date.now();
1514
- if (!session.fields[parentField]) {
1515
- session.fields[parentField] = { status: "empty" };
1516
- }
1517
- if (!session.fields[parentField].subFields) {
1518
- session.fields[parentField].subFields = {};
1519
- }
1520
- let subFieldStatus;
1521
- let error;
1522
- if (controlType.validate) {
1523
- const result = controlType.validate(value, subControl);
1524
- if (!result.valid) {
1525
- subFieldStatus = "invalid";
1526
- error = result.error;
1527
- } else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
1528
- subFieldStatus = "uncertain";
1529
- } else {
1530
- subFieldStatus = "filled";
1531
- }
1532
- } else {
1533
- const validation = validateField(value, subControl);
1534
- if (!validation.valid) {
1535
- subFieldStatus = "invalid";
1536
- error = validation.error;
1537
- } else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
1538
- subFieldStatus = "uncertain";
1539
- } else {
1540
- subFieldStatus = "filled";
1541
- }
1542
- }
1543
- session.fields[parentField].subFields[subField] = {
1544
- status: subFieldStatus,
1545
- value,
1546
- confidence,
1547
- source: "extraction",
1548
- messageId,
1549
- updatedAt: now,
1550
- error
1551
- };
1552
- session.effort.interactionCount++;
1553
- session.effort.lastInteractionAt = now;
1554
- session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
1555
- session.updatedAt = now;
1556
- await saveSession(this.runtime, session);
1557
- logger.debug(`[FormService] Updated subfield ${parentField}.${subField}`);
1558
- }
1559
- areSubFieldsFilled(session, parentField) {
1560
- const form = this.getForm(session.formId);
1561
- if (!form)
1562
- return false;
1563
- const parentControl = form.controls.find((c) => c.key === parentField);
1564
- if (!parentControl)
1565
- return false;
1566
- const controlType = this.getControlType(parentControl.type);
1567
- if (!controlType?.getSubControls)
1568
- return false;
1569
- const subControls = controlType.getSubControls(parentControl, this.runtime);
1570
- const subFields = session.fields[parentField]?.subFields || {};
1571
- for (const subControl of subControls) {
1572
- if (!subControl.required)
1573
- continue;
1574
- const subField = subFields[subControl.key];
1575
- if (!subField || subField.status !== "filled") {
1576
- return false;
1577
- }
1578
- }
1579
- return true;
1580
- }
1581
- getSubFieldValues(session, parentField) {
1582
- const subFields = session.fields[parentField]?.subFields || {};
1583
- const values = {};
1584
- for (const [key, state] of Object.entries(subFields)) {
1585
- if (state.value !== undefined) {
1586
- values[key] = state.value;
1587
- }
1588
- }
1589
- return values;
1590
- }
1591
- async activateExternalField(sessionId, entityId, field) {
1592
- const session = await getSessionById(this.runtime, entityId, sessionId);
1593
- if (!session) {
1594
- throw new Error(`Session not found: ${sessionId}`);
1595
- }
1596
- const form = this.getForm(session.formId);
1597
- if (!form) {
1598
- throw new Error(`Form not found: ${session.formId}`);
1599
- }
1600
- const control = form.controls.find((c) => c.key === field);
1601
- if (!control) {
1602
- throw new Error(`Field not found: ${field}`);
1603
- }
1604
- const controlType = this.getControlType(control.type);
1605
- if (!controlType?.activate) {
1606
- throw new Error(`Control type '${control.type}' does not support activation`);
1607
- }
1608
- const subValues = this.getSubFieldValues(session, field);
1609
- const context = {
1610
- runtime: this.runtime,
1611
- session,
1612
- control,
1613
- subValues
1614
- };
1615
- const activation = await controlType.activate(context);
1616
- const now = Date.now();
1617
- if (!session.fields[field]) {
1618
- session.fields[field] = { status: "empty" };
1619
- }
1620
- session.fields[field].status = "pending";
1621
- session.fields[field].externalState = {
1622
- status: "pending",
1623
- reference: activation.reference,
1624
- instructions: activation.instructions,
1625
- address: activation.address,
1626
- activatedAt: now
1627
- };
1628
- session.updatedAt = now;
1629
- await saveSession(this.runtime, session);
1630
- logger.info(`[FormService] Activated external field ${field} with reference ${activation.reference}`);
1631
- return activation;
1632
- }
1633
- async confirmExternalField(sessionId, entityId, field, value, externalData) {
1634
- const session = await getSessionById(this.runtime, entityId, sessionId);
1635
- if (!session) {
1636
- throw new Error(`Session not found: ${sessionId}`);
1637
- }
1638
- const fieldState = session.fields[field];
1639
- if (!fieldState || fieldState.status !== "pending") {
1640
- logger.warn(`[FormService] Cannot confirm field ${field}: not in pending state`);
1641
- return;
1642
- }
1643
- const now = Date.now();
1644
- fieldState.status = "filled";
1645
- fieldState.value = value;
1646
- fieldState.source = "external";
1647
- fieldState.updatedAt = now;
1648
- if (fieldState.externalState) {
1649
- fieldState.externalState.status = "confirmed";
1650
- fieldState.externalState.confirmedAt = now;
1651
- fieldState.externalState.externalData = externalData;
1652
- }
1653
- const form = this.getForm(session.formId);
1654
- if (form && this.checkAllRequiredFilled(session, form)) {
1655
- if (session.status === "active") {
1656
- session.status = "ready";
1657
- if (form.hooks?.onReady) {
1658
- await this.executeHook(session, "onReady");
1659
- }
1660
- }
1661
- }
1662
- session.updatedAt = now;
1663
- await saveSession(this.runtime, session);
1664
- try {
1665
- await this.runtime.emitEvent("FORM_FIELD_CONFIRMED", {
1666
- runtime: this.runtime,
1667
- sessionId,
1668
- entityId,
1669
- field,
1670
- value,
1671
- externalData
1672
- });
1673
- } catch (_error) {
1674
- logger.debug(`[FormService] No event handler for FORM_FIELD_CONFIRMED`);
1675
- }
1676
- logger.info(`[FormService] Confirmed external field ${field}`);
1677
- }
1678
- async cancelExternalField(sessionId, entityId, field, reason) {
1679
- const session = await getSessionById(this.runtime, entityId, sessionId);
1680
- if (!session) {
1681
- throw new Error(`Session not found: ${sessionId}`);
1682
- }
1683
- const form = this.getForm(session.formId);
1684
- const control = form?.controls.find((c) => c.key === field);
1685
- const controlType = control ? this.getControlType(control.type) : undefined;
1686
- if (controlType?.deactivate && control) {
1687
- try {
1688
- await controlType.deactivate({
1689
- runtime: this.runtime,
1690
- session,
1691
- control,
1692
- subValues: this.getSubFieldValues(session, field)
1693
- });
1694
- } catch (error) {
1695
- logger.error(`[FormService] Deactivate failed for ${field}: ${String(error)}`);
1696
- }
1697
- }
1698
- const fieldState = session.fields[field];
1699
- if (fieldState) {
1700
- fieldState.status = "empty";
1701
- fieldState.error = reason;
1702
- if (fieldState.externalState) {
1703
- fieldState.externalState.status = "failed";
1704
- }
1705
- }
1706
- session.updatedAt = Date.now();
1707
- await saveSession(this.runtime, session);
1708
- try {
1709
- await this.runtime.emitEvent("FORM_FIELD_CANCELLED", {
1710
- runtime: this.runtime,
1711
- sessionId,
1712
- entityId,
1713
- field,
1714
- reason
1715
- });
1716
- } catch (_error) {
1717
- logger.debug(`[FormService] No event handler for FORM_FIELD_CANCELLED`);
1718
- }
1719
- logger.info(`[FormService] Cancelled external field ${field}: ${reason}`);
1720
- }
1721
- async submit(sessionId, entityId) {
1722
- const session = await getSessionById(this.runtime, entityId, sessionId);
1723
- if (!session) {
1724
- throw new Error(`Session not found: ${sessionId}`);
1725
- }
1726
- const form = this.getForm(session.formId);
1727
- if (!form) {
1728
- throw new Error(`Form not found: ${session.formId}`);
1729
- }
1730
- if (!this.checkAllRequiredFilled(session, form)) {
1731
- throw new Error("Not all required fields are filled");
1732
- }
1733
- const now = Date.now();
1734
- const values = {};
1735
- const mappedValues = {};
1736
- const files = {};
1737
- for (const control of form.controls) {
1738
- const fieldState = session.fields[control.key];
1739
- if (fieldState?.value !== undefined) {
1740
- values[control.key] = fieldState.value;
1741
- const dbKey = control.dbbind || control.key;
1742
- mappedValues[dbKey] = fieldState.value;
1743
- }
1744
- if (fieldState?.files) {
1745
- files[control.key] = fieldState.files;
1746
- }
1747
- }
1748
- const submission = {
1749
- id: v4_default(),
1750
- formId: session.formId,
1751
- formVersion: session.formVersion,
1752
- sessionId: session.id,
1753
- entityId: session.entityId,
1754
- values,
1755
- mappedValues,
1756
- files: Object.keys(files).length > 0 ? files : undefined,
1757
- submittedAt: now,
1758
- meta: session.meta
1759
- };
1760
- await saveSubmission(this.runtime, submission);
1761
- await saveAutofillData(this.runtime, entityId, session.formId, values);
1762
- session.status = "submitted";
1763
- session.submittedAt = now;
1764
- session.updatedAt = now;
1765
- await saveSession(this.runtime, session);
1766
- if (form.hooks?.onSubmit) {
1767
- const submissionPayload = JSON.parse(JSON.stringify(submission));
1768
- await this.executeHook(session, "onSubmit", {
1769
- submission: submissionPayload
1770
- });
1771
- }
1772
- logger.debug(`[FormService] Submitted session ${sessionId}`);
1773
- return submission;
1774
- }
1775
- async stash(sessionId, entityId) {
1776
- const session = await getSessionById(this.runtime, entityId, sessionId);
1777
- if (!session) {
1778
- throw new Error(`Session not found: ${sessionId}`);
1779
- }
1780
- const form = this.getForm(session.formId);
1781
- session.status = "stashed";
1782
- session.updatedAt = Date.now();
1783
- await saveSession(this.runtime, session);
1784
- if (form?.hooks?.onCancel) {}
1785
- logger.debug(`[FormService] Stashed session ${sessionId}`);
1786
- }
1787
- async restore(sessionId, entityId) {
1788
- const session = await getSessionById(this.runtime, entityId, sessionId);
1789
- if (!session) {
1790
- throw new Error(`Session not found: ${sessionId}`);
1791
- }
1792
- if (session.status !== "stashed") {
1793
- throw new Error(`Session is not stashed: ${session.status}`);
1794
- }
1795
- const existing = await getActiveSession(this.runtime, entityId, session.roomId);
1796
- if (existing && existing.id !== sessionId) {
1797
- throw new Error(`Active session already exists in room: ${existing.id}`);
1798
- }
1799
- session.status = "active";
1800
- session.updatedAt = Date.now();
1801
- session.expiresAt = this.calculateTTL(session);
1802
- await saveSession(this.runtime, session);
1803
- logger.debug(`[FormService] Restored session ${sessionId}`);
1804
- return session;
1805
- }
1806
- async cancel(sessionId, entityId, force = false) {
1807
- const session = await getSessionById(this.runtime, entityId, sessionId);
1808
- if (!session) {
1809
- throw new Error(`Session not found: ${sessionId}`);
1810
- }
1811
- if (!force && this.shouldConfirmCancel(session) && !session.cancelConfirmationAsked) {
1812
- session.cancelConfirmationAsked = true;
1813
- session.updatedAt = Date.now();
1814
- await saveSession(this.runtime, session);
1815
- return false;
1816
- }
1817
- const form = this.getForm(session.formId);
1818
- session.status = "cancelled";
1819
- session.updatedAt = Date.now();
1820
- await saveSession(this.runtime, session);
1821
- if (form?.hooks?.onCancel) {
1822
- await this.executeHook(session, "onCancel");
1823
- }
1824
- logger.debug(`[FormService] Cancelled session ${sessionId}`);
1825
- return true;
1826
- }
1827
- async getSubmissions(entityId, formId) {
1828
- return getSubmissions(this.runtime, entityId, formId);
1829
- }
1830
- async getAutofill(entityId, formId) {
1831
- const data = await getAutofillData(this.runtime, entityId, formId);
1832
- return data?.values || null;
1833
- }
1834
- async applyAutofill(session) {
1835
- const form = this.getForm(session.formId);
1836
- if (!form?.ux?.allowAutofill) {
1837
- return [];
1838
- }
1839
- const autofill = await getAutofillData(this.runtime, session.entityId, session.formId);
1840
- if (!autofill) {
1841
- return [];
1842
- }
1843
- const appliedFields = [];
1844
- const now = Date.now();
1845
- for (const control of form.controls) {
1846
- if (session.fields[control.key]?.status !== "empty") {
1847
- continue;
1848
- }
1849
- const value = autofill.values[control.key];
1850
- if (value !== undefined) {
1851
- session.fields[control.key] = {
1852
- status: "filled",
1853
- value,
1854
- source: "autofill",
1855
- updatedAt: now
1856
- };
1857
- appliedFields.push(control.key);
1858
- }
1859
- }
1860
- if (appliedFields.length > 0) {
1861
- session.updatedAt = now;
1862
- await saveSession(this.runtime, session);
1863
- }
1864
- return appliedFields;
1865
- }
1866
- getSessionContext(session) {
1867
- const form = this.getForm(session.formId);
1868
- if (!form) {
1869
- return {
1870
- hasActiveForm: false,
1871
- progress: 0,
1872
- filledFields: [],
1873
- missingRequired: [],
1874
- uncertainFields: [],
1875
- nextField: null,
1876
- pendingExternalFields: []
1877
- };
1878
- }
1879
- const filledFields = [];
1880
- const missingRequired = [];
1881
- const uncertainFields = [];
1882
- const pendingExternalFields = [];
1883
- let nextField = null;
1884
- let filledCount = 0;
1885
- let totalRequired = 0;
1886
- for (const control of form.controls) {
1887
- if (control.hidden)
1888
- continue;
1889
- const fieldState = session.fields[control.key];
1890
- if (control.required) {
1891
- totalRequired++;
1892
- }
1893
- if (fieldState?.status === "filled") {
1894
- filledCount++;
1895
- filledFields.push({
1896
- key: control.key,
1897
- label: control.label,
1898
- displayValue: formatValue(fieldState.value ?? null, control)
1899
- });
1900
- } else if (fieldState?.status === "pending") {
1901
- if (fieldState.externalState) {
1902
- pendingExternalFields.push({
1903
- key: control.key,
1904
- label: control.label,
1905
- instructions: fieldState.externalState.instructions || "Waiting for confirmation...",
1906
- reference: fieldState.externalState.reference || "",
1907
- activatedAt: fieldState.externalState.activatedAt || Date.now(),
1908
- address: fieldState.externalState.address
1909
- });
1910
- }
1911
- } else if (fieldState?.status === "uncertain") {
1912
- uncertainFields.push({
1913
- key: control.key,
1914
- label: control.label,
1915
- value: fieldState.value ?? null,
1916
- confidence: fieldState.confidence ?? 0
1917
- });
1918
- } else if (fieldState?.status === "invalid") {
1919
- missingRequired.push({
1920
- key: control.key,
1921
- label: control.label,
1922
- description: control.description,
1923
- askPrompt: control.askPrompt
1924
- });
1925
- if (!nextField)
1926
- nextField = control;
1927
- } else if (control.required && fieldState?.status !== "skipped") {
1928
- missingRequired.push({
1929
- key: control.key,
1930
- label: control.label,
1931
- description: control.description,
1932
- askPrompt: control.askPrompt
1933
- });
1934
- if (!nextField)
1935
- nextField = control;
1936
- } else if (!nextField && fieldState?.status === "empty") {
1937
- nextField = control;
1938
- }
1939
- }
1940
- const progress = totalRequired > 0 ? Math.round(filledCount / totalRequired * 100) : 100;
1941
- return {
1942
- hasActiveForm: true,
1943
- formId: session.formId,
1944
- formName: form.name,
1945
- progress,
1946
- filledFields,
1947
- missingRequired,
1948
- uncertainFields,
1949
- nextField,
1950
- status: session.status,
1951
- pendingCancelConfirmation: session.cancelConfirmationAsked && session.status === "active",
1952
- pendingExternalFields
1953
- };
1954
- }
1955
- getValues(session) {
1956
- const values = {};
1957
- for (const [key, state] of Object.entries(session.fields)) {
1958
- if (state.value !== undefined) {
1959
- values[key] = state.value;
1960
- }
1961
- }
1962
- return values;
1963
- }
1964
- getMappedValues(session) {
1965
- const form = this.getForm(session.formId);
1966
- if (!form)
1967
- return {};
1968
- const values = {};
1969
- for (const control of form.controls) {
1970
- const state = session.fields[control.key];
1971
- if (state?.value !== undefined) {
1972
- const key = control.dbbind || control.key;
1973
- values[key] = state.value;
1974
- }
1975
- }
1976
- return values;
1977
- }
1978
- calculateTTL(session) {
1979
- const form = this.getForm(session.formId);
1980
- const config = form?.ttl || {};
1981
- const minDays = config.minDays ?? 14;
1982
- const maxDays = config.maxDays ?? 90;
1983
- const multiplier = config.effortMultiplier ?? 0.5;
1984
- const minutesSpent = session.effort.timeSpentMs / 60000;
1985
- const effortDays = minutesSpent * multiplier;
1986
- const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
1987
- return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
1988
- }
1989
- shouldConfirmCancel(session) {
1990
- const minEffortMs = 5 * 60 * 1000;
1991
- return session.effort.timeSpentMs > minEffortMs;
1992
- }
1993
- async executeHook(session, hookName, options) {
1994
- const form = this.getForm(session.formId);
1995
- const workerName = form?.hooks?.[hookName];
1996
- if (!workerName)
1997
- return;
1998
- const worker = this.runtime.getTaskWorker(workerName);
1999
- if (!worker) {
2000
- logger.warn(`[FormService] Hook worker not found: ${workerName}`);
2001
- return;
2002
- }
2003
- try {
2004
- const task = {
2005
- id: session.id,
2006
- name: workerName,
2007
- roomId: session.roomId,
2008
- entityId: session.entityId,
2009
- tags: ["form-hook", hookName]
2010
- };
2011
- await worker.execute(this.runtime, {
2012
- session,
2013
- form,
2014
- ...options
2015
- }, task);
2016
- } catch (error) {
2017
- logger.error(`[FormService] Hook execution failed: ${hookName}`, String(error));
2018
- }
2019
- }
2020
- checkAllRequiredFilled(session, form) {
2021
- for (const control of form.controls) {
2022
- if (!control.required)
2023
- continue;
2024
- const fieldState = session.fields[control.key];
2025
- if (!fieldState || fieldState.status === "empty" || fieldState.status === "invalid") {
2026
- return false;
2027
- }
2028
- }
2029
- return true;
2030
- }
2031
- };
2032
- });
2033
-
2034
- // src/actions/restore.ts
2035
- var exports_restore = {};
2036
- __export(exports_restore, {
2037
- formRestoreAction: () => formRestoreAction,
2038
- default: () => restore_default
2039
- });
2040
1
  import {
2041
- logger as logger2
2042
- } from "@elizaos/core";
2043
- var formRestoreAction, restore_default;
2044
- var init_restore = __esm(() => {
2045
- formRestoreAction = {
2046
- name: "FORM_RESTORE",
2047
- similes: ["RESUME_FORM", "CONTINUE_FORM"],
2048
- description: "Restore a previously stashed form session",
2049
- validate: async (runtime, message, state, options) => {
2050
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
2051
- const __avText = __avTextRaw.toLowerCase();
2052
- const __avKeywords = ["form", "restore"];
2053
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
2054
- const __avRegex = /\b(?:form|restore)\b/i;
2055
- const __avRegexOk = Boolean(__avText.match(__avRegex));
2056
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
2057
- const __avExpectedSource = "";
2058
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
2059
- const __avOptions = options && typeof options === "object" ? options : {};
2060
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
2061
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
2062
- return false;
2063
- }
2064
- const __avLegacyValidate = async (runtime2, message2, _state) => {
2065
- try {
2066
- const text = message2.content?.text || "";
2067
- const intent = quickIntentDetect(text);
2068
- if (intent !== "restore") {
2069
- return false;
2070
- }
2071
- const formService = runtime2.getService("FORM");
2072
- if (!formService) {
2073
- return false;
2074
- }
2075
- const entityId = message2.entityId;
2076
- if (!entityId)
2077
- return false;
2078
- const stashed = await formService.getStashedSessions(entityId);
2079
- return stashed.length > 0;
2080
- } catch (error) {
2081
- logger2.error("[FormRestoreAction] Validation error:", String(error));
2082
- return false;
2083
- }
2084
- };
2085
- try {
2086
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
2087
- } catch {
2088
- return false;
2089
- }
2090
- },
2091
- handler: async (runtime, message, _state, _options, callback) => {
2092
- try {
2093
- const formService = runtime.getService("FORM");
2094
- if (!formService) {
2095
- await callback?.({
2096
- text: "Sorry, I couldn't find the form service."
2097
- });
2098
- return { success: false };
2099
- }
2100
- const entityId = message.entityId;
2101
- const roomId = message.roomId;
2102
- if (!entityId || !roomId) {
2103
- await callback?.({
2104
- text: "Sorry, I couldn't identify you."
2105
- });
2106
- return { success: false };
2107
- }
2108
- const existing = await formService.getActiveSession(entityId, roomId);
2109
- if (existing) {
2110
- const form2 = formService.getForm(existing.formId);
2111
- await callback?.({
2112
- text: `You already have an active form: "${form2?.name || existing.formId}". Would you like to continue with that one, or should I save it and restore your other form?`
2113
- });
2114
- return { success: false };
2115
- }
2116
- const stashed = await formService.getStashedSessions(entityId);
2117
- if (stashed.length === 0) {
2118
- await callback?.({
2119
- text: "You don't have any saved forms to resume."
2120
- });
2121
- return { success: false };
2122
- }
2123
- const sessionToRestore = stashed.sort((a, b) => b.updatedAt - a.updatedAt)[0];
2124
- const session = await formService.restore(sessionToRestore.id, entityId);
2125
- const form = formService.getForm(session.formId);
2126
- const context = formService.getSessionContext(session);
2127
- let responseText = `I've restored your "${form?.name || session.formId}" form. `;
2128
- responseText += `You're ${context.progress}% complete. `;
2129
- if (context.filledFields.length > 0) {
2130
- responseText += `
2131
-
2132
- Here's what I have so far:
2133
- `;
2134
- for (const field of context.filledFields) {
2135
- responseText += `• ${field.label}: ${field.displayValue}
2136
- `;
2137
- }
2138
- }
2139
- if (context.nextField) {
2140
- responseText += `
2141
- Let's continue with ${context.nextField.label}.`;
2142
- if (context.nextField.askPrompt) {
2143
- responseText += ` ${context.nextField.askPrompt}`;
2144
- }
2145
- } else if (context.status === "ready") {
2146
- responseText += `
2147
- Everything looks complete! Ready to submit?`;
2148
- }
2149
- await callback?.({
2150
- text: responseText
2151
- });
2152
- return {
2153
- success: true,
2154
- data: {
2155
- sessionId: session.id,
2156
- formId: session.formId,
2157
- progress: context.progress
2158
- }
2159
- };
2160
- } catch (error) {
2161
- logger2.error("[FormRestoreAction] Handler error:", String(error));
2162
- await callback?.({
2163
- text: "Sorry, I couldn't restore your form. Please try again."
2164
- });
2165
- return { success: false };
2166
- }
2167
- },
2168
- examples: [
2169
- [
2170
- {
2171
- name: "{{user1}}",
2172
- content: { text: "Resume my form" }
2173
- },
2174
- {
2175
- name: "{{agentName}}",
2176
- content: {
2177
- text: "I've restored your form. Let's continue where you left off."
2178
- }
2179
- }
2180
- ],
2181
- [
2182
- {
2183
- name: "{{user1}}",
2184
- content: { text: "Continue with my registration" }
2185
- },
2186
- {
2187
- name: "{{agentName}}",
2188
- content: {
2189
- text: "I've restored your Registration form. You're 60% complete."
2190
- }
2191
- }
2192
- ],
2193
- [
2194
- {
2195
- name: "{{user1}}",
2196
- content: { text: "Pick up where I left off" }
2197
- },
2198
- {
2199
- name: "{{agentName}}",
2200
- content: {
2201
- text: "I've restored your form. Here's what you have so far..."
2202
- }
2203
- }
2204
- ]
2205
- ]
2206
- };
2207
- restore_default = formRestoreAction;
2208
- });
2209
-
2210
- // src/evaluators/extractor.ts
2211
- var exports_extractor = {};
2212
- __export(exports_extractor, {
2213
- formEvaluator: () => formEvaluator,
2214
- default: () => extractor_default
2215
- });
2216
- import { logger as logger3 } from "@elizaos/core";
2217
- async function processExtractions(runtime, formService, session, form, entityId, extractions, messageId) {
2218
- const updatedParents = new Set;
2219
- for (const extraction of extractions) {
2220
- if (extraction.field.includes(".")) {
2221
- const [parentKey, subKey] = extraction.field.split(".");
2222
- await formService.updateSubField(session.id, entityId, parentKey, subKey, extraction.value, extraction.confidence, messageId);
2223
- await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
2224
- sessionId: session.id,
2225
- parentField: parentKey,
2226
- subField: subKey,
2227
- value: extraction.value,
2228
- confidence: extraction.confidence
2229
- });
2230
- updatedParents.add(parentKey);
2231
- if (form.debug) {
2232
- logger3.debug(`[FormEvaluator] Updated subfield ${parentKey}.${subKey}`);
2233
- }
2234
- } else {
2235
- await formService.updateField(session.id, entityId, extraction.field, extraction.value, extraction.confidence, extraction.isCorrection ? "correction" : "extraction", messageId);
2236
- await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
2237
- sessionId: session.id,
2238
- field: extraction.field,
2239
- value: extraction.value,
2240
- confidence: extraction.confidence
2241
- });
2242
- if (form.debug) {
2243
- logger3.debug(`[FormEvaluator] Updated field ${extraction.field}`);
2244
- }
2245
- }
2246
- }
2247
- for (const parentKey of updatedParents) {
2248
- await checkAndActivateExternalField(runtime, formService, session, form, entityId, parentKey);
2249
- }
2250
- }
2251
- async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
2252
- const freshSession = await formService.getActiveSession(entityId, session.roomId);
2253
- if (!freshSession)
2254
- return;
2255
- if (!formService.isExternalType(form.controls.find((c) => c.key === field)?.type || "")) {
2256
- return;
2257
- }
2258
- if (!formService.areSubFieldsFilled(freshSession, field)) {
2259
- return;
2260
- }
2261
- const subValues = formService.getSubFieldValues(freshSession, field);
2262
- await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
2263
- sessionId: session.id,
2264
- field,
2265
- subValues
2266
- });
2267
- logger3.debug(`[FormEvaluator] All subcontrols filled for ${field}, activating...`);
2268
- try {
2269
- const activation = await formService.activateExternalField(session.id, entityId, field);
2270
- const activationPayload = JSON.parse(JSON.stringify(activation));
2271
- await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
2272
- sessionId: session.id,
2273
- field,
2274
- activation: activationPayload
2275
- });
2276
- logger3.info(`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`);
2277
- } catch (error) {
2278
- logger3.error(`[FormEvaluator] Failed to activate external field ${field}:`, String(error));
2279
- }
2280
- }
2281
- async function emitEvent(runtime, eventType, payload) {
2282
- try {
2283
- if (typeof runtime.emitEvent === "function") {
2284
- const eventPayload = { runtime, ...payload };
2285
- await runtime.emitEvent(eventType, eventPayload);
2286
- }
2287
- } catch (error) {
2288
- logger3.debug(`[FormEvaluator] Event emission (${eventType}):`, String(error));
2289
- }
2290
- }
2291
- async function handleSubmit(formService, session, entityId) {
2292
- try {
2293
- await formService.submit(session.id, entityId);
2294
- } catch (error) {
2295
- logger3.debug("[FormEvaluator] Submit failed:", String(error));
2296
- }
2297
- }
2298
- async function handleUndo(formService, session, entityId, form) {
2299
- if (!form.ux?.allowUndo) {
2300
- return;
2301
- }
2302
- const result = await formService.undoLastChange(session.id, entityId);
2303
- if (result) {
2304
- logger3.debug("[FormEvaluator] Undid field:", result.field);
2305
- }
2306
- }
2307
- async function handleSkip(formService, session, entityId, form) {
2308
- if (!form.ux?.allowSkip) {
2309
- return;
2310
- }
2311
- if (session.lastAskedField) {
2312
- const skipped = await formService.skipField(session.id, entityId, session.lastAskedField);
2313
- if (skipped) {
2314
- logger3.debug("[FormEvaluator] Skipped field:", session.lastAskedField);
2315
- }
2316
- }
2317
- }
2318
- var formEvaluator, extractor_default;
2319
- var init_extractor = __esm(() => {
2320
- init_extraction();
2321
- init_template();
2322
- formEvaluator = {
2323
- name: "form_evaluator",
2324
- description: "Extracts form fields and handles form intents from user messages",
2325
- similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
2326
- examples: [],
2327
- validate: async (runtime, message, _state) => {
2328
- try {
2329
- const formService = runtime.getService("FORM");
2330
- if (!formService)
2331
- return false;
2332
- const entityId = message.entityId;
2333
- const roomId = message.roomId;
2334
- if (!entityId || !roomId)
2335
- return false;
2336
- const session = await formService.getActiveSession(entityId, roomId);
2337
- const stashed = await formService.getStashedSessions(entityId);
2338
- return session !== null || stashed.length > 0;
2339
- } catch (error) {
2340
- logger3.error("[FormEvaluator] Validation error:", String(error));
2341
- return false;
2342
- }
2343
- },
2344
- handler: async (runtime, message, _state) => {
2345
- try {
2346
- const formService = runtime.getService("FORM");
2347
- if (!formService)
2348
- return;
2349
- const entityId = message.entityId;
2350
- const roomId = message.roomId;
2351
- const text = message.content?.text || "";
2352
- if (!entityId || !roomId)
2353
- return;
2354
- if (!text.trim())
2355
- return;
2356
- let session = await formService.getActiveSession(entityId, roomId);
2357
- let intent = quickIntentDetect(text);
2358
- let extractions = [];
2359
- if (intent === "restore" && !session) {
2360
- logger3.debug("[FormEvaluator] Restore intent detected, deferring to action");
2361
- return;
2362
- }
2363
- if (!session) {
2364
- return;
2365
- }
2366
- const form = formService.getForm(session.formId);
2367
- if (!form) {
2368
- logger3.warn("[FormEvaluator] Form not found for session:", session.formId);
2369
- return;
2370
- }
2371
- const templateValues = buildTemplateValues(session);
2372
- if (!intent) {
2373
- const result = await llmIntentAndExtract(runtime, text, form, form.controls, templateValues);
2374
- intent = result.intent;
2375
- extractions = result.extractions;
2376
- if (form.debug) {
2377
- logger3.debug("[FormEvaluator] LLM extraction result:", JSON.stringify({ intent, extractions }));
2378
- }
2379
- }
2380
- switch (intent) {
2381
- case "submit":
2382
- await handleSubmit(formService, session, entityId);
2383
- break;
2384
- case "stash":
2385
- await formService.stash(session.id, entityId);
2386
- break;
2387
- case "cancel":
2388
- await formService.cancel(session.id, entityId);
2389
- break;
2390
- case "undo":
2391
- await handleUndo(formService, session, entityId, form);
2392
- break;
2393
- case "skip":
2394
- await handleSkip(formService, session, entityId, form);
2395
- break;
2396
- case "autofill":
2397
- await formService.applyAutofill(session);
2398
- break;
2399
- case "explain":
2400
- case "example":
2401
- case "progress":
2402
- logger3.debug(`[FormEvaluator] Info intent: ${intent}`);
2403
- break;
2404
- case "restore":
2405
- logger3.debug("[FormEvaluator] Restore intent - deferring to action");
2406
- break;
2407
- default:
2408
- await processExtractions(runtime, formService, session, form, entityId, extractions, message.id);
2409
- break;
2410
- }
2411
- session = await formService.getActiveSession(entityId, roomId);
2412
- if (session) {
2413
- session.lastMessageId = message.id;
2414
- await formService.saveSession(session);
2415
- }
2416
- } catch (error) {
2417
- logger3.error("[FormEvaluator] Handler error:", String(error));
2418
- return;
2419
- }
2420
- return;
2421
- }
2422
- };
2423
- extractor_default = formEvaluator;
2424
- });
2425
-
2426
- // src/providers/context.ts
2427
- var exports_context = {};
2428
- __export(exports_context, {
2429
- formContextProvider: () => formContextProvider,
2430
- default: () => context_default
2431
- });
2432
- import { logger as logger4 } from "@elizaos/core";
2433
- var formContextProvider, context_default;
2434
- var init_context = __esm(() => {
2435
- init_template();
2436
- formContextProvider = {
2437
- name: "FORM_CONTEXT",
2438
- description: "Provides context about active form sessions",
2439
- dynamic: true,
2440
- get: async (runtime, message, _state) => {
2441
- try {
2442
- const formService = runtime.getService("FORM");
2443
- if (!formService) {
2444
- return {
2445
- data: { hasActiveForm: false },
2446
- values: { formContext: "" },
2447
- text: ""
2448
- };
2449
- }
2450
- const entityId = message.entityId;
2451
- const roomId = message.roomId;
2452
- if (!entityId || !roomId) {
2453
- return {
2454
- data: { hasActiveForm: false },
2455
- values: { formContext: "" },
2456
- text: ""
2457
- };
2458
- }
2459
- const session = await formService.getActiveSession(entityId, roomId);
2460
- const stashed = await formService.getStashedSessions(entityId);
2461
- if (!session && stashed.length === 0) {
2462
- return {
2463
- data: { hasActiveForm: false, stashedCount: 0 },
2464
- values: { formContext: "" },
2465
- text: ""
2466
- };
2467
- }
2468
- let contextText = "";
2469
- let contextState;
2470
- if (session) {
2471
- contextState = formService.getSessionContext(session);
2472
- const form = formService.getForm(session.formId);
2473
- const templateValues = buildTemplateValues(session);
2474
- const resolveText = (value) => renderTemplate(value, templateValues);
2475
- contextState = {
2476
- ...contextState,
2477
- filledFields: contextState.filledFields.map((field) => ({
2478
- ...field,
2479
- label: resolveText(field.label) ?? field.label
2480
- })),
2481
- missingRequired: contextState.missingRequired.map((field) => ({
2482
- ...field,
2483
- label: resolveText(field.label) ?? field.label,
2484
- description: resolveText(field.description),
2485
- askPrompt: resolveText(field.askPrompt)
2486
- })),
2487
- uncertainFields: contextState.uncertainFields.map((field) => ({
2488
- ...field,
2489
- label: resolveText(field.label) ?? field.label
2490
- })),
2491
- nextField: contextState.nextField ? resolveControlTemplates(contextState.nextField, templateValues) : null
2492
- };
2493
- contextText = `# Active Form: ${form?.name || session.formId}
2494
-
2495
- `;
2496
- contextText += `Progress: ${contextState.progress}%
2497
-
2498
- `;
2499
- if (contextState.filledFields.length > 0) {
2500
- contextText += `## Collected Information
2501
- `;
2502
- for (const field of contextState.filledFields) {
2503
- contextText += `- ${field.label}: ${field.displayValue}
2504
- `;
2505
- }
2506
- contextText += `
2507
- `;
2508
- }
2509
- if (contextState.missingRequired.length > 0) {
2510
- contextText += `## Still Needed
2511
- `;
2512
- for (const field of contextState.missingRequired) {
2513
- contextText += `- ${field.label}${field.description ? ` (${field.description})` : ""}
2514
- `;
2515
- }
2516
- contextText += `
2517
- `;
2518
- }
2519
- if (contextState.uncertainFields.length > 0) {
2520
- contextText += `## Needs Confirmation
2521
- `;
2522
- for (const field of contextState.uncertainFields) {
2523
- contextText += `- ${field.label}: "${field.value}" (${Math.round(field.confidence * 100)}% confident)
2524
- `;
2525
- }
2526
- contextText += `
2527
- `;
2528
- }
2529
- if (contextState.pendingExternalFields.length > 0) {
2530
- contextText += `## Waiting For External Action
2531
- `;
2532
- for (const field of contextState.pendingExternalFields) {
2533
- const ageMs = Date.now() - field.activatedAt;
2534
- const ageMin = Math.floor(ageMs / 60000);
2535
- const ageText = ageMin < 1 ? "just now" : `${ageMin}m ago`;
2536
- contextText += `- ${field.label}: ${field.instructions} (started ${ageText})
2537
- `;
2538
- if (field.address) {
2539
- contextText += ` Address: ${field.address}
2540
- `;
2541
- }
2542
- }
2543
- contextText += `
2544
- `;
2545
- }
2546
- contextText += `## Agent Guidance
2547
- `;
2548
- if (contextState.pendingExternalFields.length > 0) {
2549
- const pending = contextState.pendingExternalFields[0];
2550
- contextText += `Waiting for external action. Remind user: "${pending.instructions}"
2551
- `;
2552
- } else if (contextState.pendingCancelConfirmation) {
2553
- contextText += `User is trying to cancel. Confirm: "You've spent time on this. Are you sure you want to cancel?"
2554
- `;
2555
- } else if (contextState.uncertainFields.length > 0) {
2556
- const uncertain = contextState.uncertainFields[0];
2557
- contextText += `Ask user to confirm: "I understood your ${uncertain.label} as '${uncertain.value}'. Is that correct?"
2558
- `;
2559
- } else if (contextState.nextField) {
2560
- const next = contextState.nextField;
2561
- const prompt = next.askPrompt || `Ask for their ${next.label}`;
2562
- contextText += `Next: ${prompt}
2563
- `;
2564
- if (next.example) {
2565
- contextText += `Example: "${next.example}"
2566
- `;
2567
- }
2568
- } else if (contextState.status === "ready") {
2569
- contextText += `All fields collected! Nudge user to submit: "I have everything I need. Ready to submit?"
2570
- `;
2571
- }
2572
- contextText += `
2573
- `;
2574
- contextText += `## User Can Say
2575
- `;
2576
- contextText += `- Provide information for any field
2577
- `;
2578
- contextText += `- "undo" or "go back" to revert last change
2579
- `;
2580
- contextText += `- "skip" to skip optional fields
2581
- `;
2582
- contextText += `- "why?" to get explanation about a field
2583
- `;
2584
- contextText += `- "how far?" to check progress
2585
- `;
2586
- contextText += `- "submit" or "done" when ready
2587
- `;
2588
- contextText += `- "save for later" to stash the form
2589
- `;
2590
- contextText += `- "cancel" to abandon the form
2591
- `;
2592
- } else {
2593
- contextState = {
2594
- hasActiveForm: false,
2595
- progress: 0,
2596
- filledFields: [],
2597
- missingRequired: [],
2598
- uncertainFields: [],
2599
- nextField: null,
2600
- stashedCount: stashed.length,
2601
- pendingExternalFields: []
2602
- };
2603
- }
2604
- if (stashed.length > 0) {
2605
- contextText += `
2606
- ## Saved Forms
2607
- `;
2608
- contextText += `User has ${stashed.length} saved form(s). They can say "resume" or "continue" to restore one.
2609
- `;
2610
- for (const s of stashed) {
2611
- const form = formService.getForm(s.formId);
2612
- const ctx = formService.getSessionContext(s);
2613
- contextText += `- ${form?.name || s.formId} (${ctx.progress}% complete)
2614
- `;
2615
- }
2616
- }
2617
- return {
2618
- data: JSON.parse(JSON.stringify(contextState)),
2619
- values: {
2620
- formContext: contextText,
2621
- hasActiveForm: String(contextState.hasActiveForm),
2622
- formProgress: String(contextState.progress),
2623
- formStatus: contextState.status || "",
2624
- stashedCount: String(stashed.length)
2625
- },
2626
- text: contextText
2627
- };
2628
- } catch (error) {
2629
- logger4.error("[FormContextProvider] Error:", String(error));
2630
- return {
2631
- data: { hasActiveForm: false, error: true },
2632
- values: { formContext: "Error loading form context." },
2633
- text: "Error loading form context."
2634
- };
2635
- }
2636
- }
2637
- };
2638
- context_default = formContextProvider;
2639
- });
2640
-
2641
- // index.ts
2642
- init_builtins();
2643
- init_validation();
2644
- init_storage();
2645
- init_extraction();
2646
- init_types();
2
+ BUILTIN_TYPES,
3
+ BUILTIN_TYPE_MAP,
4
+ FORM_AUTOFILL_COMPONENT,
5
+ FORM_CONTROL_DEFAULTS,
6
+ FORM_DEFINITION_DEFAULTS,
7
+ FORM_SESSION_COMPONENT,
8
+ FORM_SUBMISSION_COMPONENT,
9
+ FormService,
10
+ deleteSession,
11
+ getActiveSession,
12
+ getAllActiveSessions,
13
+ getAutofillData,
14
+ getBuiltinType,
15
+ getStashedSessions,
16
+ getSubmissions,
17
+ isBuiltinType,
18
+ registerBuiltinTypes,
19
+ saveAutofillData,
20
+ saveSession,
21
+ saveSubmission
22
+ } from "./chunk-R4VBS2YK.js";
23
+ import {
24
+ formRestoreAction
25
+ } from "./chunk-TBCL2ILB.js";
26
+ import {
27
+ detectCorrection,
28
+ extractSingleField,
29
+ formEvaluator,
30
+ llmIntentAndExtract
31
+ } from "./chunk-XHECCAUT.js";
32
+ import {
33
+ clearTypeHandlers,
34
+ formatValue,
35
+ getTypeHandler,
36
+ matchesMimeType,
37
+ parseValue,
38
+ registerTypeHandler,
39
+ validateField
40
+ } from "./chunk-ARWZY3NX.js";
41
+ import {
42
+ hasDataToExtract,
43
+ isLifecycleIntent,
44
+ isUXIntent,
45
+ quickIntentDetect
46
+ } from "./chunk-YTWANJ3R.js";
47
+ import {
48
+ formContextProvider
49
+ } from "./chunk-4B5QLNVA.js";
50
+ import "./chunk-WY4WK3HD.js";
2647
51
 
2648
52
  // src/ttl.ts
2649
- init_types();
2650
53
  function calculateTTL(session, form) {
2651
54
  const config = form?.ttl || {};
2652
55
  const minDays = config.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays;
2653
56
  const maxDays = config.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays;
2654
57
  const multiplier = config.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier;
2655
- const minutesSpent = session.effort.timeSpentMs / 60000;
58
+ const minutesSpent = session.effort.timeSpentMs / 6e4;
2656
59
  const effortDays = minutesSpent * multiplier;
2657
60
  const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
2658
- return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
61
+ return Date.now() + ttlDays * 24 * 60 * 60 * 1e3;
2659
62
  }
2660
63
  function shouldNudge(session, form) {
2661
64
  const nudgeConfig = form?.nudge;
@@ -2667,14 +70,14 @@ function shouldNudge(session, form) {
2667
70
  return false;
2668
71
  }
2669
72
  const afterInactiveHours = nudgeConfig?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours;
2670
- const inactiveMs = afterInactiveHours * 60 * 60 * 1000;
73
+ const inactiveMs = afterInactiveHours * 60 * 60 * 1e3;
2671
74
  const timeSinceInteraction = Date.now() - session.effort.lastInteractionAt;
2672
75
  if (timeSinceInteraction < inactiveMs) {
2673
76
  return false;
2674
77
  }
2675
78
  if (session.lastNudgeAt) {
2676
79
  const timeSinceNudge = Date.now() - session.lastNudgeAt;
2677
- if (timeSinceNudge < 24 * 60 * 60 * 1000) {
80
+ if (timeSinceNudge < 24 * 60 * 60 * 1e3) {
2678
81
  return false;
2679
82
  }
2680
83
  }
@@ -2687,7 +90,7 @@ function isExpired(session) {
2687
90
  return session.expiresAt < Date.now();
2688
91
  }
2689
92
  function shouldConfirmCancel(session) {
2690
- const minEffortMs = 5 * 60 * 1000;
93
+ const minEffortMs = 5 * 60 * 1e3;
2691
94
  return session.effort.timeSpentMs > minEffortMs;
2692
95
  }
2693
96
  function formatTimeRemaining(session) {
@@ -2695,7 +98,7 @@ function formatTimeRemaining(session) {
2695
98
  if (remaining <= 0) {
2696
99
  return "expired";
2697
100
  }
2698
- const hours = Math.floor(remaining / (60 * 60 * 1000));
101
+ const hours = Math.floor(remaining / (60 * 60 * 1e3));
2699
102
  const days = Math.floor(hours / 24);
2700
103
  if (days > 0) {
2701
104
  return `${days} day${days > 1 ? "s" : ""}`;
@@ -2703,11 +106,11 @@ function formatTimeRemaining(session) {
2703
106
  if (hours > 0) {
2704
107
  return `${hours} hour${hours > 1 ? "s" : ""}`;
2705
108
  }
2706
- const minutes = Math.floor(remaining / (60 * 1000));
109
+ const minutes = Math.floor(remaining / (60 * 1e3));
2707
110
  return `${minutes} minute${minutes > 1 ? "s" : ""}`;
2708
111
  }
2709
112
  function formatEffort(session) {
2710
- const minutes = Math.floor(session.effort.timeSpentMs / 60000);
113
+ const minutes = Math.floor(session.effort.timeSpentMs / 6e4);
2711
114
  if (minutes < 1) {
2712
115
  return "just started";
2713
116
  }
@@ -2721,25 +124,41 @@ function formatEffort(session) {
2721
124
  }
2722
125
  return `${hours}h ${remainingMinutes}m`;
2723
126
  }
127
+
2724
128
  // src/defaults.ts
2725
- init_types();
2726
129
  function applyControlDefaults(control) {
2727
130
  return {
131
+ // Required field (must be present)
2728
132
  key: control.key,
133
+ // Derive label from key if not provided
134
+ // WHY: User sees labels, default should be readable
2729
135
  label: control.label || prettify(control.key),
136
+ // Default type is text (most common)
2730
137
  type: control.type || FORM_CONTROL_DEFAULTS.type,
138
+ // Default not required (explicit opt-in)
139
+ // WHY: Safer to require opt-in for required fields
2731
140
  required: control.required ?? FORM_CONTROL_DEFAULTS.required,
141
+ // Default confidence threshold for auto-acceptance
142
+ // WHY 0.8: High enough to be confident, low enough to be useful
2732
143
  confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
144
+ // Spread remaining properties (override defaults)
2733
145
  ...control
2734
146
  };
2735
147
  }
2736
148
  function applyFormDefaults(form) {
2737
149
  return {
150
+ // Required fields
2738
151
  id: form.id,
152
+ // Derive name from id if not provided
2739
153
  name: form.name || prettify(form.id),
154
+ // Default version for schema tracking
2740
155
  version: form.version ?? FORM_DEFINITION_DEFAULTS.version,
156
+ // Default status is active
2741
157
  status: form.status ?? FORM_DEFINITION_DEFAULTS.status,
158
+ // Apply defaults to all controls
2742
159
  controls: (form.controls || []).map(applyControlDefaults),
160
+ // UX defaults - enable helpful features by default
161
+ // WHY enable by default: Better user experience out of the box
2743
162
  ux: {
2744
163
  allowUndo: form.ux?.allowUndo ?? FORM_DEFINITION_DEFAULTS.ux.allowUndo,
2745
164
  allowSkip: form.ux?.allowSkip ?? FORM_DEFINITION_DEFAULTS.ux.allowSkip,
@@ -2748,190 +167,266 @@ function applyFormDefaults(form) {
2748
167
  showExplanations: form.ux?.showExplanations ?? FORM_DEFINITION_DEFAULTS.ux.showExplanations,
2749
168
  allowAutofill: form.ux?.allowAutofill ?? FORM_DEFINITION_DEFAULTS.ux.allowAutofill
2750
169
  },
170
+ // TTL defaults - generous retention
171
+ // WHY generous: Better to keep data too long than lose user work
2751
172
  ttl: {
2752
173
  minDays: form.ttl?.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays,
2753
174
  maxDays: form.ttl?.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays,
2754
175
  effortMultiplier: form.ttl?.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier
2755
176
  },
177
+ // Nudge defaults - helpful but not annoying
2756
178
  nudge: {
2757
179
  enabled: form.nudge?.enabled ?? FORM_DEFINITION_DEFAULTS.nudge.enabled,
2758
180
  afterInactiveHours: form.nudge?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours,
2759
181
  maxNudges: form.nudge?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges,
2760
182
  message: form.nudge?.message
2761
183
  },
184
+ // Debug defaults to off for performance
2762
185
  debug: form.debug ?? FORM_DEFINITION_DEFAULTS.debug,
186
+ // Spread remaining properties (override defaults)
2763
187
  ...form
2764
188
  };
2765
189
  }
2766
190
  function prettify(key) {
2767
191
  return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2768
192
  }
193
+
2769
194
  // src/builder.ts
2770
- class ControlBuilder {
195
+ var ControlBuilder = class _ControlBuilder {
196
+ /** Partial control being built */
2771
197
  control;
198
+ /**
199
+ * Create a new ControlBuilder.
200
+ *
201
+ * @param key - The unique key for this control
202
+ */
2772
203
  constructor(key) {
2773
204
  this.control = { key };
2774
205
  }
206
+ // ═══ STATIC FACTORIES ═══
207
+ // WHY static factories: Cleaner than `new ControlBuilder(key).type('text')`
208
+ /** Create a generic field builder */
2775
209
  static field(key) {
2776
- return new ControlBuilder(key);
210
+ return new _ControlBuilder(key);
2777
211
  }
212
+ /** Create a text field */
2778
213
  static text(key) {
2779
- return new ControlBuilder(key).type("text");
214
+ return new _ControlBuilder(key).type("text");
2780
215
  }
216
+ /** Create an email field */
2781
217
  static email(key) {
2782
- return new ControlBuilder(key).type("email");
218
+ return new _ControlBuilder(key).type("email");
2783
219
  }
220
+ /** Create a number field */
2784
221
  static number(key) {
2785
- return new ControlBuilder(key).type("number");
222
+ return new _ControlBuilder(key).type("number");
2786
223
  }
224
+ /** Create a boolean (yes/no) field */
2787
225
  static boolean(key) {
2788
- return new ControlBuilder(key).type("boolean");
226
+ return new _ControlBuilder(key).type("boolean");
2789
227
  }
228
+ /** Create a select field with options */
2790
229
  static select(key, options) {
2791
- return new ControlBuilder(key).type("select").options(options);
230
+ return new _ControlBuilder(key).type("select").options(options);
2792
231
  }
232
+ /** Create a date field */
2793
233
  static date(key) {
2794
- return new ControlBuilder(key).type("date");
234
+ return new _ControlBuilder(key).type("date");
2795
235
  }
236
+ /** Create a file upload field */
2796
237
  static file(key) {
2797
- return new ControlBuilder(key).type("file");
238
+ return new _ControlBuilder(key).type("file");
2798
239
  }
240
+ // ═══ TYPE ═══
241
+ /** Set the field type */
2799
242
  type(type) {
2800
243
  this.control.type = type;
2801
244
  return this;
2802
245
  }
246
+ // ═══ BEHAVIOR ═══
247
+ /** Mark field as required */
2803
248
  required() {
2804
249
  this.control.required = true;
2805
250
  return this;
2806
251
  }
252
+ /** Mark field as optional (default) */
2807
253
  optional() {
2808
254
  this.control.required = false;
2809
255
  return this;
2810
256
  }
257
+ /** Mark field as hidden (extract silently, never ask) */
2811
258
  hidden() {
2812
259
  this.control.hidden = true;
2813
260
  return this;
2814
261
  }
262
+ /** Mark field as sensitive (don't echo value back) */
2815
263
  sensitive() {
2816
264
  this.control.sensitive = true;
2817
265
  return this;
2818
266
  }
267
+ /** Mark field as readonly (can't change after set) */
2819
268
  readonly() {
2820
269
  this.control.readonly = true;
2821
270
  return this;
2822
271
  }
272
+ /** Mark field as accepting multiple values */
2823
273
  multiple() {
2824
274
  this.control.multiple = true;
2825
275
  return this;
2826
276
  }
277
+ // ═══ VALIDATION ═══
278
+ /** Set regex pattern for validation */
2827
279
  pattern(regex) {
2828
280
  this.control.pattern = regex;
2829
281
  return this;
2830
282
  }
283
+ /** Set minimum value (for numbers) or minimum length (via minLength) */
2831
284
  min(n) {
2832
285
  this.control.min = n;
2833
286
  return this;
2834
287
  }
288
+ /** Set maximum value (for numbers) or maximum length (via maxLength) */
2835
289
  max(n) {
2836
290
  this.control.max = n;
2837
291
  return this;
2838
292
  }
293
+ /** Set minimum string length */
2839
294
  minLength(n) {
2840
295
  this.control.minLength = n;
2841
296
  return this;
2842
297
  }
298
+ /** Set maximum string length */
2843
299
  maxLength(n) {
2844
300
  this.control.maxLength = n;
2845
301
  return this;
2846
302
  }
303
+ /** Set allowed values (enum) */
2847
304
  enum(values) {
2848
305
  this.control.enum = values;
2849
306
  return this;
2850
307
  }
308
+ /** Set select options */
2851
309
  options(opts) {
2852
310
  this.control.options = opts;
2853
311
  return this;
2854
312
  }
313
+ // ═══ AGENT HINTS ═══
314
+ // WHY agent hints: Help LLM extract values correctly
315
+ /** Set human-readable label */
2855
316
  label(label) {
2856
317
  this.control.label = label;
2857
318
  return this;
2858
319
  }
320
+ /** Set custom prompt for asking this field */
2859
321
  ask(prompt) {
2860
322
  this.control.askPrompt = prompt;
2861
323
  return this;
2862
324
  }
325
+ /** Set description for LLM context */
2863
326
  description(desc) {
2864
327
  this.control.description = desc;
2865
328
  return this;
2866
329
  }
330
+ /** Add extraction hints (keywords to look for) */
2867
331
  hint(...hints) {
2868
332
  this.control.extractHints = hints;
2869
333
  return this;
2870
334
  }
335
+ /** Set example value for "give me an example" */
2871
336
  example(value) {
2872
337
  this.control.example = value;
2873
338
  return this;
2874
339
  }
340
+ /** Set confidence threshold for auto-acceptance */
2875
341
  confirmThreshold(n) {
2876
342
  this.control.confirmThreshold = n;
2877
343
  return this;
2878
344
  }
345
+ // ═══ FILE OPTIONS ═══
346
+ /** Set accepted MIME types for file upload */
2879
347
  accept(mimeTypes) {
2880
348
  this.control.file = { ...this.control.file, accept: mimeTypes };
2881
349
  return this;
2882
350
  }
351
+ /** Set maximum file size in bytes */
2883
352
  maxSize(bytes) {
2884
353
  this.control.file = { ...this.control.file, maxSize: bytes };
2885
354
  return this;
2886
355
  }
356
+ /** Set maximum number of files */
2887
357
  maxFiles(n) {
2888
358
  this.control.file = { ...this.control.file, maxFiles: n };
2889
359
  return this;
2890
360
  }
361
+ // ═══ ACCESS ═══
362
+ /** Set roles that can see/fill this field */
2891
363
  roles(...roles) {
2892
364
  this.control.roles = roles;
2893
365
  return this;
2894
366
  }
367
+ // ═══ DEFAULTS & CONDITIONS ═══
368
+ /** Set default value */
2895
369
  default(value) {
2896
370
  this.control.defaultValue = value;
2897
371
  return this;
2898
372
  }
373
+ /** Set dependency on another field */
2899
374
  dependsOn(field, condition = "exists", value) {
2900
375
  this.control.dependsOn = { field, condition, value };
2901
376
  return this;
2902
377
  }
378
+ // ═══ DATABASE ═══
379
+ /** Set database column name (defaults to key) */
2903
380
  dbbind(columnName) {
2904
381
  this.control.dbbind = columnName;
2905
382
  return this;
2906
383
  }
384
+ // ═══ UI ═══
385
+ /** Set section name for grouping */
2907
386
  section(name) {
2908
387
  this.control.ui = { ...this.control.ui, section: name };
2909
388
  return this;
2910
389
  }
390
+ /** Set display order within section */
2911
391
  order(n) {
2912
392
  this.control.ui = { ...this.control.ui, order: n };
2913
393
  return this;
2914
394
  }
395
+ /** Set placeholder text */
2915
396
  placeholder(text) {
2916
397
  this.control.ui = { ...this.control.ui, placeholder: text };
2917
398
  return this;
2918
399
  }
400
+ /** Set help text */
2919
401
  helpText(text) {
2920
402
  this.control.ui = { ...this.control.ui, helpText: text };
2921
403
  return this;
2922
404
  }
405
+ /** Set custom widget type */
2923
406
  widget(type) {
2924
407
  this.control.ui = { ...this.control.ui, widget: type };
2925
408
  return this;
2926
409
  }
410
+ // ═══ I18N ═══
411
+ /** Add localized text for a locale */
2927
412
  i18n(locale, translations) {
2928
413
  this.control.i18n = { ...this.control.i18n, [locale]: translations };
2929
414
  return this;
2930
415
  }
416
+ // ═══ META ═══
417
+ /** Add custom metadata */
2931
418
  meta(key, value) {
2932
419
  this.control.meta = { ...this.control.meta, [key]: value };
2933
420
  return this;
2934
421
  }
422
+ // ═══ BUILD ═══
423
+ /**
424
+ * Build the final FormControl.
425
+ *
426
+ * Applies defaults and validates the control.
427
+ *
428
+ * @returns Complete FormControl object
429
+ */
2935
430
  build() {
2936
431
  const control = {
2937
432
  key: this.control.key,
@@ -2941,131 +436,190 @@ class ControlBuilder {
2941
436
  };
2942
437
  return control;
2943
438
  }
2944
- }
2945
-
2946
- class FormBuilder {
439
+ };
440
+ var FormBuilder = class _FormBuilder {
441
+ /** Partial form being built */
2947
442
  form;
443
+ /**
444
+ * Create a new FormBuilder.
445
+ *
446
+ * @param id - Unique form identifier
447
+ */
2948
448
  constructor(id) {
2949
449
  this.form = { id, controls: [] };
2950
450
  }
451
+ // ═══ STATIC FACTORY ═══
452
+ /** Create a new form builder */
2951
453
  static create(id) {
2952
- return new FormBuilder(id);
454
+ return new _FormBuilder(id);
2953
455
  }
456
+ // ═══ METADATA ═══
457
+ /** Set form name */
2954
458
  name(name) {
2955
459
  this.form.name = name;
2956
460
  return this;
2957
461
  }
462
+ /** Set form description */
2958
463
  description(desc) {
2959
464
  this.form.description = desc;
2960
465
  return this;
2961
466
  }
467
+ /** Set form version */
2962
468
  version(v) {
2963
469
  this.form.version = v;
2964
470
  return this;
2965
471
  }
472
+ // ═══ CONTROLS ═══
473
+ /**
474
+ * Add a control to the form.
475
+ *
476
+ * Accepts either a ControlBuilder (calls .build()) or a FormControl.
477
+ */
2966
478
  control(builder) {
2967
479
  const ctrl = builder instanceof ControlBuilder ? builder.build() : builder;
2968
480
  this.form.controls?.push(ctrl);
2969
481
  return this;
2970
482
  }
483
+ /** Add multiple controls */
2971
484
  controls(...builders) {
2972
485
  for (const builder of builders) {
2973
486
  this.control(builder);
2974
487
  }
2975
488
  return this;
2976
489
  }
490
+ // ═══ SHORTHAND CONTROLS ═══
491
+ // WHY shorthands: Quick form prototyping
492
+ /** Add required text fields */
2977
493
  required(...keys) {
2978
494
  for (const key of keys) {
2979
495
  this.control(ControlBuilder.field(key).required());
2980
496
  }
2981
497
  return this;
2982
498
  }
499
+ /** Add optional text fields */
2983
500
  optional(...keys) {
2984
501
  for (const key of keys) {
2985
502
  this.control(ControlBuilder.field(key));
2986
503
  }
2987
504
  return this;
2988
505
  }
506
+ // ═══ PERMISSIONS ═══
507
+ /** Set roles that can start this form */
2989
508
  roles(...roles) {
2990
509
  this.form.roles = roles;
2991
510
  return this;
2992
511
  }
512
+ /** Allow multiple submissions per user */
2993
513
  allowMultiple() {
2994
514
  this.form.allowMultiple = true;
2995
515
  return this;
2996
516
  }
517
+ // ═══ UX ═══
518
+ /** Disable undo functionality */
2997
519
  noUndo() {
2998
520
  this.form.ux = { ...this.form.ux, allowUndo: false };
2999
521
  return this;
3000
522
  }
523
+ /** Disable skip functionality */
3001
524
  noSkip() {
3002
525
  this.form.ux = { ...this.form.ux, allowSkip: false };
3003
526
  return this;
3004
527
  }
528
+ /** Disable autofill */
3005
529
  noAutofill() {
3006
530
  this.form.ux = { ...this.form.ux, allowAutofill: false };
3007
531
  return this;
3008
532
  }
533
+ /** Set maximum undo steps */
3009
534
  maxUndoSteps(n) {
3010
535
  this.form.ux = { ...this.form.ux, maxUndoSteps: n };
3011
536
  return this;
3012
537
  }
538
+ // ═══ TTL ═══
539
+ /** Configure TTL (time-to-live) settings */
3013
540
  ttl(config) {
3014
541
  this.form.ttl = { ...this.form.ttl, ...config };
3015
542
  return this;
3016
543
  }
544
+ // ═══ NUDGE ═══
545
+ /** Disable nudge messages */
3017
546
  noNudge() {
3018
547
  this.form.nudge = { ...this.form.nudge, enabled: false };
3019
548
  return this;
3020
549
  }
550
+ /** Set inactivity hours before nudge */
3021
551
  nudgeAfter(hours) {
3022
552
  this.form.nudge = { ...this.form.nudge, afterInactiveHours: hours };
3023
553
  return this;
3024
554
  }
555
+ /** Set custom nudge message */
3025
556
  nudgeMessage(message) {
3026
557
  this.form.nudge = { ...this.form.nudge, message };
3027
558
  return this;
3028
559
  }
560
+ // ═══ HOOKS ═══
561
+ // WHY hooks: Allow consuming plugins to handle form events
562
+ /** Set task worker to call on session start */
3029
563
  onStart(workerName) {
3030
564
  this.form.hooks = { ...this.form.hooks, onStart: workerName };
3031
565
  return this;
3032
566
  }
567
+ /** Set task worker to call on field change */
3033
568
  onFieldChange(workerName) {
3034
569
  this.form.hooks = { ...this.form.hooks, onFieldChange: workerName };
3035
570
  return this;
3036
571
  }
572
+ /** Set task worker to call when form is ready to submit */
3037
573
  onReady(workerName) {
3038
574
  this.form.hooks = { ...this.form.hooks, onReady: workerName };
3039
575
  return this;
3040
576
  }
577
+ /** Set task worker to call on submission */
3041
578
  onSubmit(workerName) {
3042
579
  this.form.hooks = { ...this.form.hooks, onSubmit: workerName };
3043
580
  return this;
3044
581
  }
582
+ /** Set task worker to call on cancellation */
3045
583
  onCancel(workerName) {
3046
584
  this.form.hooks = { ...this.form.hooks, onCancel: workerName };
3047
585
  return this;
3048
586
  }
587
+ /** Set task worker to call on expiration */
3049
588
  onExpire(workerName) {
3050
589
  this.form.hooks = { ...this.form.hooks, onExpire: workerName };
3051
590
  return this;
3052
591
  }
592
+ /** Set multiple hooks at once */
3053
593
  hooks(hooks) {
3054
594
  this.form.hooks = { ...this.form.hooks, ...hooks };
3055
595
  return this;
3056
596
  }
597
+ // ═══ DEBUG ═══
598
+ /** Enable debug mode (logs extraction reasoning) */
3057
599
  debug() {
3058
600
  this.form.debug = true;
3059
601
  return this;
3060
602
  }
603
+ // ═══ I18N ═══
604
+ /** Add localized form text */
3061
605
  i18n(locale, translations) {
3062
606
  this.form.i18n = { ...this.form.i18n, [locale]: translations };
3063
607
  return this;
3064
608
  }
609
+ // ═══ META ═══
610
+ /** Add custom metadata */
3065
611
  meta(key, value) {
3066
612
  this.form.meta = { ...this.form.meta, [key]: value };
3067
613
  return this;
3068
614
  }
615
+ // ═══ BUILD ═══
616
+ /**
617
+ * Build the final FormDefinition.
618
+ *
619
+ * Applies defaults and validates the form.
620
+ *
621
+ * @returns Complete FormDefinition object
622
+ */
3069
623
  build() {
3070
624
  const form = {
3071
625
  id: this.form.id,
@@ -3075,132 +629,42 @@ class FormBuilder {
3075
629
  };
3076
630
  return form;
3077
631
  }
3078
- }
632
+ };
3079
633
  var Form = FormBuilder;
3080
634
  var C = ControlBuilder;
3081
635
  function prettify2(key) {
3082
636
  return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3083
637
  }
3084
638
 
3085
- // index.ts
3086
- init_service();
3087
- init_restore();
3088
- init_extractor();
3089
- init_context();
3090
-
3091
- // src/tasks/nudge.ts
3092
- import { logger as logger5 } from "@elizaos/core";
3093
- var formNudgeWorker = {
3094
- name: "form_nudge_check",
3095
- validate: async (_runtime, _message, _state) => {
3096
- return true;
3097
- },
3098
- execute: async (runtime, _options, _task) => {
3099
- try {
3100
- const formService = runtime.getService("FORM");
3101
- if (!formService) {
3102
- logger5.debug("[FormNudge] Form service not available");
3103
- return;
3104
- }
3105
- logger5.debug("[FormNudge] Nudge check cycle completed");
3106
- } catch (error) {
3107
- logger5.error("[FormNudge] Error during nudge check:", String(error));
3108
- }
3109
- }
3110
- };
3111
- async function processEntityNudges(runtime, entityId) {
3112
- const formService = runtime.getService("FORM");
3113
- if (!formService)
3114
- return;
3115
- const activeSessions = await formService.getAllActiveSessions(entityId);
3116
- const stashedSessions = await formService.getStashedSessions(entityId);
3117
- const allSessions = [...activeSessions, ...stashedSessions];
3118
- const now = Date.now();
3119
- const expirationWarningMs = 24 * 60 * 60 * 1000;
3120
- for (const session of allSessions) {
3121
- const form = formService.getForm(session.formId);
3122
- if (session.expiresAt < now) {
3123
- session.status = "expired";
3124
- await formService.saveSession(session);
3125
- if (form?.hooks?.onExpire) {
3126
- const worker = runtime.getTaskWorker(form.hooks.onExpire);
3127
- if (worker) {
3128
- try {
3129
- await worker.execute(runtime, { session, form }, {});
3130
- } catch (error) {
3131
- logger5.error("[FormNudge] onExpire hook failed:", String(error));
3132
- }
3133
- }
3134
- }
3135
- logger5.debug(`[FormNudge] Session ${session.id} expired`);
3136
- continue;
3137
- }
3138
- if (isExpiringSoon(session, expirationWarningMs) && !session.expirationWarned) {
3139
- await sendExpirationWarning(runtime, session, form);
3140
- session.expirationWarned = true;
3141
- await formService.saveSession(session);
3142
- continue;
3143
- }
3144
- if (session.status === "stashed" && shouldNudge(session, form)) {
3145
- await sendNudge(runtime, session, form);
3146
- session.nudgeCount = (session.nudgeCount || 0) + 1;
3147
- session.lastNudgeAt = now;
3148
- await formService.saveSession(session);
3149
- }
3150
- }
3151
- }
3152
- async function sendNudge(runtime, session, form) {
3153
- const message = form?.nudge?.message || `You have an unfinished "${form?.name || "form"}". Would you like to continue?`;
3154
- try {
3155
- if (typeof runtime.sendMessageToRoom === "function") {
3156
- await runtime.sendMessageToRoom(session.roomId, {
3157
- text: message
3158
- });
3159
- logger5.debug(`[FormNudge] Sent nudge for session in room ${session.roomId}`);
3160
- }
3161
- } catch (error) {
3162
- logger5.error("[FormNudge] Failed to send nudge:", String(error));
3163
- }
3164
- }
3165
- async function sendExpirationWarning(runtime, session, form) {
3166
- const remaining = formatTimeRemaining(session);
3167
- const message = `Your "${form?.name || "form"}" form will expire in ${remaining}. Say "resume" to keep working on it.`;
3168
- try {
3169
- if (typeof runtime.sendMessageToRoom === "function") {
3170
- await runtime.sendMessageToRoom(session.roomId, {
3171
- text: message
3172
- });
3173
- logger5.debug(`[FormNudge] Sent expiration warning for session in room ${session.roomId}`);
3174
- }
3175
- } catch (error) {
3176
- logger5.error("[FormNudge] Failed to send expiration warning:", String(error));
3177
- }
3178
- }
3179
-
3180
- // index.ts
639
+ // src/index.ts
3181
640
  var formPlugin = {
3182
641
  name: "form",
3183
642
  description: "Agent-native conversational forms for data collection",
643
+ // Service for form management
3184
644
  services: [
645
+ // FormService is registered as a static class
646
+ // It will be instantiated by the runtime
3185
647
  {
3186
648
  serviceType: "FORM",
3187
649
  start: async (runtime) => {
3188
- const { FormService: FormService2 } = await Promise.resolve().then(() => (init_service(), exports_service));
650
+ const { FormService: FormService2 } = await import("./service-TCCXKV3T.js");
3189
651
  return FormService2.start(runtime);
3190
652
  }
3191
653
  }
3192
654
  ],
655
+ // Provider for form context
3193
656
  providers: [
657
+ // Import dynamically to avoid circular deps
3194
658
  {
3195
659
  name: "FORM_CONTEXT",
3196
660
  description: "Provides context about active form sessions",
3197
- dynamic: true,
3198
661
  get: async (runtime, message, state) => {
3199
- const { formContextProvider: formContextProvider2 } = await Promise.resolve().then(() => (init_context(), exports_context));
662
+ const { formContextProvider: formContextProvider2 } = await import("./context-MHPFYZZ2.js");
3200
663
  return formContextProvider2.get(runtime, message, state);
3201
664
  }
3202
665
  }
3203
666
  ],
667
+ // Evaluator for field extraction
3204
668
  evaluators: [
3205
669
  {
3206
670
  name: "form_evaluator",
@@ -3208,26 +672,27 @@ var formPlugin = {
3208
672
  similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
3209
673
  examples: [],
3210
674
  validate: async (runtime, message, state) => {
3211
- const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
675
+ const { formEvaluator: formEvaluator2 } = await import("./extractor-UWASKXKD.js");
3212
676
  return formEvaluator2.validate(runtime, message, state);
3213
677
  },
3214
678
  handler: async (runtime, message, state) => {
3215
- const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
679
+ const { formEvaluator: formEvaluator2 } = await import("./extractor-UWASKXKD.js");
3216
680
  return formEvaluator2.handler(runtime, message, state);
3217
681
  }
3218
682
  }
3219
683
  ],
684
+ // Action for restoring stashed forms
3220
685
  actions: [
3221
686
  {
3222
687
  name: "FORM_RESTORE",
3223
688
  similes: ["RESUME_FORM", "CONTINUE_FORM"],
3224
689
  description: "Restore a previously stashed form session",
3225
690
  validate: async (runtime, message, state) => {
3226
- const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
691
+ const { formRestoreAction: formRestoreAction2 } = await import("./restore-S7JLME4H.js");
3227
692
  return formRestoreAction2.validate(runtime, message, state);
3228
693
  },
3229
694
  handler: async (runtime, message, state, options, callback) => {
3230
- const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
695
+ const { formRestoreAction: formRestoreAction2 } = await import("./restore-S7JLME4H.js");
3231
696
  return formRestoreAction2.handler(runtime, message, state, options, callback);
3232
697
  },
3233
698
  examples: [
@@ -3247,63 +712,197 @@ var formPlugin = {
3247
712
  }
3248
713
  ]
3249
714
  };
3250
- var typescript_default = formPlugin;
715
+ var index_default = formPlugin;
3251
716
  export {
3252
- validateField,
3253
- shouldNudge,
3254
- shouldConfirmCancel,
3255
- saveSubmission,
3256
- saveSession,
3257
- saveAutofillData,
3258
- registerTypeHandler,
3259
- registerBuiltinTypes,
3260
- quickIntentDetect,
3261
- processEntityNudges,
3262
- prettify,
3263
- parseValue,
3264
- matchesMimeType,
3265
- llmIntentAndExtract,
3266
- isUXIntent,
3267
- isLifecycleIntent,
3268
- isExpiringSoon,
3269
- isExpired,
3270
- isBuiltinType,
3271
- hasDataToExtract,
3272
- getTypeHandler,
3273
- getSubmissions,
3274
- getStashedSessions,
3275
- getBuiltinType,
3276
- getAutofillData,
3277
- getAllActiveSessions,
3278
- getActiveSession,
3279
- formatValue,
3280
- formatTimeRemaining,
3281
- formatEffort,
3282
- formRestoreAction,
3283
- formPlugin,
3284
- formNudgeWorker,
3285
- formEvaluator,
3286
- formContextProvider,
3287
- extractSingleField,
3288
- detectCorrection,
3289
- deleteSession,
3290
- typescript_default as default,
3291
- clearTypeHandlers,
3292
- calculateTTL,
3293
- applyFormDefaults,
3294
- applyControlDefaults,
3295
- FormService,
3296
- FormBuilder,
3297
- Form,
3298
- FORM_SUBMISSION_COMPONENT,
3299
- FORM_SESSION_COMPONENT,
3300
- FORM_DEFINITION_DEFAULTS,
3301
- FORM_CONTROL_DEFAULTS,
3302
- FORM_AUTOFILL_COMPONENT,
3303
- ControlBuilder,
3304
- C,
717
+ BUILTIN_TYPES,
3305
718
  BUILTIN_TYPE_MAP,
3306
- BUILTIN_TYPES
719
+ C,
720
+ ControlBuilder,
721
+ FORM_AUTOFILL_COMPONENT,
722
+ FORM_CONTROL_DEFAULTS,
723
+ FORM_DEFINITION_DEFAULTS,
724
+ FORM_SESSION_COMPONENT,
725
+ FORM_SUBMISSION_COMPONENT,
726
+ Form,
727
+ FormBuilder,
728
+ FormService,
729
+ applyControlDefaults,
730
+ applyFormDefaults,
731
+ calculateTTL,
732
+ clearTypeHandlers,
733
+ index_default as default,
734
+ deleteSession,
735
+ detectCorrection,
736
+ extractSingleField,
737
+ formContextProvider,
738
+ formEvaluator,
739
+ formPlugin,
740
+ formRestoreAction,
741
+ formatEffort,
742
+ formatTimeRemaining,
743
+ formatValue,
744
+ getActiveSession,
745
+ getAllActiveSessions,
746
+ getAutofillData,
747
+ getBuiltinType,
748
+ getStashedSessions,
749
+ getSubmissions,
750
+ getTypeHandler,
751
+ hasDataToExtract,
752
+ isBuiltinType,
753
+ isExpired,
754
+ isExpiringSoon,
755
+ isLifecycleIntent,
756
+ isUXIntent,
757
+ llmIntentAndExtract,
758
+ matchesMimeType,
759
+ parseValue,
760
+ prettify,
761
+ quickIntentDetect,
762
+ registerBuiltinTypes,
763
+ registerTypeHandler,
764
+ saveAutofillData,
765
+ saveSession,
766
+ saveSubmission,
767
+ shouldConfirmCancel,
768
+ shouldNudge,
769
+ validateField
3307
770
  };
3308
-
3309
- //# debugId=DC90FD3CDE2C7E4764756E2164756E21
771
+ /**
772
+ * @module @elizaos/plugin-form
773
+ * @description Guardrails for agent-guided user journeys
774
+ *
775
+ * @author Odilitime
776
+ * @copyright 2025 Odilitime
777
+ * @license MIT
778
+ *
779
+ * ## The Core Insight
780
+ *
781
+ * Forms aren't just about data collection - they're **guardrails for agents**.
782
+ *
783
+ * Without structure, agents wander. They forget context, miss required
784
+ * information, and can't reliably guide users to outcomes. This plugin
785
+ * gives agents the tools to follow conventions and shepherd users through
786
+ * structured journeys - registrations, orders, applications, onboarding flows.
787
+ *
788
+ * **Forms define the path. Agents follow it. Users reach outcomes.**
789
+ *
790
+ * ## Key Features
791
+ *
792
+ * - **Natural Language Extraction**: "I'm John, 25, john@example.com"
793
+ * - **Two-Tier Intent Detection**: Fast English keywords + LLM fallback
794
+ * - **UX Magic**: Undo, skip, explain, example, progress, autofill
795
+ * - **Smart TTL**: Retention scales with user effort
796
+ * - **Fluent Builder API**: Type-safe form definitions
797
+ * - **Extensible Types**: Register custom field types
798
+ *
799
+ * ## Architecture
800
+ *
801
+ * ```
802
+ * ┌─────────────────────────────────────────────────────────────┐
803
+ * │ Form Plugin │
804
+ * ├─────────────────────────────────────────────────────────────┤
805
+ * │ │
806
+ * │ Provider (FORM_CONTEXT) │
807
+ * │ - Runs BEFORE agent responds │
808
+ * │ - Injects form state into context │
809
+ * │ - Tells agent what to ask next │
810
+ * │ │
811
+ * │ Evaluator (form_evaluator) │
812
+ * │ - Runs AFTER each user message │
813
+ * │ - Detects intent (submit, cancel, undo, etc.) │
814
+ * │ - Extracts field values from natural language │
815
+ * │ - Updates session state │
816
+ * │ │
817
+ * │ Action (FORM_RESTORE) │
818
+ * │ - Preempts REPLY for restore intent │
819
+ * │ - Immediately restores stashed forms │
820
+ * │ │
821
+ * │ Service (FormService) │
822
+ * │ - Manages form definitions │
823
+ * │ - Manages sessions, submissions, autofill │
824
+ * │ - Executes lifecycle hooks │
825
+ * │ │
826
+ * └─────────────────────────────────────────────────────────────┘
827
+ * ```
828
+ *
829
+ * ## Quick Start
830
+ *
831
+ * ### 1. Add plugin to your agent
832
+ *
833
+ * ```typescript
834
+ * import { formPlugin } from '@elizaos/plugin-form';
835
+ *
836
+ * const agent = {
837
+ * plugins: [formPlugin, ...otherPlugins],
838
+ * };
839
+ * ```
840
+ *
841
+ * ### 2. Define a form
842
+ *
843
+ * ```typescript
844
+ * import { Form, C } from '@elizaos/plugin-form';
845
+ *
846
+ * const registrationForm = Form.create('registration')
847
+ * .name('User Registration')
848
+ * .control(C.email('email').required().ask('What email should we use?'))
849
+ * .control(C.text('name').required().ask("What's your name?"))
850
+ * .control(C.number('age').min(13))
851
+ * .onSubmit('handle_registration')
852
+ * .build();
853
+ * ```
854
+ *
855
+ * ### 3. Register and start
856
+ *
857
+ * ```typescript
858
+ * // In your plugin init:
859
+ * const formService = runtime.getService('FORM') as FormService;
860
+ * formService.registerForm(registrationForm);
861
+ *
862
+ * // When you need to collect data:
863
+ * await formService.startSession('registration', entityId, roomId);
864
+ * ```
865
+ *
866
+ * ### 4. Handle submissions
867
+ *
868
+ * ```typescript
869
+ * runtime.registerTaskWorker({
870
+ * name: 'handle_registration',
871
+ * execute: async (runtime, options) => {
872
+ * const { submission } = options;
873
+ * const { email, name, age } = submission.values;
874
+ * // Create user account, etc.
875
+ * }
876
+ * });
877
+ * ```
878
+ *
879
+ * ## User Experience
880
+ *
881
+ * The form plugin handles these user interactions:
882
+ *
883
+ * | User Says | Intent | Result |
884
+ * |-----------|--------|--------|
885
+ * | "I'm John, 25 years old" | fill_form | Extract name=John, age=25 |
886
+ * | "done" / "submit" | submit | Submit the form |
887
+ * | "save for later" | stash | Save and switch contexts |
888
+ * | "resume my form" | restore | Restore stashed form |
889
+ * | "cancel" / "nevermind" | cancel | Abandon form |
890
+ * | "undo" / "go back" | undo | Revert last change |
891
+ * | "skip" | skip | Skip optional field |
892
+ * | "why?" | explain | Explain current field |
893
+ * | "example?" | example | Show example value |
894
+ * | "how far?" | progress | Show completion status |
895
+ * | "same as last time" | autofill | Use saved values |
896
+ *
897
+ * ## Module Exports
898
+ *
899
+ * - **Types**: FormControl, FormDefinition, FormSession, etc.
900
+ * - **Builder**: Form, C (ControlBuilder)
901
+ * - **Service**: FormService
902
+ * - **Utilities**: validateField, parseValue, formatValue
903
+ * - **Plugin**: formPlugin (default export)
904
+ *
905
+ * @see {@link FormService} for form management API
906
+ * @see {@link FormBuilder} for fluent form definition
907
+ * @see {@link ControlBuilder} for field definition
908
+ */