@voicelayer/sdk 0.1.7 → 0.1.9
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.d.ts +25 -1002
- package/dist/index.js +302 -122
- package/dist/runtime/text-session.d.ts +1 -1
- package/dist/runtime/text-session.js +753 -17
- package/dist/text-session-D1j8nQac.d.ts +1996 -0
- package/package.json +5 -5
- package/dist/text-session-B8-jhQzv.d.ts +0 -837
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import 'quickjs-emscripten';
|
|
4
|
-
import { setup } from 'xstate';
|
|
5
|
-
import 'crypto';
|
|
4
|
+
import { setup, createActor } from 'xstate';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
6
|
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
8
8
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -42,8 +42,46 @@ var init_graph_model = __esm({
|
|
|
42
42
|
"src/runtime/graph/graph-model.ts"() {
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
// ../contracts/src/primitive.ts
|
|
47
|
+
var BasePrimitive;
|
|
45
48
|
var init_primitive = __esm({
|
|
46
49
|
"../contracts/src/primitive.ts"() {
|
|
50
|
+
BasePrimitive = class {
|
|
51
|
+
async onError(err, _ctx) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "fail",
|
|
54
|
+
userMessage: "I ran into an issue on my end. Let me transfer you to a teammate."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async healthCheck() {
|
|
58
|
+
return { healthy: true, lastCheckedAt: /* @__PURE__ */ new Date() };
|
|
59
|
+
}
|
|
60
|
+
getMetrics() {
|
|
61
|
+
return {
|
|
62
|
+
callsInFlight: 0,
|
|
63
|
+
p50LatencyMs: 0,
|
|
64
|
+
p90LatencyMs: 0,
|
|
65
|
+
p99LatencyMs: 0,
|
|
66
|
+
errorRate: 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
validateInput(_input) {
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
validateOutput(_output) {
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
// The event bus is injected by the orchestrator at boot time. These
|
|
76
|
+
// methods are intentionally stubs — see packages/event-bus for the real
|
|
77
|
+
// implementation.
|
|
78
|
+
emit(_event) {
|
|
79
|
+
}
|
|
80
|
+
subscribe(_type, _handler) {
|
|
81
|
+
return () => {
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
47
85
|
}
|
|
48
86
|
});
|
|
49
87
|
|
|
@@ -64,8 +102,13 @@ var init_errors = __esm({
|
|
|
64
102
|
"../contracts/src/errors.ts"() {
|
|
65
103
|
}
|
|
66
104
|
});
|
|
105
|
+
|
|
106
|
+
// ../contracts/src/ids.ts
|
|
107
|
+
var PrimitiveId, SemanticVersion;
|
|
67
108
|
var init_ids = __esm({
|
|
68
109
|
"../contracts/src/ids.ts"() {
|
|
110
|
+
PrimitiveId = (s) => s;
|
|
111
|
+
SemanticVersion = (s) => s;
|
|
69
112
|
}
|
|
70
113
|
});
|
|
71
114
|
var FlowKind, FlowStatus, FlowNodeType, FlowNodePosition, FlowNode, FlowEdge, FlowGraph;
|
|
@@ -97,6 +140,9 @@ var init_flow = __esm({
|
|
|
97
140
|
"llm",
|
|
98
141
|
"rag",
|
|
99
142
|
"memory",
|
|
143
|
+
// Call-wide voice/turn-taking capability (barge-in + endpointing tuning);
|
|
144
|
+
// managed in the Capabilities panel like `memory`, never a graph step.
|
|
145
|
+
"speech",
|
|
100
146
|
"classify",
|
|
101
147
|
// Agentic router: per-turn, selects 1-of-N skills by their plain-language
|
|
102
148
|
// descriptions and takes that port (the resident routing surface / hub).
|
|
@@ -295,6 +341,19 @@ var init_agents = __esm({
|
|
|
295
341
|
}).optional(),
|
|
296
342
|
// Tool capabilities compiled from tool nodes (maps to AgentConfig.tools).
|
|
297
343
|
tools: z.array(ProcessToolDTO).max(32).optional(),
|
|
344
|
+
// Voice turn-taking compiled from the flow's speech capability node (maps to
|
|
345
|
+
// AgentConfig.speech → applySpeechTurnHandling). Absent = platform defaults.
|
|
346
|
+
speech: z.object({
|
|
347
|
+
interruption: z.object({
|
|
348
|
+
enabled: z.boolean().optional(),
|
|
349
|
+
minWords: z.number().int().min(0).max(10).optional(),
|
|
350
|
+
minDuration: z.number().int().min(0).max(5e3).optional()
|
|
351
|
+
}).optional(),
|
|
352
|
+
endpointing: z.object({
|
|
353
|
+
minDelay: z.number().int().min(0).max(5e3).optional(),
|
|
354
|
+
maxDelay: z.number().int().min(0).max(15e3).optional()
|
|
355
|
+
}).optional()
|
|
356
|
+
}).optional(),
|
|
298
357
|
// Graph-preserving compiled program. When present and the flow-runtime graph
|
|
299
358
|
// mode is enabled, the worker executes this node-graph deterministically
|
|
300
359
|
// (the interpreter) instead of the flat goals+rails form above. Embedded here
|
|
@@ -452,7 +511,7 @@ var init_consultation = __esm({
|
|
|
452
511
|
});
|
|
453
512
|
}
|
|
454
513
|
});
|
|
455
|
-
var CallEventKind, SayCommand, HangupCommand, DtmfCommand, InstructCommand;
|
|
514
|
+
var CallEventKind, OP_CALL_EVENT_KIND, OpCallEventKind, SayCommand, HangupCommand, DtmfCommand, InstructCommand;
|
|
456
515
|
var init_call_events = __esm({
|
|
457
516
|
"../contracts/src/call-events.ts"() {
|
|
458
517
|
init_consultation();
|
|
@@ -477,6 +536,8 @@ var init_call_events = __esm({
|
|
|
477
536
|
"dtmf.received",
|
|
478
537
|
"dtmf.sent"
|
|
479
538
|
]);
|
|
539
|
+
OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
|
|
540
|
+
OpCallEventKind = z.string().regex(OP_CALL_EVENT_KIND);
|
|
480
541
|
z.object({
|
|
481
542
|
reason: z.string().optional(),
|
|
482
543
|
durationMs: z.number().int().min(0).optional(),
|
|
@@ -514,7 +575,8 @@ var init_call_events = __esm({
|
|
|
514
575
|
seq: z.number().int().min(0),
|
|
515
576
|
callId: z.string().uuid(),
|
|
516
577
|
projectId: z.string().min(1),
|
|
517
|
-
|
|
578
|
+
// Closed lifecycle kinds OR the namespaced operational vocabulary.
|
|
579
|
+
kind: z.union([CallEventKind, OpCallEventKind]),
|
|
518
580
|
payload: z.unknown(),
|
|
519
581
|
ts: z.string().datetime()
|
|
520
582
|
});
|
|
@@ -2602,7 +2664,7 @@ var init_executors = __esm({
|
|
|
2602
2664
|
const authoredSlot = slot === node.id ? "" : slot.trim();
|
|
2603
2665
|
const label = authoredSlot.replace(/[_-]+/g, " ");
|
|
2604
2666
|
if (hasSlot(deps.processRt.getData(), slot)) {
|
|
2605
|
-
|
|
2667
|
+
deps.events.note("ask.skipped", { slot: slot.trim(), reason: "already known" });
|
|
2606
2668
|
return { kind: "advance" };
|
|
2607
2669
|
}
|
|
2608
2670
|
const askText = interpolate(
|
|
@@ -2616,10 +2678,10 @@ var init_executors = __esm({
|
|
|
2616
2678
|
state.lastUtterance = t.text;
|
|
2617
2679
|
state.lastDtmf = t.dtmf;
|
|
2618
2680
|
if (hasSlot(deps.processRt.getData(), slot)) {
|
|
2619
|
-
|
|
2681
|
+
deps.events.note("ask.captured", { slot: slot.trim() });
|
|
2620
2682
|
return { kind: "advance" };
|
|
2621
2683
|
}
|
|
2622
|
-
|
|
2684
|
+
deps.events.note("ask.clarify", { slot: slot.trim(), turn });
|
|
2623
2685
|
await withHistory(
|
|
2624
2686
|
deps,
|
|
2625
2687
|
historyPolicyOf(node),
|
|
@@ -2645,7 +2707,7 @@ var init_executors = __esm({
|
|
|
2645
2707
|
if (msg) await speak(interpolate(msg, scopeFor(state, deps)), deps);
|
|
2646
2708
|
return { kind: "end" };
|
|
2647
2709
|
};
|
|
2648
|
-
handoffExecutor = async (node,
|
|
2710
|
+
handoffExecutor = async (node, state, deps) => {
|
|
2649
2711
|
const cfg = node.config;
|
|
2650
2712
|
const target = str(cfg.to) || str(cfg.fallback);
|
|
2651
2713
|
if (!target) {
|
|
@@ -2654,9 +2716,14 @@ var init_executors = __esm({
|
|
|
2654
2716
|
}
|
|
2655
2717
|
const reason = str(cfg.reason) || `flow:${node.id}`;
|
|
2656
2718
|
const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
|
|
2719
|
+
const briefing = str(cfg.briefing) ? interpolate(str(cfg.briefing), scopeFor(state, deps)) : void 0;
|
|
2657
2720
|
deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
|
|
2658
2721
|
try {
|
|
2659
|
-
await deps.ctx.handoff(target, {
|
|
2722
|
+
await deps.ctx.handoff(target, {
|
|
2723
|
+
reason,
|
|
2724
|
+
...mode ? { mode } : {},
|
|
2725
|
+
...briefing ? { briefing } : {}
|
|
2726
|
+
});
|
|
2660
2727
|
} catch (err) {
|
|
2661
2728
|
deps.events.note("handoff.failed", {
|
|
2662
2729
|
node: node.id,
|
|
@@ -2710,7 +2777,24 @@ ${collected}`);
|
|
|
2710
2777
|
const cfg = node.config;
|
|
2711
2778
|
const name = str(cfg.name) || node.id;
|
|
2712
2779
|
const routeByOutcome = () => {
|
|
2713
|
-
const
|
|
2780
|
+
const result = state.toolResults[node.id];
|
|
2781
|
+
const ok = result?.ok === true;
|
|
2782
|
+
if (ok && cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
2783
|
+
for (const [slot, path] of Object.entries(cfg.outputAs)) {
|
|
2784
|
+
if (typeof path !== "string" || !path.trim()) continue;
|
|
2785
|
+
const value = resolvePath(result, path.trim());
|
|
2786
|
+
if (value === void 0 || value === null) {
|
|
2787
|
+
deps.events.note("tool.output_missing", { node: node.id, slot, path });
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
const captured = deps.processRt.captureField(slot, value);
|
|
2791
|
+
deps.events.note(captured ? "tool.output_captured" : "tool.output_rejected", {
|
|
2792
|
+
node: node.id,
|
|
2793
|
+
slot,
|
|
2794
|
+
...captured ? {} : { reason: "slot not declared or value not coercible" }
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2714
2798
|
return routeByPort(node, deps, ok ? "ok" : "fail");
|
|
2715
2799
|
};
|
|
2716
2800
|
const toolRef = str(cfg.toolRef);
|
|
@@ -3217,13 +3301,19 @@ ${collected}`);
|
|
|
3217
3301
|
setExecutor = async (node, state, deps) => {
|
|
3218
3302
|
const assignments = Array.isArray(node.config.assignments) ? node.config.assignments : [];
|
|
3219
3303
|
let applied = 0;
|
|
3304
|
+
const rejected = [];
|
|
3220
3305
|
for (const a of assignments) {
|
|
3221
3306
|
if (!a || typeof a.name !== "string" || !a.name.trim()) continue;
|
|
3307
|
+
const name = a.name.trim();
|
|
3222
3308
|
const value = interpolate(typeof a.value === "string" ? a.value : "", scopeFor(state, deps));
|
|
3223
|
-
deps.processRt.captureField(
|
|
3224
|
-
|
|
3309
|
+
if (deps.processRt.captureField(name, value)) applied += 1;
|
|
3310
|
+
else rejected.push(name);
|
|
3225
3311
|
}
|
|
3226
|
-
deps.events.note("set.applied", {
|
|
3312
|
+
deps.events.note("set.applied", {
|
|
3313
|
+
node: node.id,
|
|
3314
|
+
count: applied,
|
|
3315
|
+
...rejected.length ? { rejected } : {}
|
|
3316
|
+
});
|
|
3227
3317
|
return { kind: "advance" };
|
|
3228
3318
|
};
|
|
3229
3319
|
MAX_SUBFLOW_DEPTH = 3;
|
|
@@ -3681,7 +3771,54 @@ init_openai_default();
|
|
|
3681
3771
|
// ../primitives/process-schema/src/index.ts
|
|
3682
3772
|
init_src();
|
|
3683
3773
|
var PROCESS_LIFECYCLE_MAX_EVENTS = 100;
|
|
3684
|
-
|
|
3774
|
+
function isZodSchema(v) {
|
|
3775
|
+
return typeof v === "object" && v !== null && "_def" in v && "parse" in v;
|
|
3776
|
+
}
|
|
3777
|
+
function isPlainObject(value) {
|
|
3778
|
+
if (value === null || typeof value !== "object") return false;
|
|
3779
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
3780
|
+
}
|
|
3781
|
+
function compileFieldValidator(field) {
|
|
3782
|
+
if (isZodSchema(field.validation)) return field.validation;
|
|
3783
|
+
return legacySpecToZod(isPlainObject(field.validation) ? field.validation : {});
|
|
3784
|
+
}
|
|
3785
|
+
function legacySpecToZod(spec) {
|
|
3786
|
+
switch (spec.type) {
|
|
3787
|
+
case "number": {
|
|
3788
|
+
let s = z.number();
|
|
3789
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3790
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3791
|
+
return narrowEnum(s, spec.enum);
|
|
3792
|
+
}
|
|
3793
|
+
case "boolean":
|
|
3794
|
+
return narrowEnum(z.boolean(), spec.enum);
|
|
3795
|
+
case "datetime":
|
|
3796
|
+
return z.string().datetime();
|
|
3797
|
+
case "array": {
|
|
3798
|
+
let s = z.array(z.unknown());
|
|
3799
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3800
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3801
|
+
return s;
|
|
3802
|
+
}
|
|
3803
|
+
case "object":
|
|
3804
|
+
return z.record(z.string(), z.unknown());
|
|
3805
|
+
case "string":
|
|
3806
|
+
default: {
|
|
3807
|
+
let s = z.string();
|
|
3808
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3809
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3810
|
+
if (spec.pattern !== void 0) s = s.regex(new RegExp(spec.pattern));
|
|
3811
|
+
return narrowEnum(s, spec.enum);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
function narrowEnum(base, enumValues) {
|
|
3816
|
+
if (!enumValues || enumValues.length === 0) return base;
|
|
3817
|
+
return base.refine((v) => enumValues.some((c) => Object.is(c, v)), {
|
|
3818
|
+
message: "value not in enum"
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
var machine = setup({
|
|
3685
3822
|
types: {},
|
|
3686
3823
|
guards: {
|
|
3687
3824
|
allRequiredCaptured: ({ context }) => context.requiredFields.every((n) => context.fieldsCaptured.has(n)),
|
|
@@ -3776,10 +3913,277 @@ setup({
|
|
|
3776
3913
|
}
|
|
3777
3914
|
}
|
|
3778
3915
|
});
|
|
3916
|
+
var ProcessSchemaPrimitive = class extends BasePrimitive {
|
|
3917
|
+
primitiveId = PrimitiveId("process-schema");
|
|
3918
|
+
version = SemanticVersion("0.2.0");
|
|
3919
|
+
schema = null;
|
|
3920
|
+
compiledFields = /* @__PURE__ */ new Map();
|
|
3921
|
+
actor;
|
|
3922
|
+
lastSnapshot = null;
|
|
3923
|
+
lastBlockedReason;
|
|
3924
|
+
constructor() {
|
|
3925
|
+
super();
|
|
3926
|
+
this.actor = createActor(machine);
|
|
3927
|
+
this.actor.start();
|
|
3928
|
+
}
|
|
3929
|
+
async loadSchema(schema) {
|
|
3930
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3931
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
3932
|
+
for (const field of schema.fields) {
|
|
3933
|
+
if (seen.has(field.name)) throw new Error(`duplicate process field: ${field.name}`);
|
|
3934
|
+
seen.add(field.name);
|
|
3935
|
+
compiled.set(field.name, { ...field, validator: compileFieldValidator(field) });
|
|
3936
|
+
}
|
|
3937
|
+
for (const required of schema.completionGate.requiredFields) {
|
|
3938
|
+
if (!seen.has(required)) {
|
|
3939
|
+
throw new Error(`completion gate references unknown field: ${required}`);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
this.schema = schema;
|
|
3943
|
+
this.compiledFields = compiled;
|
|
3944
|
+
this.lastBlockedReason = void 0;
|
|
3945
|
+
this.actor.send({
|
|
3946
|
+
type: "LOAD",
|
|
3947
|
+
requiredFields: schema.completionGate.requiredFields,
|
|
3948
|
+
requiresAck: schema.completionGate.backendAck !== void 0
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
async onCallStart(_ctx) {
|
|
3952
|
+
if (this.schema === null) return;
|
|
3953
|
+
this.lastBlockedReason = void 0;
|
|
3954
|
+
this.actor.send({
|
|
3955
|
+
type: "LOAD",
|
|
3956
|
+
requiredFields: this.schema.completionGate.requiredFields,
|
|
3957
|
+
requiresAck: this.schema.completionGate.backendAck !== void 0
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
async onCallEnd(_ctx, outcome) {
|
|
3961
|
+
if (this.schema === null) return;
|
|
3962
|
+
this.lastSnapshot = this.exportSnapshot();
|
|
3963
|
+
if (outcome.processCompleted) {
|
|
3964
|
+
this.actor.send({ type: "COMPLETE" });
|
|
3965
|
+
} else {
|
|
3966
|
+
const gate = this.canCallEnd();
|
|
3967
|
+
if (!gate.ok) this.lastBlockedReason = gate.reason;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
getCurrentState() {
|
|
3971
|
+
const snap = this.actor.getSnapshot();
|
|
3972
|
+
const ctx = snap.context;
|
|
3973
|
+
const blockedReason = this.computeBlockedReason(snap);
|
|
3974
|
+
return {
|
|
3975
|
+
currentNode: this.currentNodeName(snap),
|
|
3976
|
+
fieldsCaptured: new Map(ctx.fieldsCaptured),
|
|
3977
|
+
backendAckReceived: ctx.backendAckReceived,
|
|
3978
|
+
...blockedReason === void 0 ? {} : { blockedReason },
|
|
3979
|
+
...ctx.backendAckReceivedAt === void 0 ? {} : { backendAckReceivedAt: ctx.backendAckReceivedAt }
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
getFieldStatus() {
|
|
3983
|
+
if (this.schema === null) return [];
|
|
3984
|
+
const ctx = this.actor.getSnapshot().context;
|
|
3985
|
+
return this.schema.fields.map((field) => {
|
|
3986
|
+
const captured = ctx.fieldsCaptured.has(field.name);
|
|
3987
|
+
const value = ctx.fieldsCaptured.get(field.name);
|
|
3988
|
+
const validator = this.compiledFields.get(field.name)?.validator;
|
|
3989
|
+
const valid = captured && validator ? validator.safeParse(value).success : !captured;
|
|
3990
|
+
return { name: field.name, captured, value, required: field.required, valid };
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
getCompletionPercentage() {
|
|
3994
|
+
if (this.schema === null) return 0;
|
|
3995
|
+
const requiredCount = this.schema.completionGate.requiredFields.length;
|
|
3996
|
+
const requiresAck = this.schema.completionGate.backendAck !== void 0;
|
|
3997
|
+
const denom = requiredCount + (requiresAck ? 1 : 0);
|
|
3998
|
+
if (denom === 0) return 100;
|
|
3999
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4000
|
+
const capturedRequired = this.schema.completionGate.requiredFields.filter(
|
|
4001
|
+
(n) => ctx.fieldsCaptured.has(n)
|
|
4002
|
+
).length;
|
|
4003
|
+
const ackCount = ctx.backendAckReceived ? 1 : 0;
|
|
4004
|
+
return Math.round((capturedRequired + ackCount) / denom * 100);
|
|
4005
|
+
}
|
|
4006
|
+
canCallEnd() {
|
|
4007
|
+
if (this.schema === null) return { ok: false, reason: "schema_not_loaded", missingFields: [] };
|
|
4008
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4009
|
+
const missingFields = this.schema.completionGate.requiredFields.filter(
|
|
4010
|
+
(n) => !ctx.fieldsCaptured.has(n)
|
|
4011
|
+
);
|
|
4012
|
+
if (missingFields.length > 0) {
|
|
4013
|
+
return {
|
|
4014
|
+
ok: false,
|
|
4015
|
+
reason: `${missingFields.length} required field(s) missing`,
|
|
4016
|
+
missingFields
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
if (this.schema.completionGate.backendAck !== void 0 && !ctx.backendAckReceived) {
|
|
4020
|
+
return { ok: false, reason: "backend acknowledgement pending", missingFields: [] };
|
|
4021
|
+
}
|
|
4022
|
+
return { ok: true };
|
|
4023
|
+
}
|
|
4024
|
+
captureField(f) {
|
|
4025
|
+
if (this.schema === null) return { ok: false, reason: "schema_not_loaded" };
|
|
4026
|
+
const field = this.compiledFields.get(f.name);
|
|
4027
|
+
if (field === void 0) return { ok: false, reason: `unknown field: ${f.name}` };
|
|
4028
|
+
const parsed = field.validator.safeParse(f.value);
|
|
4029
|
+
if (!parsed.success) {
|
|
4030
|
+
const reason = parsed.error.issues[0]?.message ?? "invalid value";
|
|
4031
|
+
this.lastBlockedReason = `${f.name}: ${reason}`;
|
|
4032
|
+
return { ok: false, reason };
|
|
4033
|
+
}
|
|
4034
|
+
const before = this.actor.getSnapshot().context.fieldsCaptured.get(f.name);
|
|
4035
|
+
const clonedValue = structuredClone(parsed.data);
|
|
4036
|
+
this.actor.send({ type: "CAPTURE", name: f.name, value: clonedValue });
|
|
4037
|
+
this.lastBlockedReason = void 0;
|
|
4038
|
+
return { ok: true, advanced: !Object.is(before, parsed.data) };
|
|
4039
|
+
}
|
|
4040
|
+
recordBackendAck(receivedAt = /* @__PURE__ */ new Date()) {
|
|
4041
|
+
this.actor.send({ type: "ACK", receivedAt });
|
|
4042
|
+
}
|
|
4043
|
+
exportSnapshot() {
|
|
4044
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4045
|
+
return {
|
|
4046
|
+
schemaId: this.schema?.id ?? "unloaded",
|
|
4047
|
+
currentNode: this.currentNodeName(this.actor.getSnapshot()),
|
|
4048
|
+
fieldsCaptured: Object.fromEntries(ctx.fieldsCaptured),
|
|
4049
|
+
backendAckReceived: ctx.backendAckReceived,
|
|
4050
|
+
...ctx.backendAckReceivedAt === void 0 ? {} : { backendAckReceivedAt: new Date(ctx.backendAckReceivedAt) },
|
|
4051
|
+
// Defensive copy — callers shouldn't be able to mutate the live array.
|
|
4052
|
+
lifecycle: ctx.lifecycle.slice()
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Record a side-channel lifecycle event. Does not advance the state machine.
|
|
4057
|
+
* Triggers the snapshot subscription so the dashboard sees the event in
|
|
4058
|
+
* real time alongside any state transitions.
|
|
4059
|
+
*/
|
|
4060
|
+
emitLifecycle(event) {
|
|
4061
|
+
this.actor.send({ type: "EMIT_LIFECYCLE", event });
|
|
4062
|
+
}
|
|
4063
|
+
hydrateSnapshot(snapshot) {
|
|
4064
|
+
if (this.schema === null) {
|
|
4065
|
+
throw new Error("cannot hydrate process state before schema is loaded");
|
|
4066
|
+
}
|
|
4067
|
+
if (snapshot.schemaId !== this.schema.id) {
|
|
4068
|
+
throw new Error(`snapshot schema mismatch: ${snapshot.schemaId} !== ${this.schema.id}`);
|
|
4069
|
+
}
|
|
4070
|
+
this.actor.send({
|
|
4071
|
+
type: "LOAD",
|
|
4072
|
+
requiredFields: this.schema.completionGate.requiredFields,
|
|
4073
|
+
requiresAck: this.schema.completionGate.backendAck !== void 0
|
|
4074
|
+
});
|
|
4075
|
+
for (const [name, value] of Object.entries(snapshot.fieldsCaptured)) {
|
|
4076
|
+
this.actor.send({ type: "CAPTURE", name, value });
|
|
4077
|
+
}
|
|
4078
|
+
if (snapshot.backendAckReceived) {
|
|
4079
|
+
this.actor.send({ type: "ACK", receivedAt: snapshot.backendAckReceivedAt ?? /* @__PURE__ */ new Date() });
|
|
4080
|
+
}
|
|
4081
|
+
for (const ev of snapshot.lifecycle) {
|
|
4082
|
+
this.actor.send({ type: "EMIT_LIFECYCLE", event: ev });
|
|
4083
|
+
}
|
|
4084
|
+
this.lastSnapshot = snapshot;
|
|
4085
|
+
}
|
|
4086
|
+
getLastSnapshot() {
|
|
4087
|
+
return this.lastSnapshot;
|
|
4088
|
+
}
|
|
4089
|
+
/** @internal — for XState devtools / Stately Inspector. */
|
|
4090
|
+
getActor() {
|
|
4091
|
+
return this.actor;
|
|
4092
|
+
}
|
|
4093
|
+
/**
|
|
4094
|
+
* Fire-and-forget subscription to state-machine transitions. Listener receives
|
|
4095
|
+
* a fresh `ProcessSnapshot` on every actor transition (capture, ack, gate).
|
|
4096
|
+
*
|
|
4097
|
+
* Hot-path contract: the listener is invoked synchronously inside the actor
|
|
4098
|
+
* subscription; callers MUST NOT await any I/O inside it. For external fan-out
|
|
4099
|
+
* (BullMQ, SSE), enqueue work without awaiting — `queue.add(...).catch(noop)`.
|
|
4100
|
+
*
|
|
4101
|
+
* Returns an unsubscribe function.
|
|
4102
|
+
*/
|
|
4103
|
+
onSnapshot(listener) {
|
|
4104
|
+
const sub = this.actor.subscribe(() => {
|
|
4105
|
+
try {
|
|
4106
|
+
listener(this.exportSnapshot());
|
|
4107
|
+
} catch (err) {
|
|
4108
|
+
console.error("[process-schema] subscriber threw", err);
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
return () => sub.unsubscribe();
|
|
4112
|
+
}
|
|
4113
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
4114
|
+
currentNodeName(snap) {
|
|
4115
|
+
const value = snap.value;
|
|
4116
|
+
return typeof value === "string" ? value : Object.keys(value)[0] ?? "unloaded";
|
|
4117
|
+
}
|
|
4118
|
+
computeBlockedReason(snap) {
|
|
4119
|
+
if (this.schema === null) return "schema_not_loaded";
|
|
4120
|
+
if (this.lastBlockedReason !== void 0) return this.lastBlockedReason;
|
|
4121
|
+
const ctx = snap.context;
|
|
4122
|
+
const missing = this.schema.completionGate.requiredFields.filter((n) => !ctx.fieldsCaptured.has(n));
|
|
4123
|
+
if (missing.length > 0) return `${missing.length} required field(s) missing`;
|
|
4124
|
+
if (this.schema.completionGate.backendAck !== void 0 && !ctx.backendAckReceived) {
|
|
4125
|
+
return "backend acknowledgement pending";
|
|
4126
|
+
}
|
|
4127
|
+
return void 0;
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
var DEFAULT_SCHEMA_ID = "process";
|
|
4131
|
+
function buildProcessSchema(def, schemaId = DEFAULT_SCHEMA_ID) {
|
|
4132
|
+
const fields = Object.entries(def.collect).map(([name, field]) => ({
|
|
4133
|
+
name,
|
|
4134
|
+
required: field.required === true,
|
|
4135
|
+
validation: fieldToZod(field)
|
|
4136
|
+
}));
|
|
4137
|
+
const requiredFields = Object.entries(def.collect).filter(([, f]) => f.required === true).map(([n]) => n);
|
|
4138
|
+
const gate = {
|
|
4139
|
+
requiredFields,
|
|
4140
|
+
...def.backendAck ? {
|
|
4141
|
+
backendAck: {
|
|
4142
|
+
url: def.backendAck.url,
|
|
4143
|
+
timeoutMs: def.backendAck.timeoutMs ?? 3e4
|
|
4144
|
+
}
|
|
4145
|
+
} : {}
|
|
4146
|
+
};
|
|
4147
|
+
return { id: schemaId, fields, completionGate: gate };
|
|
4148
|
+
}
|
|
4149
|
+
function fieldToZod(field) {
|
|
4150
|
+
switch (field.type) {
|
|
4151
|
+
case "string":
|
|
4152
|
+
case "email":
|
|
4153
|
+
case "phone": {
|
|
4154
|
+
let s = z.string();
|
|
4155
|
+
if (field.type === "email") s = s.email();
|
|
4156
|
+
if (field.pattern) s = s.regex(field.pattern);
|
|
4157
|
+
if (field.min !== void 0) s = s.min(field.min);
|
|
4158
|
+
if (field.max !== void 0) s = s.max(field.max);
|
|
4159
|
+
return narrowEnum2(s, field.enum);
|
|
4160
|
+
}
|
|
4161
|
+
case "string[]": {
|
|
4162
|
+
let arr = z.array(z.string());
|
|
4163
|
+
if (field.min !== void 0) arr = arr.min(field.min);
|
|
4164
|
+
if (field.max !== void 0) arr = arr.max(field.max);
|
|
4165
|
+
return arr;
|
|
4166
|
+
}
|
|
4167
|
+
case "number": {
|
|
4168
|
+
let n = z.number();
|
|
4169
|
+
if (field.min !== void 0) n = n.min(field.min);
|
|
4170
|
+
if (field.max !== void 0) n = n.max(field.max);
|
|
4171
|
+
return n;
|
|
4172
|
+
}
|
|
4173
|
+
case "boolean":
|
|
4174
|
+
return z.boolean();
|
|
4175
|
+
case "datetime":
|
|
4176
|
+
return z.union([z.date(), z.string().datetime()]);
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
function narrowEnum2(base, enumValues) {
|
|
4180
|
+
if (!enumValues || enumValues.length === 0) return base;
|
|
4181
|
+
return base.refine((v) => enumValues.includes(v), { message: "value not in enum" });
|
|
4182
|
+
}
|
|
3779
4183
|
|
|
3780
4184
|
// src/runtime/process.ts
|
|
3781
4185
|
function createProcessRuntime(def) {
|
|
3782
|
-
{
|
|
4186
|
+
if (!def) {
|
|
3783
4187
|
return {
|
|
3784
4188
|
augmentPrompt: (p) => p,
|
|
3785
4189
|
ingest: async () => {
|
|
@@ -3795,6 +4199,278 @@ function createProcessRuntime(def) {
|
|
|
3795
4199
|
}
|
|
3796
4200
|
};
|
|
3797
4201
|
}
|
|
4202
|
+
const primitive = new ProcessSchemaPrimitive();
|
|
4203
|
+
const schema = buildProcessSchema(def);
|
|
4204
|
+
void primitive.loadSchema(schema);
|
|
4205
|
+
let completed = false;
|
|
4206
|
+
const getOpenAi = () => defaultOpenAI();
|
|
4207
|
+
return {
|
|
4208
|
+
augmentPrompt(basePrompt) {
|
|
4209
|
+
const lines = Object.entries(def.collect).map(([name, field]) => {
|
|
4210
|
+
const parts = [`type=${field.type}`];
|
|
4211
|
+
if (field.required) parts.push("required");
|
|
4212
|
+
if (field.enum) parts.push(`one of: ${field.enum.join("|")}`);
|
|
4213
|
+
const ask = field.ask ?? `the ${name}`;
|
|
4214
|
+
return `- ${name} (${parts.join(", ")}) \u2014 ask: "${ask}"`;
|
|
4215
|
+
});
|
|
4216
|
+
return [
|
|
4217
|
+
basePrompt,
|
|
4218
|
+
"",
|
|
4219
|
+
"You are collecting the following structured information from the caller:",
|
|
4220
|
+
...lines,
|
|
4221
|
+
"",
|
|
4222
|
+
"Ask for ONE missing field at a time. Acknowledge captured values. Do not invent values."
|
|
4223
|
+
].join("\n");
|
|
4224
|
+
},
|
|
4225
|
+
async ingest(utterance, ctx) {
|
|
4226
|
+
if (completed) return;
|
|
4227
|
+
const status = primitive.getFieldStatus();
|
|
4228
|
+
const remaining = Object.entries(def.collect).filter(([name]) => {
|
|
4229
|
+
const s = status.find((f) => f.name === name);
|
|
4230
|
+
return !s || !s.captured;
|
|
4231
|
+
});
|
|
4232
|
+
if (remaining.length > 0) {
|
|
4233
|
+
let extractions;
|
|
4234
|
+
try {
|
|
4235
|
+
extractions = await extractFields(getOpenAi(), utterance, remaining);
|
|
4236
|
+
} catch (err) {
|
|
4237
|
+
console.warn("[process] extractor failed", err);
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
for (const { name, value } of extractions) {
|
|
4241
|
+
const field = def.collect[name];
|
|
4242
|
+
if (!field) continue;
|
|
4243
|
+
const coerced = coerce(value, field);
|
|
4244
|
+
if (coerced === INVALID) {
|
|
4245
|
+
console.debug("[process] rejected capture (coerce failed)", { name, value });
|
|
4246
|
+
continue;
|
|
4247
|
+
}
|
|
4248
|
+
const result = primitive.captureField({ name, value: coerced });
|
|
4249
|
+
if (!result.ok) {
|
|
4250
|
+
console.debug("[process] primitive rejected capture", { name, reason: result.reason });
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
if (def.onField) {
|
|
4254
|
+
try {
|
|
4255
|
+
await def.onField(
|
|
4256
|
+
name,
|
|
4257
|
+
coerced,
|
|
4258
|
+
ctx
|
|
4259
|
+
);
|
|
4260
|
+
} catch (err) {
|
|
4261
|
+
console.error("[process] onField threw", err);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
await maybeComplete(def, primitive, ctx, () => {
|
|
4267
|
+
completed = true;
|
|
4268
|
+
});
|
|
4269
|
+
},
|
|
4270
|
+
getData() {
|
|
4271
|
+
const out = {};
|
|
4272
|
+
for (const f of primitive.getFieldStatus()) {
|
|
4273
|
+
if (f.captured) out[f.name] = f.value;
|
|
4274
|
+
}
|
|
4275
|
+
return out;
|
|
4276
|
+
},
|
|
4277
|
+
isComplete() {
|
|
4278
|
+
return completed;
|
|
4279
|
+
},
|
|
4280
|
+
captureField(name, value) {
|
|
4281
|
+
const field = def.collect[name];
|
|
4282
|
+
if (!field) return false;
|
|
4283
|
+
const coerced = coerce(value, field);
|
|
4284
|
+
if (coerced === INVALID) return false;
|
|
4285
|
+
const result = primitive.captureField({ name, value: coerced });
|
|
4286
|
+
return result.ok;
|
|
4287
|
+
},
|
|
4288
|
+
recordEvent({ kind, source, data }) {
|
|
4289
|
+
primitive.emitLifecycle({
|
|
4290
|
+
id: randomUUID(),
|
|
4291
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4292
|
+
kind,
|
|
4293
|
+
source: source ?? "agent",
|
|
4294
|
+
data: data ?? {}
|
|
4295
|
+
});
|
|
4296
|
+
},
|
|
4297
|
+
getSnapshot() {
|
|
4298
|
+
return primitive.exportSnapshot();
|
|
4299
|
+
},
|
|
4300
|
+
getSchema() {
|
|
4301
|
+
return schema;
|
|
4302
|
+
},
|
|
4303
|
+
subscribe(listener) {
|
|
4304
|
+
return primitive.onSnapshot(listener);
|
|
4305
|
+
}
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
var INVALID = /* @__PURE__ */ Symbol("invalid");
|
|
4309
|
+
function coerce(value, field) {
|
|
4310
|
+
switch (field.type) {
|
|
4311
|
+
case "string":
|
|
4312
|
+
case "email":
|
|
4313
|
+
case "phone":
|
|
4314
|
+
return typeof value === "string" ? value : INVALID;
|
|
4315
|
+
case "string[]":
|
|
4316
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
|
|
4317
|
+
if (typeof value === "string") return [value];
|
|
4318
|
+
return INVALID;
|
|
4319
|
+
case "number": {
|
|
4320
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4321
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
4322
|
+
const n = Number(value);
|
|
4323
|
+
if (Number.isFinite(n)) return n;
|
|
4324
|
+
}
|
|
4325
|
+
return INVALID;
|
|
4326
|
+
}
|
|
4327
|
+
case "boolean":
|
|
4328
|
+
if (typeof value === "boolean") return value;
|
|
4329
|
+
if (typeof value === "string") {
|
|
4330
|
+
const v = value.toLowerCase();
|
|
4331
|
+
if (["true", "yes", "y"].includes(v)) return true;
|
|
4332
|
+
if (["false", "no", "n"].includes(v)) return false;
|
|
4333
|
+
}
|
|
4334
|
+
return INVALID;
|
|
4335
|
+
case "datetime":
|
|
4336
|
+
if (value instanceof Date) return value;
|
|
4337
|
+
if (typeof value === "string") {
|
|
4338
|
+
const d = new Date(value);
|
|
4339
|
+
if (!Number.isNaN(d.getTime())) return d;
|
|
4340
|
+
}
|
|
4341
|
+
return INVALID;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
async function maybeComplete(def, primitive, ctx, markComplete) {
|
|
4345
|
+
const data = {};
|
|
4346
|
+
for (const f of primitive.getFieldStatus()) if (f.captured) data[f.name] = f.value;
|
|
4347
|
+
const strategyOk = await checkStrategy(def, data, ctx);
|
|
4348
|
+
if (!strategyOk) return;
|
|
4349
|
+
const missingRequired = primitive.getFieldStatus().filter((f) => f.required && !f.captured);
|
|
4350
|
+
if (missingRequired.length > 0) return;
|
|
4351
|
+
if (def.backendAck && !primitive.exportSnapshot().backendAckReceived) {
|
|
4352
|
+
try {
|
|
4353
|
+
const res = await fetchWithTimeout(
|
|
4354
|
+
def.backendAck.url,
|
|
4355
|
+
{
|
|
4356
|
+
method: "POST",
|
|
4357
|
+
headers: { "content-type": "application/json", ...def.backendAck.headers ?? {} },
|
|
4358
|
+
body: JSON.stringify(data)
|
|
4359
|
+
},
|
|
4360
|
+
def.backendAck.timeoutMs ?? 3e4
|
|
4361
|
+
);
|
|
4362
|
+
if (!res.ok) {
|
|
4363
|
+
console.warn("[process] backend ACK non-2xx; not completing", res.status);
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
primitive.recordBackendAck(/* @__PURE__ */ new Date());
|
|
4367
|
+
} catch (err) {
|
|
4368
|
+
console.warn("[process] backend ACK failed; not completing", err);
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
const finalGate = primitive.canCallEnd();
|
|
4373
|
+
if (!finalGate.ok) return;
|
|
4374
|
+
markComplete();
|
|
4375
|
+
if (def.onComplete) {
|
|
4376
|
+
try {
|
|
4377
|
+
await def.onComplete(data, ctx);
|
|
4378
|
+
} catch (err) {
|
|
4379
|
+
console.error("[process] onComplete threw", err);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
async function checkStrategy(def, data, ctx) {
|
|
4384
|
+
const strategy = def.completeWhen ?? "all-required-captured";
|
|
4385
|
+
if (strategy === "all-required-captured") {
|
|
4386
|
+
return Object.entries(def.collect).every(([name, f]) => !f.required || data[name] !== void 0);
|
|
4387
|
+
}
|
|
4388
|
+
if (strategy === "all-captured") {
|
|
4389
|
+
return Object.keys(def.collect).every((name) => data[name] !== void 0);
|
|
4390
|
+
}
|
|
4391
|
+
if (typeof strategy === "object" && "custom" in strategy) {
|
|
4392
|
+
return Promise.resolve(strategy.custom(data, ctx));
|
|
4393
|
+
}
|
|
4394
|
+
return false;
|
|
4395
|
+
}
|
|
4396
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
4397
|
+
const ctrl = new AbortController();
|
|
4398
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
4399
|
+
try {
|
|
4400
|
+
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
4401
|
+
} finally {
|
|
4402
|
+
clearTimeout(t);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
async function extractFields(client, utterance, remaining) {
|
|
4406
|
+
const fieldLines = remaining.map(([name, f]) => {
|
|
4407
|
+
const parts = [`type=${f.type}`];
|
|
4408
|
+
if (f.pattern) parts.push(`pattern=${f.pattern.source}`);
|
|
4409
|
+
if (f.enum) parts.push(`enum=${JSON.stringify(f.enum)}`);
|
|
4410
|
+
if (f.extract) parts.push(`hint=${f.extract}`);
|
|
4411
|
+
return `- ${name} (${parts.join(", ")})${f.required ? " [required]" : ""}`;
|
|
4412
|
+
});
|
|
4413
|
+
const prompt = [
|
|
4414
|
+
"Schema fields still to capture:",
|
|
4415
|
+
...fieldLines,
|
|
4416
|
+
"",
|
|
4417
|
+
`Only emit values for: ${remaining.map(([n]) => n).join(", ")}`,
|
|
4418
|
+
"",
|
|
4419
|
+
`Latest caller utterance:
|
|
4420
|
+
"""
|
|
4421
|
+
${utterance}
|
|
4422
|
+
"""`,
|
|
4423
|
+
"",
|
|
4424
|
+
'Respond as JSON: {"fields": [{"name": "<field>", "value": <value>}, ...]}.',
|
|
4425
|
+
"Omit any field you are not confident about. Empty array if nothing to extract.",
|
|
4426
|
+
"For datetime fields use ISO 8601. For arrays return arrays. For booleans return true/false."
|
|
4427
|
+
].join("\n");
|
|
4428
|
+
const ctrl = new AbortController();
|
|
4429
|
+
const timer = setTimeout(() => ctrl.abort(), 4e3);
|
|
4430
|
+
try {
|
|
4431
|
+
const completion = await client.chat.completions.create(
|
|
4432
|
+
{
|
|
4433
|
+
model: process.env["VOICELAYER_EXTRACTOR_MODEL"] ?? "gpt-4o-mini",
|
|
4434
|
+
response_format: { type: "json_object" },
|
|
4435
|
+
temperature: 0,
|
|
4436
|
+
messages: [
|
|
4437
|
+
{
|
|
4438
|
+
role: "system",
|
|
4439
|
+
content: "You extract structured fields from a single caller utterance for a voice agent. Only emit fields you are confident about based on the utterance. Never invent values."
|
|
4440
|
+
},
|
|
4441
|
+
{ role: "user", content: prompt }
|
|
4442
|
+
]
|
|
4443
|
+
},
|
|
4444
|
+
{ signal: ctrl.signal }
|
|
4445
|
+
);
|
|
4446
|
+
const raw = completion.choices[0]?.message?.content ?? "{}";
|
|
4447
|
+
return parseExtractorResponse(raw, new Set(remaining.map(([n]) => n)));
|
|
4448
|
+
} finally {
|
|
4449
|
+
clearTimeout(timer);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
function parseExtractorResponse(raw, allowed) {
|
|
4453
|
+
let parsed;
|
|
4454
|
+
try {
|
|
4455
|
+
parsed = JSON.parse(raw);
|
|
4456
|
+
} catch {
|
|
4457
|
+
return [];
|
|
4458
|
+
}
|
|
4459
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
4460
|
+
const fields = parsed.fields;
|
|
4461
|
+
if (!Array.isArray(fields)) return [];
|
|
4462
|
+
const out = [];
|
|
4463
|
+
for (const item of fields) {
|
|
4464
|
+
if (!item || typeof item !== "object") continue;
|
|
4465
|
+
const rawName = item.name;
|
|
4466
|
+
const value = item.value;
|
|
4467
|
+
if (typeof rawName !== "string") continue;
|
|
4468
|
+
const name = allowed.has(rawName) ? rawName : [...allowed].find((a) => a.trim() === rawName.trim());
|
|
4469
|
+
if (name === void 0) continue;
|
|
4470
|
+
if (value === null || value === void 0) continue;
|
|
4471
|
+
out.push({ name, value });
|
|
4472
|
+
}
|
|
4473
|
+
return out;
|
|
3798
4474
|
}
|
|
3799
4475
|
|
|
3800
4476
|
// src/runtime/session-adapter.ts
|
|
@@ -4040,6 +4716,29 @@ function createTextSessionAdapter(transport) {
|
|
|
4040
4716
|
return { adapter, push, setState, close };
|
|
4041
4717
|
}
|
|
4042
4718
|
|
|
4719
|
+
// src/flow-runtime.ts
|
|
4720
|
+
init_define_tool();
|
|
4721
|
+
function processSchemaToDefinition(schema) {
|
|
4722
|
+
const required = new Set(schema.completionGate.requiredFields);
|
|
4723
|
+
const collect = {};
|
|
4724
|
+
for (const f of schema.fields) {
|
|
4725
|
+
collect[f.name] = {
|
|
4726
|
+
type: f.type,
|
|
4727
|
+
required: required.has(f.name) || f.required,
|
|
4728
|
+
...f.ask !== void 0 ? { ask: f.ask } : {},
|
|
4729
|
+
...f.pattern !== void 0 ? { pattern: new RegExp(f.pattern) } : {},
|
|
4730
|
+
...f.min !== void 0 ? { min: f.min } : {},
|
|
4731
|
+
...f.max !== void 0 ? { max: f.max } : {},
|
|
4732
|
+
...f.enum !== void 0 ? { enum: f.enum } : {}
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
return {
|
|
4736
|
+
collect,
|
|
4737
|
+
completeWhen: "all-required-captured",
|
|
4738
|
+
...schema.completionGate.backendAck !== void 0 ? { backendAck: schema.completionGate.backendAck } : {}
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4043
4742
|
// src/runtime/text-session.ts
|
|
4044
4743
|
function buildTextContext(adapter, call, processRt) {
|
|
4045
4744
|
return {
|
|
@@ -4062,7 +4761,7 @@ function buildTextContext(adapter, call, processRt) {
|
|
|
4062
4761
|
};
|
|
4063
4762
|
}
|
|
4064
4763
|
function runTextSession(opts) {
|
|
4065
|
-
const processRt = opts.processRt ?? createProcessRuntime();
|
|
4764
|
+
const processRt = opts.processRt ?? createProcessRuntime(void 0);
|
|
4066
4765
|
const handle = createTextSessionAdapter({
|
|
4067
4766
|
send: opts.onAgentText,
|
|
4068
4767
|
...opts.generate ? { generate: opts.generate } : {}
|
|
@@ -4098,14 +4797,51 @@ function runTextSession(opts) {
|
|
|
4098
4797
|
end: () => handle.close()
|
|
4099
4798
|
};
|
|
4100
4799
|
}
|
|
4800
|
+
function deriveCollectFromProgram(program) {
|
|
4801
|
+
const collect = {};
|
|
4802
|
+
for (const [nodeId, node] of Object.entries(program.nodes)) {
|
|
4803
|
+
const cfg = node.config;
|
|
4804
|
+
if (node.type === "ask") {
|
|
4805
|
+
const name = typeof cfg.slot === "string" && cfg.slot ? cfg.slot : nodeId;
|
|
4806
|
+
const type = cfg.type;
|
|
4807
|
+
collect[name] = {
|
|
4808
|
+
type: type === "number" || type === "boolean" || type === "datetime" || type === "string[]" ? type : "string",
|
|
4809
|
+
...cfg.required !== false ? { required: true } : {},
|
|
4810
|
+
...typeof cfg.ask === "string" ? { ask: cfg.ask } : {},
|
|
4811
|
+
...typeof cfg.pattern === "string" ? { pattern: new RegExp(cfg.pattern) } : {}
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
if (cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
4815
|
+
for (const slot of Object.keys(cfg.outputAs)) {
|
|
4816
|
+
collect[slot] ??= { type: "string" };
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
if (Array.isArray(cfg.assignments)) {
|
|
4820
|
+
for (const a of cfg.assignments) {
|
|
4821
|
+
if (a && typeof a.name === "string" && a.name.trim()) collect[a.name.trim()] ??= { type: "string" };
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
return { collect, completeWhen: "all-required-captured" };
|
|
4826
|
+
}
|
|
4827
|
+
function definitionFor(opts) {
|
|
4828
|
+
if (opts.schema) {
|
|
4829
|
+
try {
|
|
4830
|
+
return processSchemaToDefinition(opts.schema);
|
|
4831
|
+
} catch {
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
return deriveCollectFromProgram(opts.program);
|
|
4835
|
+
}
|
|
4101
4836
|
async function runTextTranscript(opts) {
|
|
4102
4837
|
const replies = [];
|
|
4103
4838
|
const trace = [];
|
|
4839
|
+
const processRt = opts.processRt ?? createProcessRuntime(definitionFor(opts));
|
|
4104
4840
|
const session = runTextSession({
|
|
4105
4841
|
program: opts.program,
|
|
4106
4842
|
onAgentText: (t) => void replies.push(t),
|
|
4107
4843
|
onTrace: (e) => void trace.push(e),
|
|
4108
|
-
...
|
|
4844
|
+
...processRt ? { processRt } : {},
|
|
4109
4845
|
...opts.generate ? { generate: opts.generate } : {},
|
|
4110
4846
|
...opts.call ? { call: opts.call } : {},
|
|
4111
4847
|
...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {}
|