@elizaos/plugin-form 2.0.0-alpha.2 → 2.0.0-alpha.4

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 ADDED
@@ -0,0 +1,3293 @@
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
+ registerType(type, handler) {
1246
+ registerTypeHandler(type, handler);
1247
+ logger.debug(`[FormService] Registered type handler: ${type}`);
1248
+ }
1249
+ getTypeHandler(type) {
1250
+ return getTypeHandler(type);
1251
+ }
1252
+ registerControlType(type, options) {
1253
+ const existing = this.controlTypes.get(type.id);
1254
+ if (existing) {
1255
+ if (existing.builtin && !options?.allowOverride) {
1256
+ logger.warn(`[FormService] Cannot override builtin type '${type.id}' without allowOverride: true`);
1257
+ return;
1258
+ }
1259
+ logger.warn(`[FormService] Overriding control type: ${type.id}`);
1260
+ }
1261
+ this.controlTypes.set(type.id, type);
1262
+ logger.debug(`[FormService] Registered control type: ${type.id}`);
1263
+ }
1264
+ getControlType(typeId) {
1265
+ return this.controlTypes.get(typeId);
1266
+ }
1267
+ listControlTypes() {
1268
+ return Array.from(this.controlTypes.values());
1269
+ }
1270
+ isCompositeType(typeId) {
1271
+ const type = this.controlTypes.get(typeId);
1272
+ return !!type?.getSubControls;
1273
+ }
1274
+ isExternalType(typeId) {
1275
+ const type = this.controlTypes.get(typeId);
1276
+ return !!type?.activate;
1277
+ }
1278
+ getSubControls(control) {
1279
+ const type = this.controlTypes.get(control.type);
1280
+ if (!type?.getSubControls) {
1281
+ return [];
1282
+ }
1283
+ return type.getSubControls(control, this.runtime);
1284
+ }
1285
+ async startSession(formId, entityId, roomId, options) {
1286
+ const form = this.getForm(formId);
1287
+ if (!form) {
1288
+ throw new Error(`Form not found: ${formId}`);
1289
+ }
1290
+ const existing = await getActiveSession(this.runtime, entityId, roomId);
1291
+ if (existing) {
1292
+ throw new Error(`Active session already exists for this user/room: ${existing.id}`);
1293
+ }
1294
+ const now = Date.now();
1295
+ const fields = {};
1296
+ for (const control of form.controls) {
1297
+ if (options?.initialValues?.[control.key] !== undefined) {
1298
+ fields[control.key] = {
1299
+ status: "filled",
1300
+ value: options.initialValues[control.key],
1301
+ source: "manual",
1302
+ updatedAt: now
1303
+ };
1304
+ } else if (control.defaultValue !== undefined) {
1305
+ fields[control.key] = {
1306
+ status: "filled",
1307
+ value: control.defaultValue,
1308
+ source: "default",
1309
+ updatedAt: now
1310
+ };
1311
+ } else {
1312
+ fields[control.key] = { status: "empty" };
1313
+ }
1314
+ }
1315
+ const ttlDays = form.ttl?.minDays ?? 14;
1316
+ const expiresAt = now + ttlDays * 24 * 60 * 60 * 1000;
1317
+ const session = {
1318
+ id: v4_default(),
1319
+ formId,
1320
+ formVersion: form.version,
1321
+ entityId,
1322
+ roomId,
1323
+ status: "active",
1324
+ fields,
1325
+ history: [],
1326
+ context: options?.context,
1327
+ locale: options?.locale,
1328
+ effort: {
1329
+ interactionCount: 0,
1330
+ timeSpentMs: 0,
1331
+ firstInteractionAt: now,
1332
+ lastInteractionAt: now
1333
+ },
1334
+ expiresAt,
1335
+ createdAt: now,
1336
+ updatedAt: now
1337
+ };
1338
+ await saveSession(this.runtime, session);
1339
+ if (form.hooks?.onStart) {
1340
+ await this.executeHook(session, "onStart");
1341
+ }
1342
+ logger.debug(`[FormService] Started session ${session.id} for form ${formId}`);
1343
+ return session;
1344
+ }
1345
+ async getActiveSession(entityId, roomId) {
1346
+ return getActiveSession(this.runtime, entityId, roomId);
1347
+ }
1348
+ async getAllActiveSessions(entityId) {
1349
+ return getAllActiveSessions(this.runtime, entityId);
1350
+ }
1351
+ async getStashedSessions(entityId) {
1352
+ return getStashedSessions(this.runtime, entityId);
1353
+ }
1354
+ async saveSession(session) {
1355
+ session.updatedAt = Date.now();
1356
+ await saveSession(this.runtime, session);
1357
+ }
1358
+ async updateField(sessionId, entityId, field, value, confidence, source, messageId) {
1359
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1360
+ if (!session) {
1361
+ throw new Error(`Session not found: ${sessionId}`);
1362
+ }
1363
+ const form = this.getForm(session.formId);
1364
+ if (!form) {
1365
+ throw new Error(`Form not found: ${session.formId}`);
1366
+ }
1367
+ const control = form.controls.find((c) => c.key === field);
1368
+ if (!control) {
1369
+ throw new Error(`Field not found: ${field}`);
1370
+ }
1371
+ const oldValue = session.fields[field]?.value;
1372
+ const validation = validateField(value, control);
1373
+ let status;
1374
+ if (!validation.valid) {
1375
+ status = "invalid";
1376
+ } else if (confidence < (control.confirmThreshold ?? 0.8)) {
1377
+ status = "uncertain";
1378
+ } else {
1379
+ status = "filled";
1380
+ }
1381
+ const now = Date.now();
1382
+ if (oldValue !== undefined) {
1383
+ const historyEntry = {
1384
+ field,
1385
+ oldValue,
1386
+ newValue: value,
1387
+ timestamp: now
1388
+ };
1389
+ session.history.push(historyEntry);
1390
+ const maxUndo = form.ux?.maxUndoSteps ?? 5;
1391
+ if (session.history.length > maxUndo) {
1392
+ session.history = session.history.slice(-maxUndo);
1393
+ }
1394
+ }
1395
+ session.fields[field] = {
1396
+ status,
1397
+ value,
1398
+ confidence,
1399
+ source,
1400
+ messageId,
1401
+ updatedAt: now,
1402
+ error: !validation.valid ? validation.error : undefined
1403
+ };
1404
+ session.effort.interactionCount++;
1405
+ session.effort.lastInteractionAt = now;
1406
+ session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
1407
+ session.expiresAt = this.calculateTTL(session);
1408
+ const allRequiredFilled = this.checkAllRequiredFilled(session, form);
1409
+ if (allRequiredFilled && session.status === "active") {
1410
+ session.status = "ready";
1411
+ if (form.hooks?.onReady) {
1412
+ await this.executeHook(session, "onReady");
1413
+ }
1414
+ }
1415
+ session.updatedAt = now;
1416
+ await saveSession(this.runtime, session);
1417
+ if (form.hooks?.onFieldChange) {
1418
+ const hookPayload = { field, value };
1419
+ if (oldValue !== undefined) {
1420
+ hookPayload.oldValue = oldValue;
1421
+ }
1422
+ await this.executeHook(session, "onFieldChange", hookPayload);
1423
+ }
1424
+ }
1425
+ async undoLastChange(sessionId, entityId) {
1426
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1427
+ if (!session) {
1428
+ throw new Error(`Session not found: ${sessionId}`);
1429
+ }
1430
+ const form = this.getForm(session.formId);
1431
+ if (!form?.ux?.allowUndo) {
1432
+ return null;
1433
+ }
1434
+ const lastChange = session.history.pop();
1435
+ if (!lastChange) {
1436
+ return null;
1437
+ }
1438
+ if (lastChange.oldValue !== undefined) {
1439
+ session.fields[lastChange.field] = {
1440
+ status: "filled",
1441
+ value: lastChange.oldValue,
1442
+ source: "correction",
1443
+ updatedAt: Date.now()
1444
+ };
1445
+ } else {
1446
+ session.fields[lastChange.field] = { status: "empty" };
1447
+ }
1448
+ session.updatedAt = Date.now();
1449
+ await saveSession(this.runtime, session);
1450
+ return { field: lastChange.field, restoredValue: lastChange.oldValue };
1451
+ }
1452
+ async skipField(sessionId, entityId, field) {
1453
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1454
+ if (!session) {
1455
+ throw new Error(`Session not found: ${sessionId}`);
1456
+ }
1457
+ const form = this.getForm(session.formId);
1458
+ if (!form?.ux?.allowSkip) {
1459
+ return false;
1460
+ }
1461
+ const control = form.controls.find((c) => c.key === field);
1462
+ if (!control) {
1463
+ return false;
1464
+ }
1465
+ if (control.required) {
1466
+ return false;
1467
+ }
1468
+ session.fields[field] = {
1469
+ status: "skipped",
1470
+ updatedAt: Date.now()
1471
+ };
1472
+ session.updatedAt = Date.now();
1473
+ await saveSession(this.runtime, session);
1474
+ return true;
1475
+ }
1476
+ async confirmField(sessionId, entityId, field, accepted) {
1477
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1478
+ if (!session) {
1479
+ throw new Error(`Session not found: ${sessionId}`);
1480
+ }
1481
+ const fieldState = session.fields[field];
1482
+ if (!fieldState || fieldState.status !== "uncertain") {
1483
+ return;
1484
+ }
1485
+ const now = Date.now();
1486
+ if (accepted) {
1487
+ fieldState.status = "filled";
1488
+ fieldState.confirmedAt = now;
1489
+ } else {
1490
+ fieldState.status = "empty";
1491
+ fieldState.value = undefined;
1492
+ fieldState.confidence = undefined;
1493
+ }
1494
+ fieldState.updatedAt = now;
1495
+ session.updatedAt = now;
1496
+ await saveSession(this.runtime, session);
1497
+ }
1498
+ async updateSubField(sessionId, entityId, parentField, subField, value, confidence, messageId) {
1499
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1500
+ if (!session) {
1501
+ throw new Error(`Session not found: ${sessionId}`);
1502
+ }
1503
+ const form = this.getForm(session.formId);
1504
+ if (!form) {
1505
+ throw new Error(`Form not found: ${session.formId}`);
1506
+ }
1507
+ const parentControl = form.controls.find((c) => c.key === parentField);
1508
+ if (!parentControl) {
1509
+ throw new Error(`Parent field not found: ${parentField}`);
1510
+ }
1511
+ const controlType = this.getControlType(parentControl.type);
1512
+ if (!controlType?.getSubControls) {
1513
+ throw new Error(`Control type '${parentControl.type}' is not a composite type`);
1514
+ }
1515
+ const subControls = controlType.getSubControls(parentControl, this.runtime);
1516
+ const subControl = subControls.find((c) => c.key === subField);
1517
+ if (!subControl) {
1518
+ throw new Error(`Subfield not found: ${subField} in ${parentField}`);
1519
+ }
1520
+ const now = Date.now();
1521
+ if (!session.fields[parentField]) {
1522
+ session.fields[parentField] = { status: "empty" };
1523
+ }
1524
+ if (!session.fields[parentField].subFields) {
1525
+ session.fields[parentField].subFields = {};
1526
+ }
1527
+ let subFieldStatus;
1528
+ let error;
1529
+ if (controlType.validate) {
1530
+ const result = controlType.validate(value, subControl);
1531
+ if (!result.valid) {
1532
+ subFieldStatus = "invalid";
1533
+ error = result.error;
1534
+ } else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
1535
+ subFieldStatus = "uncertain";
1536
+ } else {
1537
+ subFieldStatus = "filled";
1538
+ }
1539
+ } else {
1540
+ const validation = validateField(value, subControl);
1541
+ if (!validation.valid) {
1542
+ subFieldStatus = "invalid";
1543
+ error = validation.error;
1544
+ } else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
1545
+ subFieldStatus = "uncertain";
1546
+ } else {
1547
+ subFieldStatus = "filled";
1548
+ }
1549
+ }
1550
+ session.fields[parentField].subFields[subField] = {
1551
+ status: subFieldStatus,
1552
+ value,
1553
+ confidence,
1554
+ source: "extraction",
1555
+ messageId,
1556
+ updatedAt: now,
1557
+ error
1558
+ };
1559
+ session.effort.interactionCount++;
1560
+ session.effort.lastInteractionAt = now;
1561
+ session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
1562
+ session.updatedAt = now;
1563
+ await saveSession(this.runtime, session);
1564
+ logger.debug(`[FormService] Updated subfield ${parentField}.${subField}`);
1565
+ }
1566
+ areSubFieldsFilled(session, parentField) {
1567
+ const form = this.getForm(session.formId);
1568
+ if (!form)
1569
+ return false;
1570
+ const parentControl = form.controls.find((c) => c.key === parentField);
1571
+ if (!parentControl)
1572
+ return false;
1573
+ const controlType = this.getControlType(parentControl.type);
1574
+ if (!controlType?.getSubControls)
1575
+ return false;
1576
+ const subControls = controlType.getSubControls(parentControl, this.runtime);
1577
+ const subFields = session.fields[parentField]?.subFields || {};
1578
+ for (const subControl of subControls) {
1579
+ if (!subControl.required)
1580
+ continue;
1581
+ const subField = subFields[subControl.key];
1582
+ if (!subField || subField.status !== "filled") {
1583
+ return false;
1584
+ }
1585
+ }
1586
+ return true;
1587
+ }
1588
+ getSubFieldValues(session, parentField) {
1589
+ const subFields = session.fields[parentField]?.subFields || {};
1590
+ const values = {};
1591
+ for (const [key, state] of Object.entries(subFields)) {
1592
+ if (state.value !== undefined) {
1593
+ values[key] = state.value;
1594
+ }
1595
+ }
1596
+ return values;
1597
+ }
1598
+ async activateExternalField(sessionId, entityId, field) {
1599
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1600
+ if (!session) {
1601
+ throw new Error(`Session not found: ${sessionId}`);
1602
+ }
1603
+ const form = this.getForm(session.formId);
1604
+ if (!form) {
1605
+ throw new Error(`Form not found: ${session.formId}`);
1606
+ }
1607
+ const control = form.controls.find((c) => c.key === field);
1608
+ if (!control) {
1609
+ throw new Error(`Field not found: ${field}`);
1610
+ }
1611
+ const controlType = this.getControlType(control.type);
1612
+ if (!controlType?.activate) {
1613
+ throw new Error(`Control type '${control.type}' does not support activation`);
1614
+ }
1615
+ const subValues = this.getSubFieldValues(session, field);
1616
+ const context = {
1617
+ runtime: this.runtime,
1618
+ session,
1619
+ control,
1620
+ subValues
1621
+ };
1622
+ const activation = await controlType.activate(context);
1623
+ const now = Date.now();
1624
+ if (!session.fields[field]) {
1625
+ session.fields[field] = { status: "empty" };
1626
+ }
1627
+ session.fields[field].status = "pending";
1628
+ session.fields[field].externalState = {
1629
+ status: "pending",
1630
+ reference: activation.reference,
1631
+ instructions: activation.instructions,
1632
+ address: activation.address,
1633
+ activatedAt: now
1634
+ };
1635
+ session.updatedAt = now;
1636
+ await saveSession(this.runtime, session);
1637
+ logger.info(`[FormService] Activated external field ${field} with reference ${activation.reference}`);
1638
+ return activation;
1639
+ }
1640
+ async confirmExternalField(sessionId, entityId, field, value, externalData) {
1641
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1642
+ if (!session) {
1643
+ throw new Error(`Session not found: ${sessionId}`);
1644
+ }
1645
+ const fieldState = session.fields[field];
1646
+ if (!fieldState || fieldState.status !== "pending") {
1647
+ logger.warn(`[FormService] Cannot confirm field ${field}: not in pending state`);
1648
+ return;
1649
+ }
1650
+ const now = Date.now();
1651
+ fieldState.status = "filled";
1652
+ fieldState.value = value;
1653
+ fieldState.source = "external";
1654
+ fieldState.updatedAt = now;
1655
+ if (fieldState.externalState) {
1656
+ fieldState.externalState.status = "confirmed";
1657
+ fieldState.externalState.confirmedAt = now;
1658
+ fieldState.externalState.externalData = externalData;
1659
+ }
1660
+ const form = this.getForm(session.formId);
1661
+ if (form && this.checkAllRequiredFilled(session, form)) {
1662
+ if (session.status === "active") {
1663
+ session.status = "ready";
1664
+ if (form.hooks?.onReady) {
1665
+ await this.executeHook(session, "onReady");
1666
+ }
1667
+ }
1668
+ }
1669
+ session.updatedAt = now;
1670
+ await saveSession(this.runtime, session);
1671
+ try {
1672
+ await this.runtime.emitEvent("FORM_FIELD_CONFIRMED", {
1673
+ runtime: this.runtime,
1674
+ sessionId,
1675
+ entityId,
1676
+ field,
1677
+ value,
1678
+ externalData
1679
+ });
1680
+ } catch (_error) {
1681
+ logger.debug(`[FormService] No event handler for FORM_FIELD_CONFIRMED`);
1682
+ }
1683
+ logger.info(`[FormService] Confirmed external field ${field}`);
1684
+ }
1685
+ async cancelExternalField(sessionId, entityId, field, reason) {
1686
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1687
+ if (!session) {
1688
+ throw new Error(`Session not found: ${sessionId}`);
1689
+ }
1690
+ const form = this.getForm(session.formId);
1691
+ const control = form?.controls.find((c) => c.key === field);
1692
+ const controlType = control ? this.getControlType(control.type) : undefined;
1693
+ if (controlType?.deactivate && control) {
1694
+ try {
1695
+ await controlType.deactivate({
1696
+ runtime: this.runtime,
1697
+ session,
1698
+ control,
1699
+ subValues: this.getSubFieldValues(session, field)
1700
+ });
1701
+ } catch (error) {
1702
+ logger.error(`[FormService] Deactivate failed for ${field}: ${String(error)}`);
1703
+ }
1704
+ }
1705
+ const fieldState = session.fields[field];
1706
+ if (fieldState) {
1707
+ fieldState.status = "empty";
1708
+ fieldState.error = reason;
1709
+ if (fieldState.externalState) {
1710
+ fieldState.externalState.status = "failed";
1711
+ }
1712
+ }
1713
+ session.updatedAt = Date.now();
1714
+ await saveSession(this.runtime, session);
1715
+ try {
1716
+ await this.runtime.emitEvent("FORM_FIELD_CANCELLED", {
1717
+ runtime: this.runtime,
1718
+ sessionId,
1719
+ entityId,
1720
+ field,
1721
+ reason
1722
+ });
1723
+ } catch (_error) {
1724
+ logger.debug(`[FormService] No event handler for FORM_FIELD_CANCELLED`);
1725
+ }
1726
+ logger.info(`[FormService] Cancelled external field ${field}: ${reason}`);
1727
+ }
1728
+ async submit(sessionId, entityId) {
1729
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1730
+ if (!session) {
1731
+ throw new Error(`Session not found: ${sessionId}`);
1732
+ }
1733
+ const form = this.getForm(session.formId);
1734
+ if (!form) {
1735
+ throw new Error(`Form not found: ${session.formId}`);
1736
+ }
1737
+ if (!this.checkAllRequiredFilled(session, form)) {
1738
+ throw new Error("Not all required fields are filled");
1739
+ }
1740
+ const now = Date.now();
1741
+ const values = {};
1742
+ const mappedValues = {};
1743
+ const files = {};
1744
+ for (const control of form.controls) {
1745
+ const fieldState = session.fields[control.key];
1746
+ if (fieldState?.value !== undefined) {
1747
+ values[control.key] = fieldState.value;
1748
+ const dbKey = control.dbbind || control.key;
1749
+ mappedValues[dbKey] = fieldState.value;
1750
+ }
1751
+ if (fieldState?.files) {
1752
+ files[control.key] = fieldState.files;
1753
+ }
1754
+ }
1755
+ const submission = {
1756
+ id: v4_default(),
1757
+ formId: session.formId,
1758
+ formVersion: session.formVersion,
1759
+ sessionId: session.id,
1760
+ entityId: session.entityId,
1761
+ values,
1762
+ mappedValues,
1763
+ files: Object.keys(files).length > 0 ? files : undefined,
1764
+ submittedAt: now,
1765
+ meta: session.meta
1766
+ };
1767
+ await saveSubmission(this.runtime, submission);
1768
+ await saveAutofillData(this.runtime, entityId, session.formId, values);
1769
+ session.status = "submitted";
1770
+ session.submittedAt = now;
1771
+ session.updatedAt = now;
1772
+ await saveSession(this.runtime, session);
1773
+ if (form.hooks?.onSubmit) {
1774
+ const submissionPayload = JSON.parse(JSON.stringify(submission));
1775
+ await this.executeHook(session, "onSubmit", {
1776
+ submission: submissionPayload
1777
+ });
1778
+ }
1779
+ logger.debug(`[FormService] Submitted session ${sessionId}`);
1780
+ return submission;
1781
+ }
1782
+ async stash(sessionId, entityId) {
1783
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1784
+ if (!session) {
1785
+ throw new Error(`Session not found: ${sessionId}`);
1786
+ }
1787
+ const form = this.getForm(session.formId);
1788
+ session.status = "stashed";
1789
+ session.updatedAt = Date.now();
1790
+ await saveSession(this.runtime, session);
1791
+ if (form?.hooks?.onCancel) {}
1792
+ logger.debug(`[FormService] Stashed session ${sessionId}`);
1793
+ }
1794
+ async restore(sessionId, entityId) {
1795
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1796
+ if (!session) {
1797
+ throw new Error(`Session not found: ${sessionId}`);
1798
+ }
1799
+ if (session.status !== "stashed") {
1800
+ throw new Error(`Session is not stashed: ${session.status}`);
1801
+ }
1802
+ const existing = await getActiveSession(this.runtime, entityId, session.roomId);
1803
+ if (existing && existing.id !== sessionId) {
1804
+ throw new Error(`Active session already exists in room: ${existing.id}`);
1805
+ }
1806
+ session.status = "active";
1807
+ session.updatedAt = Date.now();
1808
+ session.expiresAt = this.calculateTTL(session);
1809
+ await saveSession(this.runtime, session);
1810
+ logger.debug(`[FormService] Restored session ${sessionId}`);
1811
+ return session;
1812
+ }
1813
+ async cancel(sessionId, entityId, force = false) {
1814
+ const session = await getSessionById(this.runtime, entityId, sessionId);
1815
+ if (!session) {
1816
+ throw new Error(`Session not found: ${sessionId}`);
1817
+ }
1818
+ if (!force && this.shouldConfirmCancel(session) && !session.cancelConfirmationAsked) {
1819
+ session.cancelConfirmationAsked = true;
1820
+ session.updatedAt = Date.now();
1821
+ await saveSession(this.runtime, session);
1822
+ return false;
1823
+ }
1824
+ const form = this.getForm(session.formId);
1825
+ session.status = "cancelled";
1826
+ session.updatedAt = Date.now();
1827
+ await saveSession(this.runtime, session);
1828
+ if (form?.hooks?.onCancel) {
1829
+ await this.executeHook(session, "onCancel");
1830
+ }
1831
+ logger.debug(`[FormService] Cancelled session ${sessionId}`);
1832
+ return true;
1833
+ }
1834
+ async getSubmissions(entityId, formId) {
1835
+ return getSubmissions(this.runtime, entityId, formId);
1836
+ }
1837
+ async getAutofill(entityId, formId) {
1838
+ const data = await getAutofillData(this.runtime, entityId, formId);
1839
+ return data?.values || null;
1840
+ }
1841
+ async applyAutofill(session) {
1842
+ const form = this.getForm(session.formId);
1843
+ if (!form?.ux?.allowAutofill) {
1844
+ return [];
1845
+ }
1846
+ const autofill = await getAutofillData(this.runtime, session.entityId, session.formId);
1847
+ if (!autofill) {
1848
+ return [];
1849
+ }
1850
+ const appliedFields = [];
1851
+ const now = Date.now();
1852
+ for (const control of form.controls) {
1853
+ if (session.fields[control.key]?.status !== "empty") {
1854
+ continue;
1855
+ }
1856
+ const value = autofill.values[control.key];
1857
+ if (value !== undefined) {
1858
+ session.fields[control.key] = {
1859
+ status: "filled",
1860
+ value,
1861
+ source: "autofill",
1862
+ updatedAt: now
1863
+ };
1864
+ appliedFields.push(control.key);
1865
+ }
1866
+ }
1867
+ if (appliedFields.length > 0) {
1868
+ session.updatedAt = now;
1869
+ await saveSession(this.runtime, session);
1870
+ }
1871
+ return appliedFields;
1872
+ }
1873
+ getSessionContext(session) {
1874
+ const form = this.getForm(session.formId);
1875
+ if (!form) {
1876
+ return {
1877
+ hasActiveForm: false,
1878
+ progress: 0,
1879
+ filledFields: [],
1880
+ missingRequired: [],
1881
+ uncertainFields: [],
1882
+ nextField: null,
1883
+ pendingExternalFields: []
1884
+ };
1885
+ }
1886
+ const filledFields = [];
1887
+ const missingRequired = [];
1888
+ const uncertainFields = [];
1889
+ const pendingExternalFields = [];
1890
+ let nextField = null;
1891
+ let filledCount = 0;
1892
+ let totalRequired = 0;
1893
+ for (const control of form.controls) {
1894
+ if (control.hidden)
1895
+ continue;
1896
+ const fieldState = session.fields[control.key];
1897
+ if (control.required) {
1898
+ totalRequired++;
1899
+ }
1900
+ if (fieldState?.status === "filled") {
1901
+ filledCount++;
1902
+ filledFields.push({
1903
+ key: control.key,
1904
+ label: control.label,
1905
+ displayValue: formatValue(fieldState.value ?? null, control)
1906
+ });
1907
+ } else if (fieldState?.status === "pending") {
1908
+ if (fieldState.externalState) {
1909
+ pendingExternalFields.push({
1910
+ key: control.key,
1911
+ label: control.label,
1912
+ instructions: fieldState.externalState.instructions || "Waiting for confirmation...",
1913
+ reference: fieldState.externalState.reference || "",
1914
+ activatedAt: fieldState.externalState.activatedAt || Date.now(),
1915
+ address: fieldState.externalState.address
1916
+ });
1917
+ }
1918
+ } else if (fieldState?.status === "uncertain") {
1919
+ uncertainFields.push({
1920
+ key: control.key,
1921
+ label: control.label,
1922
+ value: fieldState.value ?? null,
1923
+ confidence: fieldState.confidence ?? 0
1924
+ });
1925
+ } else if (fieldState?.status === "invalid") {
1926
+ missingRequired.push({
1927
+ key: control.key,
1928
+ label: control.label,
1929
+ description: control.description,
1930
+ askPrompt: control.askPrompt
1931
+ });
1932
+ if (!nextField)
1933
+ nextField = control;
1934
+ } else if (control.required && fieldState?.status !== "skipped") {
1935
+ missingRequired.push({
1936
+ key: control.key,
1937
+ label: control.label,
1938
+ description: control.description,
1939
+ askPrompt: control.askPrompt
1940
+ });
1941
+ if (!nextField)
1942
+ nextField = control;
1943
+ } else if (!nextField && fieldState?.status === "empty") {
1944
+ nextField = control;
1945
+ }
1946
+ }
1947
+ const progress = totalRequired > 0 ? Math.round(filledCount / totalRequired * 100) : 100;
1948
+ return {
1949
+ hasActiveForm: true,
1950
+ formId: session.formId,
1951
+ formName: form.name,
1952
+ progress,
1953
+ filledFields,
1954
+ missingRequired,
1955
+ uncertainFields,
1956
+ nextField,
1957
+ status: session.status,
1958
+ pendingCancelConfirmation: session.cancelConfirmationAsked && session.status === "active",
1959
+ pendingExternalFields
1960
+ };
1961
+ }
1962
+ getValues(session) {
1963
+ const values = {};
1964
+ for (const [key, state] of Object.entries(session.fields)) {
1965
+ if (state.value !== undefined) {
1966
+ values[key] = state.value;
1967
+ }
1968
+ }
1969
+ return values;
1970
+ }
1971
+ getMappedValues(session) {
1972
+ const form = this.getForm(session.formId);
1973
+ if (!form)
1974
+ return {};
1975
+ const values = {};
1976
+ for (const control of form.controls) {
1977
+ const state = session.fields[control.key];
1978
+ if (state?.value !== undefined) {
1979
+ const key = control.dbbind || control.key;
1980
+ values[key] = state.value;
1981
+ }
1982
+ }
1983
+ return values;
1984
+ }
1985
+ calculateTTL(session) {
1986
+ const form = this.getForm(session.formId);
1987
+ const config = form?.ttl || {};
1988
+ const minDays = config.minDays ?? 14;
1989
+ const maxDays = config.maxDays ?? 90;
1990
+ const multiplier = config.effortMultiplier ?? 0.5;
1991
+ const minutesSpent = session.effort.timeSpentMs / 60000;
1992
+ const effortDays = minutesSpent * multiplier;
1993
+ const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
1994
+ return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
1995
+ }
1996
+ shouldConfirmCancel(session) {
1997
+ const minEffortMs = 5 * 60 * 1000;
1998
+ return session.effort.timeSpentMs > minEffortMs;
1999
+ }
2000
+ async executeHook(session, hookName, options) {
2001
+ const form = this.getForm(session.formId);
2002
+ const workerName = form?.hooks?.[hookName];
2003
+ if (!workerName)
2004
+ return;
2005
+ const worker = this.runtime.getTaskWorker(workerName);
2006
+ if (!worker) {
2007
+ logger.warn(`[FormService] Hook worker not found: ${workerName}`);
2008
+ return;
2009
+ }
2010
+ try {
2011
+ const task = {
2012
+ id: session.id,
2013
+ name: workerName,
2014
+ roomId: session.roomId,
2015
+ entityId: session.entityId
2016
+ };
2017
+ await worker.execute(this.runtime, {
2018
+ session,
2019
+ form,
2020
+ ...options
2021
+ }, task);
2022
+ } catch (error) {
2023
+ logger.error(`[FormService] Hook execution failed: ${hookName}`, String(error));
2024
+ }
2025
+ }
2026
+ checkAllRequiredFilled(session, form) {
2027
+ for (const control of form.controls) {
2028
+ if (!control.required)
2029
+ continue;
2030
+ const fieldState = session.fields[control.key];
2031
+ if (!fieldState || fieldState.status === "empty" || fieldState.status === "invalid") {
2032
+ return false;
2033
+ }
2034
+ }
2035
+ return true;
2036
+ }
2037
+ };
2038
+ });
2039
+
2040
+ // src/actions/restore.ts
2041
+ var exports_restore = {};
2042
+ __export(exports_restore, {
2043
+ formRestoreAction: () => formRestoreAction,
2044
+ default: () => restore_default
2045
+ });
2046
+ import {
2047
+ logger as logger2
2048
+ } from "@elizaos/core";
2049
+ var formRestoreAction, restore_default;
2050
+ var init_restore = __esm(() => {
2051
+ formRestoreAction = {
2052
+ name: "FORM_RESTORE",
2053
+ similes: ["RESUME_FORM", "CONTINUE_FORM"],
2054
+ description: "Restore a previously stashed form session",
2055
+ validate: async (runtime, message, _state) => {
2056
+ try {
2057
+ const text = message.content?.text || "";
2058
+ const intent = quickIntentDetect(text);
2059
+ if (intent !== "restore") {
2060
+ return false;
2061
+ }
2062
+ const formService = runtime.getService("FORM");
2063
+ if (!formService) {
2064
+ return false;
2065
+ }
2066
+ const entityId = message.entityId;
2067
+ if (!entityId)
2068
+ return false;
2069
+ const stashed = await formService.getStashedSessions(entityId);
2070
+ return stashed.length > 0;
2071
+ } catch (error) {
2072
+ logger2.error("[FormRestoreAction] Validation error:", String(error));
2073
+ return false;
2074
+ }
2075
+ },
2076
+ handler: async (runtime, message, _state, _options, callback) => {
2077
+ try {
2078
+ const formService = runtime.getService("FORM");
2079
+ if (!formService) {
2080
+ await callback?.({
2081
+ text: "Sorry, I couldn't find the form service."
2082
+ });
2083
+ return { success: false };
2084
+ }
2085
+ const entityId = message.entityId;
2086
+ const roomId = message.roomId;
2087
+ if (!entityId || !roomId) {
2088
+ await callback?.({
2089
+ text: "Sorry, I couldn't identify you."
2090
+ });
2091
+ return { success: false };
2092
+ }
2093
+ const existing = await formService.getActiveSession(entityId, roomId);
2094
+ if (existing) {
2095
+ const form2 = formService.getForm(existing.formId);
2096
+ await callback?.({
2097
+ 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?`
2098
+ });
2099
+ return { success: false };
2100
+ }
2101
+ const stashed = await formService.getStashedSessions(entityId);
2102
+ if (stashed.length === 0) {
2103
+ await callback?.({
2104
+ text: "You don't have any saved forms to resume."
2105
+ });
2106
+ return { success: false };
2107
+ }
2108
+ const sessionToRestore = stashed.sort((a, b) => b.updatedAt - a.updatedAt)[0];
2109
+ const session = await formService.restore(sessionToRestore.id, entityId);
2110
+ const form = formService.getForm(session.formId);
2111
+ const context = formService.getSessionContext(session);
2112
+ let responseText = `I've restored your "${form?.name || session.formId}" form. `;
2113
+ responseText += `You're ${context.progress}% complete. `;
2114
+ if (context.filledFields.length > 0) {
2115
+ responseText += `
2116
+
2117
+ Here's what I have so far:
2118
+ `;
2119
+ for (const field of context.filledFields) {
2120
+ responseText += `• ${field.label}: ${field.displayValue}
2121
+ `;
2122
+ }
2123
+ }
2124
+ if (context.nextField) {
2125
+ responseText += `
2126
+ Let's continue with ${context.nextField.label}.`;
2127
+ if (context.nextField.askPrompt) {
2128
+ responseText += ` ${context.nextField.askPrompt}`;
2129
+ }
2130
+ } else if (context.status === "ready") {
2131
+ responseText += `
2132
+ Everything looks complete! Ready to submit?`;
2133
+ }
2134
+ await callback?.({
2135
+ text: responseText
2136
+ });
2137
+ return {
2138
+ success: true,
2139
+ data: {
2140
+ sessionId: session.id,
2141
+ formId: session.formId,
2142
+ progress: context.progress
2143
+ }
2144
+ };
2145
+ } catch (error) {
2146
+ logger2.error("[FormRestoreAction] Handler error:", String(error));
2147
+ await callback?.({
2148
+ text: "Sorry, I couldn't restore your form. Please try again."
2149
+ });
2150
+ return { success: false };
2151
+ }
2152
+ },
2153
+ examples: [
2154
+ [
2155
+ {
2156
+ name: "{{user1}}",
2157
+ content: { text: "Resume my form" }
2158
+ },
2159
+ {
2160
+ name: "{{agentName}}",
2161
+ content: {
2162
+ text: "I've restored your form. Let's continue where you left off."
2163
+ }
2164
+ }
2165
+ ],
2166
+ [
2167
+ {
2168
+ name: "{{user1}}",
2169
+ content: { text: "Continue with my registration" }
2170
+ },
2171
+ {
2172
+ name: "{{agentName}}",
2173
+ content: {
2174
+ text: "I've restored your Registration form. You're 60% complete."
2175
+ }
2176
+ }
2177
+ ],
2178
+ [
2179
+ {
2180
+ name: "{{user1}}",
2181
+ content: { text: "Pick up where I left off" }
2182
+ },
2183
+ {
2184
+ name: "{{agentName}}",
2185
+ content: {
2186
+ text: "I've restored your form. Here's what you have so far..."
2187
+ }
2188
+ }
2189
+ ]
2190
+ ]
2191
+ };
2192
+ restore_default = formRestoreAction;
2193
+ });
2194
+
2195
+ // src/evaluators/extractor.ts
2196
+ var exports_extractor = {};
2197
+ __export(exports_extractor, {
2198
+ formEvaluator: () => formEvaluator,
2199
+ default: () => extractor_default
2200
+ });
2201
+ import { logger as logger3 } from "@elizaos/core";
2202
+ async function processExtractions(runtime, formService, session, form, entityId, extractions, messageId) {
2203
+ const updatedParents = new Set;
2204
+ for (const extraction of extractions) {
2205
+ if (extraction.field.includes(".")) {
2206
+ const [parentKey, subKey] = extraction.field.split(".");
2207
+ await formService.updateSubField(session.id, entityId, parentKey, subKey, extraction.value, extraction.confidence, messageId);
2208
+ await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
2209
+ sessionId: session.id,
2210
+ parentField: parentKey,
2211
+ subField: subKey,
2212
+ value: extraction.value,
2213
+ confidence: extraction.confidence
2214
+ });
2215
+ updatedParents.add(parentKey);
2216
+ if (form.debug) {
2217
+ logger3.debug(`[FormEvaluator] Updated subfield ${parentKey}.${subKey}`);
2218
+ }
2219
+ } else {
2220
+ await formService.updateField(session.id, entityId, extraction.field, extraction.value, extraction.confidence, extraction.isCorrection ? "correction" : "extraction", messageId);
2221
+ await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
2222
+ sessionId: session.id,
2223
+ field: extraction.field,
2224
+ value: extraction.value,
2225
+ confidence: extraction.confidence
2226
+ });
2227
+ if (form.debug) {
2228
+ logger3.debug(`[FormEvaluator] Updated field ${extraction.field}`);
2229
+ }
2230
+ }
2231
+ }
2232
+ for (const parentKey of updatedParents) {
2233
+ await checkAndActivateExternalField(runtime, formService, session, form, entityId, parentKey);
2234
+ }
2235
+ }
2236
+ async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
2237
+ const freshSession = await formService.getActiveSession(entityId, session.roomId);
2238
+ if (!freshSession)
2239
+ return;
2240
+ if (!formService.isExternalType(form.controls.find((c) => c.key === field)?.type || "")) {
2241
+ return;
2242
+ }
2243
+ if (!formService.areSubFieldsFilled(freshSession, field)) {
2244
+ return;
2245
+ }
2246
+ const subValues = formService.getSubFieldValues(freshSession, field);
2247
+ await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
2248
+ sessionId: session.id,
2249
+ field,
2250
+ subValues
2251
+ });
2252
+ logger3.debug(`[FormEvaluator] All subcontrols filled for ${field}, activating...`);
2253
+ try {
2254
+ const activation = await formService.activateExternalField(session.id, entityId, field);
2255
+ const activationPayload = JSON.parse(JSON.stringify(activation));
2256
+ await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
2257
+ sessionId: session.id,
2258
+ field,
2259
+ activation: activationPayload
2260
+ });
2261
+ logger3.info(`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`);
2262
+ } catch (error) {
2263
+ logger3.error(`[FormEvaluator] Failed to activate external field ${field}:`, String(error));
2264
+ }
2265
+ }
2266
+ async function emitEvent(runtime, eventType, payload) {
2267
+ try {
2268
+ if (typeof runtime.emitEvent === "function") {
2269
+ const eventPayload = { runtime, ...payload };
2270
+ await runtime.emitEvent(eventType, eventPayload);
2271
+ }
2272
+ } catch (error) {
2273
+ logger3.debug(`[FormEvaluator] Event emission (${eventType}):`, String(error));
2274
+ }
2275
+ }
2276
+ async function handleSubmit(formService, session, entityId) {
2277
+ try {
2278
+ await formService.submit(session.id, entityId);
2279
+ } catch (error) {
2280
+ logger3.debug("[FormEvaluator] Submit failed:", String(error));
2281
+ }
2282
+ }
2283
+ async function handleUndo(formService, session, entityId, form) {
2284
+ if (!form.ux?.allowUndo) {
2285
+ return;
2286
+ }
2287
+ const result = await formService.undoLastChange(session.id, entityId);
2288
+ if (result) {
2289
+ logger3.debug("[FormEvaluator] Undid field:", result.field);
2290
+ }
2291
+ }
2292
+ async function handleSkip(formService, session, entityId, form) {
2293
+ if (!form.ux?.allowSkip) {
2294
+ return;
2295
+ }
2296
+ if (session.lastAskedField) {
2297
+ const skipped = await formService.skipField(session.id, entityId, session.lastAskedField);
2298
+ if (skipped) {
2299
+ logger3.debug("[FormEvaluator] Skipped field:", session.lastAskedField);
2300
+ }
2301
+ }
2302
+ }
2303
+ var formEvaluator, extractor_default;
2304
+ var init_extractor = __esm(() => {
2305
+ init_extraction();
2306
+ init_template();
2307
+ formEvaluator = {
2308
+ name: "form_evaluator",
2309
+ description: "Extracts form fields and handles form intents from user messages",
2310
+ similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
2311
+ examples: [],
2312
+ validate: async (runtime, message, _state) => {
2313
+ try {
2314
+ const formService = runtime.getService("FORM");
2315
+ if (!formService)
2316
+ return false;
2317
+ const entityId = message.entityId;
2318
+ const roomId = message.roomId;
2319
+ if (!entityId || !roomId)
2320
+ return false;
2321
+ const session = await formService.getActiveSession(entityId, roomId);
2322
+ const stashed = await formService.getStashedSessions(entityId);
2323
+ return session !== null || stashed.length > 0;
2324
+ } catch (error) {
2325
+ logger3.error("[FormEvaluator] Validation error:", String(error));
2326
+ return false;
2327
+ }
2328
+ },
2329
+ handler: async (runtime, message, _state) => {
2330
+ try {
2331
+ const formService = runtime.getService("FORM");
2332
+ if (!formService)
2333
+ return;
2334
+ const entityId = message.entityId;
2335
+ const roomId = message.roomId;
2336
+ const text = message.content?.text || "";
2337
+ if (!entityId || !roomId)
2338
+ return;
2339
+ if (!text.trim())
2340
+ return;
2341
+ let session = await formService.getActiveSession(entityId, roomId);
2342
+ let intent = quickIntentDetect(text);
2343
+ let extractions = [];
2344
+ if (intent === "restore" && !session) {
2345
+ logger3.debug("[FormEvaluator] Restore intent detected, deferring to action");
2346
+ return;
2347
+ }
2348
+ if (!session) {
2349
+ return;
2350
+ }
2351
+ const form = formService.getForm(session.formId);
2352
+ if (!form) {
2353
+ logger3.warn("[FormEvaluator] Form not found for session:", session.formId);
2354
+ return;
2355
+ }
2356
+ const templateValues = buildTemplateValues(session);
2357
+ if (!intent) {
2358
+ const result = await llmIntentAndExtract(runtime, text, form, form.controls, templateValues);
2359
+ intent = result.intent;
2360
+ extractions = result.extractions;
2361
+ if (form.debug) {
2362
+ logger3.debug("[FormEvaluator] LLM extraction result:", JSON.stringify({ intent, extractions }));
2363
+ }
2364
+ }
2365
+ switch (intent) {
2366
+ case "submit":
2367
+ await handleSubmit(formService, session, entityId);
2368
+ break;
2369
+ case "stash":
2370
+ await formService.stash(session.id, entityId);
2371
+ break;
2372
+ case "cancel":
2373
+ await formService.cancel(session.id, entityId);
2374
+ break;
2375
+ case "undo":
2376
+ await handleUndo(formService, session, entityId, form);
2377
+ break;
2378
+ case "skip":
2379
+ await handleSkip(formService, session, entityId, form);
2380
+ break;
2381
+ case "autofill":
2382
+ await formService.applyAutofill(session);
2383
+ break;
2384
+ case "explain":
2385
+ case "example":
2386
+ case "progress":
2387
+ logger3.debug(`[FormEvaluator] Info intent: ${intent}`);
2388
+ break;
2389
+ case "restore":
2390
+ logger3.debug("[FormEvaluator] Restore intent - deferring to action");
2391
+ break;
2392
+ default:
2393
+ await processExtractions(runtime, formService, session, form, entityId, extractions, message.id);
2394
+ break;
2395
+ }
2396
+ session = await formService.getActiveSession(entityId, roomId);
2397
+ if (session) {
2398
+ session.lastMessageId = message.id;
2399
+ await formService.saveSession(session);
2400
+ }
2401
+ } catch (error) {
2402
+ logger3.error("[FormEvaluator] Handler error:", String(error));
2403
+ return;
2404
+ }
2405
+ return;
2406
+ }
2407
+ };
2408
+ extractor_default = formEvaluator;
2409
+ });
2410
+
2411
+ // src/providers/context.ts
2412
+ var exports_context = {};
2413
+ __export(exports_context, {
2414
+ formContextProvider: () => formContextProvider,
2415
+ default: () => context_default
2416
+ });
2417
+ import { logger as logger4 } from "@elizaos/core";
2418
+ var formContextProvider, context_default;
2419
+ var init_context = __esm(() => {
2420
+ init_template();
2421
+ formContextProvider = {
2422
+ name: "FORM_CONTEXT",
2423
+ description: "Provides context about active form sessions",
2424
+ get: async (runtime, message, _state) => {
2425
+ try {
2426
+ const formService = runtime.getService("FORM");
2427
+ if (!formService) {
2428
+ return {
2429
+ data: { hasActiveForm: false },
2430
+ values: { formContext: "" },
2431
+ text: ""
2432
+ };
2433
+ }
2434
+ const entityId = message.entityId;
2435
+ const roomId = message.roomId;
2436
+ if (!entityId || !roomId) {
2437
+ return {
2438
+ data: { hasActiveForm: false },
2439
+ values: { formContext: "" },
2440
+ text: ""
2441
+ };
2442
+ }
2443
+ const session = await formService.getActiveSession(entityId, roomId);
2444
+ const stashed = await formService.getStashedSessions(entityId);
2445
+ if (!session && stashed.length === 0) {
2446
+ return {
2447
+ data: { hasActiveForm: false, stashedCount: 0 },
2448
+ values: { formContext: "" },
2449
+ text: ""
2450
+ };
2451
+ }
2452
+ let contextText = "";
2453
+ let contextState;
2454
+ if (session) {
2455
+ contextState = formService.getSessionContext(session);
2456
+ const form = formService.getForm(session.formId);
2457
+ const templateValues = buildTemplateValues(session);
2458
+ const resolveText = (value) => renderTemplate(value, templateValues);
2459
+ contextState = {
2460
+ ...contextState,
2461
+ filledFields: contextState.filledFields.map((field) => ({
2462
+ ...field,
2463
+ label: resolveText(field.label) ?? field.label
2464
+ })),
2465
+ missingRequired: contextState.missingRequired.map((field) => ({
2466
+ ...field,
2467
+ label: resolveText(field.label) ?? field.label,
2468
+ description: resolveText(field.description),
2469
+ askPrompt: resolveText(field.askPrompt)
2470
+ })),
2471
+ uncertainFields: contextState.uncertainFields.map((field) => ({
2472
+ ...field,
2473
+ label: resolveText(field.label) ?? field.label
2474
+ })),
2475
+ nextField: contextState.nextField ? resolveControlTemplates(contextState.nextField, templateValues) : null
2476
+ };
2477
+ contextText = `# Active Form: ${form?.name || session.formId}
2478
+
2479
+ `;
2480
+ contextText += `Progress: ${contextState.progress}%
2481
+
2482
+ `;
2483
+ if (contextState.filledFields.length > 0) {
2484
+ contextText += `## Collected Information
2485
+ `;
2486
+ for (const field of contextState.filledFields) {
2487
+ contextText += `- ${field.label}: ${field.displayValue}
2488
+ `;
2489
+ }
2490
+ contextText += `
2491
+ `;
2492
+ }
2493
+ if (contextState.missingRequired.length > 0) {
2494
+ contextText += `## Still Needed
2495
+ `;
2496
+ for (const field of contextState.missingRequired) {
2497
+ contextText += `- ${field.label}${field.description ? ` (${field.description})` : ""}
2498
+ `;
2499
+ }
2500
+ contextText += `
2501
+ `;
2502
+ }
2503
+ if (contextState.uncertainFields.length > 0) {
2504
+ contextText += `## Needs Confirmation
2505
+ `;
2506
+ for (const field of contextState.uncertainFields) {
2507
+ contextText += `- ${field.label}: "${field.value}" (${Math.round(field.confidence * 100)}% confident)
2508
+ `;
2509
+ }
2510
+ contextText += `
2511
+ `;
2512
+ }
2513
+ if (contextState.pendingExternalFields.length > 0) {
2514
+ contextText += `## Waiting For External Action
2515
+ `;
2516
+ for (const field of contextState.pendingExternalFields) {
2517
+ const ageMs = Date.now() - field.activatedAt;
2518
+ const ageMin = Math.floor(ageMs / 60000);
2519
+ const ageText = ageMin < 1 ? "just now" : `${ageMin}m ago`;
2520
+ contextText += `- ${field.label}: ${field.instructions} (started ${ageText})
2521
+ `;
2522
+ if (field.address) {
2523
+ contextText += ` Address: ${field.address}
2524
+ `;
2525
+ }
2526
+ }
2527
+ contextText += `
2528
+ `;
2529
+ }
2530
+ contextText += `## Agent Guidance
2531
+ `;
2532
+ if (contextState.pendingExternalFields.length > 0) {
2533
+ const pending = contextState.pendingExternalFields[0];
2534
+ contextText += `Waiting for external action. Remind user: "${pending.instructions}"
2535
+ `;
2536
+ } else if (contextState.pendingCancelConfirmation) {
2537
+ contextText += `User is trying to cancel. Confirm: "You've spent time on this. Are you sure you want to cancel?"
2538
+ `;
2539
+ } else if (contextState.uncertainFields.length > 0) {
2540
+ const uncertain = contextState.uncertainFields[0];
2541
+ contextText += `Ask user to confirm: "I understood your ${uncertain.label} as '${uncertain.value}'. Is that correct?"
2542
+ `;
2543
+ } else if (contextState.nextField) {
2544
+ const next = contextState.nextField;
2545
+ const prompt = next.askPrompt || `Ask for their ${next.label}`;
2546
+ contextText += `Next: ${prompt}
2547
+ `;
2548
+ if (next.example) {
2549
+ contextText += `Example: "${next.example}"
2550
+ `;
2551
+ }
2552
+ } else if (contextState.status === "ready") {
2553
+ contextText += `All fields collected! Nudge user to submit: "I have everything I need. Ready to submit?"
2554
+ `;
2555
+ }
2556
+ contextText += `
2557
+ `;
2558
+ contextText += `## User Can Say
2559
+ `;
2560
+ contextText += `- Provide information for any field
2561
+ `;
2562
+ contextText += `- "undo" or "go back" to revert last change
2563
+ `;
2564
+ contextText += `- "skip" to skip optional fields
2565
+ `;
2566
+ contextText += `- "why?" to get explanation about a field
2567
+ `;
2568
+ contextText += `- "how far?" to check progress
2569
+ `;
2570
+ contextText += `- "submit" or "done" when ready
2571
+ `;
2572
+ contextText += `- "save for later" to stash the form
2573
+ `;
2574
+ contextText += `- "cancel" to abandon the form
2575
+ `;
2576
+ } else {
2577
+ contextState = {
2578
+ hasActiveForm: false,
2579
+ progress: 0,
2580
+ filledFields: [],
2581
+ missingRequired: [],
2582
+ uncertainFields: [],
2583
+ nextField: null,
2584
+ stashedCount: stashed.length,
2585
+ pendingExternalFields: []
2586
+ };
2587
+ }
2588
+ if (stashed.length > 0) {
2589
+ contextText += `
2590
+ ## Saved Forms
2591
+ `;
2592
+ contextText += `User has ${stashed.length} saved form(s). They can say "resume" or "continue" to restore one.
2593
+ `;
2594
+ for (const s of stashed) {
2595
+ const form = formService.getForm(s.formId);
2596
+ const ctx = formService.getSessionContext(s);
2597
+ contextText += `- ${form?.name || s.formId} (${ctx.progress}% complete)
2598
+ `;
2599
+ }
2600
+ }
2601
+ return {
2602
+ data: JSON.parse(JSON.stringify(contextState)),
2603
+ values: {
2604
+ formContext: contextText,
2605
+ hasActiveForm: String(contextState.hasActiveForm),
2606
+ formProgress: String(contextState.progress),
2607
+ formStatus: contextState.status || "",
2608
+ stashedCount: String(stashed.length)
2609
+ },
2610
+ text: contextText
2611
+ };
2612
+ } catch (error) {
2613
+ logger4.error("[FormContextProvider] Error:", String(error));
2614
+ return {
2615
+ data: { hasActiveForm: false, error: true },
2616
+ values: { formContext: "Error loading form context." },
2617
+ text: "Error loading form context."
2618
+ };
2619
+ }
2620
+ }
2621
+ };
2622
+ context_default = formContextProvider;
2623
+ });
2624
+
2625
+ // index.ts
2626
+ init_builtins();
2627
+ init_validation();
2628
+ init_storage();
2629
+ init_extraction();
2630
+ init_types();
2631
+
2632
+ // src/ttl.ts
2633
+ init_types();
2634
+ function calculateTTL(session, form) {
2635
+ const config = form?.ttl || {};
2636
+ const minDays = config.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays;
2637
+ const maxDays = config.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays;
2638
+ const multiplier = config.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier;
2639
+ const minutesSpent = session.effort.timeSpentMs / 60000;
2640
+ const effortDays = minutesSpent * multiplier;
2641
+ const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
2642
+ return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
2643
+ }
2644
+ function shouldNudge(session, form) {
2645
+ const nudgeConfig = form?.nudge;
2646
+ if (nudgeConfig?.enabled === false) {
2647
+ return false;
2648
+ }
2649
+ const maxNudges = nudgeConfig?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges;
2650
+ if ((session.nudgeCount || 0) >= maxNudges) {
2651
+ return false;
2652
+ }
2653
+ const afterInactiveHours = nudgeConfig?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours;
2654
+ const inactiveMs = afterInactiveHours * 60 * 60 * 1000;
2655
+ const timeSinceInteraction = Date.now() - session.effort.lastInteractionAt;
2656
+ if (timeSinceInteraction < inactiveMs) {
2657
+ return false;
2658
+ }
2659
+ if (session.lastNudgeAt) {
2660
+ const timeSinceNudge = Date.now() - session.lastNudgeAt;
2661
+ if (timeSinceNudge < 24 * 60 * 60 * 1000) {
2662
+ return false;
2663
+ }
2664
+ }
2665
+ return true;
2666
+ }
2667
+ function isExpiringSoon(session, withinMs) {
2668
+ return session.expiresAt - Date.now() < withinMs;
2669
+ }
2670
+ function isExpired(session) {
2671
+ return session.expiresAt < Date.now();
2672
+ }
2673
+ function shouldConfirmCancel(session) {
2674
+ const minEffortMs = 5 * 60 * 1000;
2675
+ return session.effort.timeSpentMs > minEffortMs;
2676
+ }
2677
+ function formatTimeRemaining(session) {
2678
+ const remaining = session.expiresAt - Date.now();
2679
+ if (remaining <= 0) {
2680
+ return "expired";
2681
+ }
2682
+ const hours = Math.floor(remaining / (60 * 60 * 1000));
2683
+ const days = Math.floor(hours / 24);
2684
+ if (days > 0) {
2685
+ return `${days} day${days > 1 ? "s" : ""}`;
2686
+ }
2687
+ if (hours > 0) {
2688
+ return `${hours} hour${hours > 1 ? "s" : ""}`;
2689
+ }
2690
+ const minutes = Math.floor(remaining / (60 * 1000));
2691
+ return `${minutes} minute${minutes > 1 ? "s" : ""}`;
2692
+ }
2693
+ function formatEffort(session) {
2694
+ const minutes = Math.floor(session.effort.timeSpentMs / 60000);
2695
+ if (minutes < 1) {
2696
+ return "just started";
2697
+ }
2698
+ if (minutes < 60) {
2699
+ return `${minutes} minute${minutes > 1 ? "s" : ""}`;
2700
+ }
2701
+ const hours = Math.floor(minutes / 60);
2702
+ const remainingMinutes = minutes % 60;
2703
+ if (remainingMinutes === 0) {
2704
+ return `${hours} hour${hours > 1 ? "s" : ""}`;
2705
+ }
2706
+ return `${hours}h ${remainingMinutes}m`;
2707
+ }
2708
+ // src/defaults.ts
2709
+ init_types();
2710
+ function applyControlDefaults(control) {
2711
+ return {
2712
+ key: control.key,
2713
+ label: control.label || prettify(control.key),
2714
+ type: control.type || FORM_CONTROL_DEFAULTS.type,
2715
+ required: control.required ?? FORM_CONTROL_DEFAULTS.required,
2716
+ confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
2717
+ ...control
2718
+ };
2719
+ }
2720
+ function applyFormDefaults(form) {
2721
+ return {
2722
+ id: form.id,
2723
+ name: form.name || prettify(form.id),
2724
+ version: form.version ?? FORM_DEFINITION_DEFAULTS.version,
2725
+ status: form.status ?? FORM_DEFINITION_DEFAULTS.status,
2726
+ controls: (form.controls || []).map(applyControlDefaults),
2727
+ ux: {
2728
+ allowUndo: form.ux?.allowUndo ?? FORM_DEFINITION_DEFAULTS.ux.allowUndo,
2729
+ allowSkip: form.ux?.allowSkip ?? FORM_DEFINITION_DEFAULTS.ux.allowSkip,
2730
+ maxUndoSteps: form.ux?.maxUndoSteps ?? FORM_DEFINITION_DEFAULTS.ux.maxUndoSteps,
2731
+ showExamples: form.ux?.showExamples ?? FORM_DEFINITION_DEFAULTS.ux.showExamples,
2732
+ showExplanations: form.ux?.showExplanations ?? FORM_DEFINITION_DEFAULTS.ux.showExplanations,
2733
+ allowAutofill: form.ux?.allowAutofill ?? FORM_DEFINITION_DEFAULTS.ux.allowAutofill
2734
+ },
2735
+ ttl: {
2736
+ minDays: form.ttl?.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays,
2737
+ maxDays: form.ttl?.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays,
2738
+ effortMultiplier: form.ttl?.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier
2739
+ },
2740
+ nudge: {
2741
+ enabled: form.nudge?.enabled ?? FORM_DEFINITION_DEFAULTS.nudge.enabled,
2742
+ afterInactiveHours: form.nudge?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours,
2743
+ maxNudges: form.nudge?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges,
2744
+ message: form.nudge?.message
2745
+ },
2746
+ debug: form.debug ?? FORM_DEFINITION_DEFAULTS.debug,
2747
+ ...form
2748
+ };
2749
+ }
2750
+ function prettify(key) {
2751
+ return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2752
+ }
2753
+ // src/builder.ts
2754
+ class ControlBuilder {
2755
+ control;
2756
+ constructor(key) {
2757
+ this.control = { key };
2758
+ }
2759
+ static field(key) {
2760
+ return new ControlBuilder(key);
2761
+ }
2762
+ static text(key) {
2763
+ return new ControlBuilder(key).type("text");
2764
+ }
2765
+ static email(key) {
2766
+ return new ControlBuilder(key).type("email");
2767
+ }
2768
+ static number(key) {
2769
+ return new ControlBuilder(key).type("number");
2770
+ }
2771
+ static boolean(key) {
2772
+ return new ControlBuilder(key).type("boolean");
2773
+ }
2774
+ static select(key, options) {
2775
+ return new ControlBuilder(key).type("select").options(options);
2776
+ }
2777
+ static date(key) {
2778
+ return new ControlBuilder(key).type("date");
2779
+ }
2780
+ static file(key) {
2781
+ return new ControlBuilder(key).type("file");
2782
+ }
2783
+ type(type) {
2784
+ this.control.type = type;
2785
+ return this;
2786
+ }
2787
+ required() {
2788
+ this.control.required = true;
2789
+ return this;
2790
+ }
2791
+ optional() {
2792
+ this.control.required = false;
2793
+ return this;
2794
+ }
2795
+ hidden() {
2796
+ this.control.hidden = true;
2797
+ return this;
2798
+ }
2799
+ sensitive() {
2800
+ this.control.sensitive = true;
2801
+ return this;
2802
+ }
2803
+ readonly() {
2804
+ this.control.readonly = true;
2805
+ return this;
2806
+ }
2807
+ multiple() {
2808
+ this.control.multiple = true;
2809
+ return this;
2810
+ }
2811
+ pattern(regex) {
2812
+ this.control.pattern = regex;
2813
+ return this;
2814
+ }
2815
+ min(n) {
2816
+ this.control.min = n;
2817
+ return this;
2818
+ }
2819
+ max(n) {
2820
+ this.control.max = n;
2821
+ return this;
2822
+ }
2823
+ minLength(n) {
2824
+ this.control.minLength = n;
2825
+ return this;
2826
+ }
2827
+ maxLength(n) {
2828
+ this.control.maxLength = n;
2829
+ return this;
2830
+ }
2831
+ enum(values) {
2832
+ this.control.enum = values;
2833
+ return this;
2834
+ }
2835
+ options(opts) {
2836
+ this.control.options = opts;
2837
+ return this;
2838
+ }
2839
+ label(label) {
2840
+ this.control.label = label;
2841
+ return this;
2842
+ }
2843
+ ask(prompt) {
2844
+ this.control.askPrompt = prompt;
2845
+ return this;
2846
+ }
2847
+ description(desc) {
2848
+ this.control.description = desc;
2849
+ return this;
2850
+ }
2851
+ hint(...hints) {
2852
+ this.control.extractHints = hints;
2853
+ return this;
2854
+ }
2855
+ example(value) {
2856
+ this.control.example = value;
2857
+ return this;
2858
+ }
2859
+ confirmThreshold(n) {
2860
+ this.control.confirmThreshold = n;
2861
+ return this;
2862
+ }
2863
+ accept(mimeTypes) {
2864
+ this.control.file = { ...this.control.file, accept: mimeTypes };
2865
+ return this;
2866
+ }
2867
+ maxSize(bytes) {
2868
+ this.control.file = { ...this.control.file, maxSize: bytes };
2869
+ return this;
2870
+ }
2871
+ maxFiles(n) {
2872
+ this.control.file = { ...this.control.file, maxFiles: n };
2873
+ return this;
2874
+ }
2875
+ roles(...roles) {
2876
+ this.control.roles = roles;
2877
+ return this;
2878
+ }
2879
+ default(value) {
2880
+ this.control.defaultValue = value;
2881
+ return this;
2882
+ }
2883
+ dependsOn(field, condition = "exists", value) {
2884
+ this.control.dependsOn = { field, condition, value };
2885
+ return this;
2886
+ }
2887
+ dbbind(columnName) {
2888
+ this.control.dbbind = columnName;
2889
+ return this;
2890
+ }
2891
+ section(name) {
2892
+ this.control.ui = { ...this.control.ui, section: name };
2893
+ return this;
2894
+ }
2895
+ order(n) {
2896
+ this.control.ui = { ...this.control.ui, order: n };
2897
+ return this;
2898
+ }
2899
+ placeholder(text) {
2900
+ this.control.ui = { ...this.control.ui, placeholder: text };
2901
+ return this;
2902
+ }
2903
+ helpText(text) {
2904
+ this.control.ui = { ...this.control.ui, helpText: text };
2905
+ return this;
2906
+ }
2907
+ widget(type) {
2908
+ this.control.ui = { ...this.control.ui, widget: type };
2909
+ return this;
2910
+ }
2911
+ i18n(locale, translations) {
2912
+ this.control.i18n = { ...this.control.i18n, [locale]: translations };
2913
+ return this;
2914
+ }
2915
+ meta(key, value) {
2916
+ this.control.meta = { ...this.control.meta, [key]: value };
2917
+ return this;
2918
+ }
2919
+ build() {
2920
+ const control = {
2921
+ key: this.control.key,
2922
+ label: this.control.label || prettify2(this.control.key),
2923
+ type: this.control.type || "text",
2924
+ ...this.control
2925
+ };
2926
+ return control;
2927
+ }
2928
+ }
2929
+
2930
+ class FormBuilder {
2931
+ form;
2932
+ constructor(id) {
2933
+ this.form = { id, controls: [] };
2934
+ }
2935
+ static create(id) {
2936
+ return new FormBuilder(id);
2937
+ }
2938
+ name(name) {
2939
+ this.form.name = name;
2940
+ return this;
2941
+ }
2942
+ description(desc) {
2943
+ this.form.description = desc;
2944
+ return this;
2945
+ }
2946
+ version(v) {
2947
+ this.form.version = v;
2948
+ return this;
2949
+ }
2950
+ control(builder) {
2951
+ const ctrl = builder instanceof ControlBuilder ? builder.build() : builder;
2952
+ this.form.controls?.push(ctrl);
2953
+ return this;
2954
+ }
2955
+ controls(...builders) {
2956
+ for (const builder of builders) {
2957
+ this.control(builder);
2958
+ }
2959
+ return this;
2960
+ }
2961
+ required(...keys) {
2962
+ for (const key of keys) {
2963
+ this.control(ControlBuilder.field(key).required());
2964
+ }
2965
+ return this;
2966
+ }
2967
+ optional(...keys) {
2968
+ for (const key of keys) {
2969
+ this.control(ControlBuilder.field(key));
2970
+ }
2971
+ return this;
2972
+ }
2973
+ roles(...roles) {
2974
+ this.form.roles = roles;
2975
+ return this;
2976
+ }
2977
+ allowMultiple() {
2978
+ this.form.allowMultiple = true;
2979
+ return this;
2980
+ }
2981
+ noUndo() {
2982
+ this.form.ux = { ...this.form.ux, allowUndo: false };
2983
+ return this;
2984
+ }
2985
+ noSkip() {
2986
+ this.form.ux = { ...this.form.ux, allowSkip: false };
2987
+ return this;
2988
+ }
2989
+ noAutofill() {
2990
+ this.form.ux = { ...this.form.ux, allowAutofill: false };
2991
+ return this;
2992
+ }
2993
+ maxUndoSteps(n) {
2994
+ this.form.ux = { ...this.form.ux, maxUndoSteps: n };
2995
+ return this;
2996
+ }
2997
+ ttl(config) {
2998
+ this.form.ttl = { ...this.form.ttl, ...config };
2999
+ return this;
3000
+ }
3001
+ noNudge() {
3002
+ this.form.nudge = { ...this.form.nudge, enabled: false };
3003
+ return this;
3004
+ }
3005
+ nudgeAfter(hours) {
3006
+ this.form.nudge = { ...this.form.nudge, afterInactiveHours: hours };
3007
+ return this;
3008
+ }
3009
+ nudgeMessage(message) {
3010
+ this.form.nudge = { ...this.form.nudge, message };
3011
+ return this;
3012
+ }
3013
+ onStart(workerName) {
3014
+ this.form.hooks = { ...this.form.hooks, onStart: workerName };
3015
+ return this;
3016
+ }
3017
+ onFieldChange(workerName) {
3018
+ this.form.hooks = { ...this.form.hooks, onFieldChange: workerName };
3019
+ return this;
3020
+ }
3021
+ onReady(workerName) {
3022
+ this.form.hooks = { ...this.form.hooks, onReady: workerName };
3023
+ return this;
3024
+ }
3025
+ onSubmit(workerName) {
3026
+ this.form.hooks = { ...this.form.hooks, onSubmit: workerName };
3027
+ return this;
3028
+ }
3029
+ onCancel(workerName) {
3030
+ this.form.hooks = { ...this.form.hooks, onCancel: workerName };
3031
+ return this;
3032
+ }
3033
+ onExpire(workerName) {
3034
+ this.form.hooks = { ...this.form.hooks, onExpire: workerName };
3035
+ return this;
3036
+ }
3037
+ hooks(hooks) {
3038
+ this.form.hooks = { ...this.form.hooks, ...hooks };
3039
+ return this;
3040
+ }
3041
+ debug() {
3042
+ this.form.debug = true;
3043
+ return this;
3044
+ }
3045
+ i18n(locale, translations) {
3046
+ this.form.i18n = { ...this.form.i18n, [locale]: translations };
3047
+ return this;
3048
+ }
3049
+ meta(key, value) {
3050
+ this.form.meta = { ...this.form.meta, [key]: value };
3051
+ return this;
3052
+ }
3053
+ build() {
3054
+ const form = {
3055
+ id: this.form.id,
3056
+ name: this.form.name || prettify2(this.form.id),
3057
+ controls: this.form.controls || [],
3058
+ ...this.form
3059
+ };
3060
+ return form;
3061
+ }
3062
+ }
3063
+ var Form = FormBuilder;
3064
+ var C = ControlBuilder;
3065
+ function prettify2(key) {
3066
+ return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3067
+ }
3068
+
3069
+ // index.ts
3070
+ init_service();
3071
+ init_restore();
3072
+ init_extractor();
3073
+ init_context();
3074
+
3075
+ // src/tasks/nudge.ts
3076
+ import { logger as logger5 } from "@elizaos/core";
3077
+ var formNudgeWorker = {
3078
+ name: "form_nudge_check",
3079
+ validate: async (_runtime, _message, _state) => {
3080
+ return true;
3081
+ },
3082
+ execute: async (runtime, _options, _task) => {
3083
+ try {
3084
+ const formService = runtime.getService("FORM");
3085
+ if (!formService) {
3086
+ logger5.debug("[FormNudge] Form service not available");
3087
+ return;
3088
+ }
3089
+ logger5.debug("[FormNudge] Nudge check cycle completed");
3090
+ } catch (error) {
3091
+ logger5.error("[FormNudge] Error during nudge check:", String(error));
3092
+ }
3093
+ }
3094
+ };
3095
+ async function processEntityNudges(runtime, entityId) {
3096
+ const formService = runtime.getService("FORM");
3097
+ if (!formService)
3098
+ return;
3099
+ const activeSessions = await formService.getAllActiveSessions(entityId);
3100
+ const stashedSessions = await formService.getStashedSessions(entityId);
3101
+ const allSessions = [...activeSessions, ...stashedSessions];
3102
+ const now = Date.now();
3103
+ const expirationWarningMs = 24 * 60 * 60 * 1000;
3104
+ for (const session of allSessions) {
3105
+ const form = formService.getForm(session.formId);
3106
+ if (session.expiresAt < now) {
3107
+ session.status = "expired";
3108
+ await formService.saveSession(session);
3109
+ if (form?.hooks?.onExpire) {
3110
+ const worker = runtime.getTaskWorker(form.hooks.onExpire);
3111
+ if (worker) {
3112
+ try {
3113
+ await worker.execute(runtime, { session, form }, {});
3114
+ } catch (error) {
3115
+ logger5.error("[FormNudge] onExpire hook failed:", String(error));
3116
+ }
3117
+ }
3118
+ }
3119
+ logger5.debug(`[FormNudge] Session ${session.id} expired`);
3120
+ continue;
3121
+ }
3122
+ if (isExpiringSoon(session, expirationWarningMs) && !session.expirationWarned) {
3123
+ await sendExpirationWarning(runtime, session, form);
3124
+ session.expirationWarned = true;
3125
+ await formService.saveSession(session);
3126
+ continue;
3127
+ }
3128
+ if (session.status === "stashed" && shouldNudge(session, form)) {
3129
+ await sendNudge(runtime, session, form);
3130
+ session.nudgeCount = (session.nudgeCount || 0) + 1;
3131
+ session.lastNudgeAt = now;
3132
+ await formService.saveSession(session);
3133
+ }
3134
+ }
3135
+ }
3136
+ async function sendNudge(runtime, session, form) {
3137
+ const message = form?.nudge?.message || `You have an unfinished "${form?.name || "form"}". Would you like to continue?`;
3138
+ try {
3139
+ if (typeof runtime.sendMessageToRoom === "function") {
3140
+ await runtime.sendMessageToRoom(session.roomId, {
3141
+ text: message
3142
+ });
3143
+ logger5.debug(`[FormNudge] Sent nudge for session in room ${session.roomId}`);
3144
+ }
3145
+ } catch (error) {
3146
+ logger5.error("[FormNudge] Failed to send nudge:", String(error));
3147
+ }
3148
+ }
3149
+ async function sendExpirationWarning(runtime, session, form) {
3150
+ const remaining = formatTimeRemaining(session);
3151
+ const message = `Your "${form?.name || "form"}" form will expire in ${remaining}. Say "resume" to keep working on it.`;
3152
+ try {
3153
+ if (typeof runtime.sendMessageToRoom === "function") {
3154
+ await runtime.sendMessageToRoom(session.roomId, {
3155
+ text: message
3156
+ });
3157
+ logger5.debug(`[FormNudge] Sent expiration warning for session in room ${session.roomId}`);
3158
+ }
3159
+ } catch (error) {
3160
+ logger5.error("[FormNudge] Failed to send expiration warning:", String(error));
3161
+ }
3162
+ }
3163
+
3164
+ // index.ts
3165
+ var formPlugin = {
3166
+ name: "form",
3167
+ description: "Agent-native conversational forms for data collection",
3168
+ services: [
3169
+ {
3170
+ serviceType: "FORM",
3171
+ start: async (runtime) => {
3172
+ const { FormService: FormService2 } = await Promise.resolve().then(() => (init_service(), exports_service));
3173
+ return FormService2.start(runtime);
3174
+ }
3175
+ }
3176
+ ],
3177
+ providers: [
3178
+ {
3179
+ name: "FORM_CONTEXT",
3180
+ description: "Provides context about active form sessions",
3181
+ get: async (runtime, message, state) => {
3182
+ const { formContextProvider: formContextProvider2 } = await Promise.resolve().then(() => (init_context(), exports_context));
3183
+ return formContextProvider2.get(runtime, message, state);
3184
+ }
3185
+ }
3186
+ ],
3187
+ evaluators: [
3188
+ {
3189
+ name: "form_evaluator",
3190
+ description: "Extracts form fields and handles form intents",
3191
+ similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
3192
+ examples: [],
3193
+ validate: async (runtime, message, state) => {
3194
+ const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
3195
+ return formEvaluator2.validate(runtime, message, state);
3196
+ },
3197
+ handler: async (runtime, message, state) => {
3198
+ const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
3199
+ return formEvaluator2.handler(runtime, message, state);
3200
+ }
3201
+ }
3202
+ ],
3203
+ actions: [
3204
+ {
3205
+ name: "FORM_RESTORE",
3206
+ similes: ["RESUME_FORM", "CONTINUE_FORM"],
3207
+ description: "Restore a previously stashed form session",
3208
+ validate: async (runtime, message, state) => {
3209
+ const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
3210
+ return formRestoreAction2.validate(runtime, message, state);
3211
+ },
3212
+ handler: async (runtime, message, state, options, callback) => {
3213
+ const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
3214
+ return formRestoreAction2.handler(runtime, message, state, options, callback);
3215
+ },
3216
+ examples: [
3217
+ [
3218
+ {
3219
+ name: "{{user1}}",
3220
+ content: { text: "Resume my form" }
3221
+ },
3222
+ {
3223
+ name: "{{agentName}}",
3224
+ content: {
3225
+ text: "I've restored your form. Let's continue where you left off."
3226
+ }
3227
+ }
3228
+ ]
3229
+ ]
3230
+ }
3231
+ ]
3232
+ };
3233
+ var typescript_default = formPlugin;
3234
+ export {
3235
+ validateField,
3236
+ shouldNudge,
3237
+ shouldConfirmCancel,
3238
+ saveSubmission,
3239
+ saveSession,
3240
+ saveAutofillData,
3241
+ registerTypeHandler,
3242
+ registerBuiltinTypes,
3243
+ quickIntentDetect,
3244
+ processEntityNudges,
3245
+ prettify,
3246
+ parseValue,
3247
+ matchesMimeType,
3248
+ llmIntentAndExtract,
3249
+ isUXIntent,
3250
+ isLifecycleIntent,
3251
+ isExpiringSoon,
3252
+ isExpired,
3253
+ isBuiltinType,
3254
+ hasDataToExtract,
3255
+ getTypeHandler,
3256
+ getSubmissions,
3257
+ getStashedSessions,
3258
+ getBuiltinType,
3259
+ getAutofillData,
3260
+ getAllActiveSessions,
3261
+ getActiveSession,
3262
+ formatValue,
3263
+ formatTimeRemaining,
3264
+ formatEffort,
3265
+ formRestoreAction,
3266
+ formPlugin,
3267
+ formNudgeWorker,
3268
+ formEvaluator,
3269
+ formContextProvider,
3270
+ extractSingleField,
3271
+ detectCorrection,
3272
+ deleteSession,
3273
+ typescript_default as default,
3274
+ clearTypeHandlers,
3275
+ calculateTTL,
3276
+ applyFormDefaults,
3277
+ applyControlDefaults,
3278
+ FormService,
3279
+ FormBuilder,
3280
+ Form,
3281
+ FORM_SUBMISSION_COMPONENT,
3282
+ FORM_SESSION_COMPONENT,
3283
+ FORM_DEFINITION_DEFAULTS,
3284
+ FORM_CONTROL_DEFAULTS,
3285
+ FORM_AUTOFILL_COMPONENT,
3286
+ ControlBuilder,
3287
+ C,
3288
+ BUILTIN_TYPE_MAP,
3289
+ BUILTIN_TYPES
3290
+ };
3291
+
3292
+ //# debugId=D9CCEBB86B648A7B64756E2164756E21
3293
+ //# sourceMappingURL=index.js.map