@voicelayer/sdk 0.1.8 → 0.1.10
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 +34 -1184
- package/dist/index.js +209 -131
- package/dist/runtime/text-session.d.ts +1 -1
- package/dist/runtime/text-session.js +730 -16
- package/dist/text-session-D1j8nQac.d.ts +1996 -0
- package/package.json +4 -4
- package/dist/text-session-BoUmDEX9.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;
|
|
@@ -2621,7 +2664,7 @@ var init_executors = __esm({
|
|
|
2621
2664
|
const authoredSlot = slot === node.id ? "" : slot.trim();
|
|
2622
2665
|
const label = authoredSlot.replace(/[_-]+/g, " ");
|
|
2623
2666
|
if (hasSlot(deps.processRt.getData(), slot)) {
|
|
2624
|
-
|
|
2667
|
+
deps.events.note("ask.skipped", { slot: slot.trim(), reason: "already known" });
|
|
2625
2668
|
return { kind: "advance" };
|
|
2626
2669
|
}
|
|
2627
2670
|
const askText = interpolate(
|
|
@@ -2635,10 +2678,10 @@ var init_executors = __esm({
|
|
|
2635
2678
|
state.lastUtterance = t.text;
|
|
2636
2679
|
state.lastDtmf = t.dtmf;
|
|
2637
2680
|
if (hasSlot(deps.processRt.getData(), slot)) {
|
|
2638
|
-
|
|
2681
|
+
deps.events.note("ask.captured", { slot: slot.trim() });
|
|
2639
2682
|
return { kind: "advance" };
|
|
2640
2683
|
}
|
|
2641
|
-
|
|
2684
|
+
deps.events.note("ask.clarify", { slot: slot.trim(), turn });
|
|
2642
2685
|
await withHistory(
|
|
2643
2686
|
deps,
|
|
2644
2687
|
historyPolicyOf(node),
|
|
@@ -2739,11 +2782,18 @@ ${collected}`);
|
|
|
2739
2782
|
if (ok && cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
2740
2783
|
for (const [slot, path] of Object.entries(cfg.outputAs)) {
|
|
2741
2784
|
if (typeof path !== "string" || !path.trim()) continue;
|
|
2742
|
-
const
|
|
2743
|
-
if (
|
|
2744
|
-
deps.
|
|
2745
|
-
|
|
2785
|
+
const raw = resolvePath(result, path.trim());
|
|
2786
|
+
if (raw === void 0 || raw === null) {
|
|
2787
|
+
deps.events.note("tool.output_missing", { node: node.id, slot, path });
|
|
2788
|
+
continue;
|
|
2746
2789
|
}
|
|
2790
|
+
const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" ? String(raw) : JSON.stringify(raw);
|
|
2791
|
+
const captured = deps.processRt.captureField(slot, value);
|
|
2792
|
+
deps.events.note(captured ? "tool.output_captured" : "tool.output_rejected", {
|
|
2793
|
+
node: node.id,
|
|
2794
|
+
slot,
|
|
2795
|
+
...captured ? {} : { reason: "slot not declared or value not coercible" }
|
|
2796
|
+
});
|
|
2747
2797
|
}
|
|
2748
2798
|
}
|
|
2749
2799
|
return routeByPort(node, deps, ok ? "ok" : "fail");
|
|
@@ -3252,13 +3302,19 @@ ${collected}`);
|
|
|
3252
3302
|
setExecutor = async (node, state, deps) => {
|
|
3253
3303
|
const assignments = Array.isArray(node.config.assignments) ? node.config.assignments : [];
|
|
3254
3304
|
let applied = 0;
|
|
3305
|
+
const rejected = [];
|
|
3255
3306
|
for (const a of assignments) {
|
|
3256
3307
|
if (!a || typeof a.name !== "string" || !a.name.trim()) continue;
|
|
3308
|
+
const name = a.name.trim();
|
|
3257
3309
|
const value = interpolate(typeof a.value === "string" ? a.value : "", scopeFor(state, deps));
|
|
3258
|
-
deps.processRt.captureField(
|
|
3259
|
-
|
|
3310
|
+
if (deps.processRt.captureField(name, value)) applied += 1;
|
|
3311
|
+
else rejected.push(name);
|
|
3260
3312
|
}
|
|
3261
|
-
deps.events.note("set.applied", {
|
|
3313
|
+
deps.events.note("set.applied", {
|
|
3314
|
+
node: node.id,
|
|
3315
|
+
count: applied,
|
|
3316
|
+
...rejected.length ? { rejected } : {}
|
|
3317
|
+
});
|
|
3262
3318
|
return { kind: "advance" };
|
|
3263
3319
|
};
|
|
3264
3320
|
MAX_SUBFLOW_DEPTH = 3;
|
|
@@ -3716,7 +3772,54 @@ init_openai_default();
|
|
|
3716
3772
|
// ../primitives/process-schema/src/index.ts
|
|
3717
3773
|
init_src();
|
|
3718
3774
|
var PROCESS_LIFECYCLE_MAX_EVENTS = 100;
|
|
3719
|
-
|
|
3775
|
+
function isZodSchema(v) {
|
|
3776
|
+
return typeof v === "object" && v !== null && "_def" in v && "parse" in v;
|
|
3777
|
+
}
|
|
3778
|
+
function isPlainObject(value) {
|
|
3779
|
+
if (value === null || typeof value !== "object") return false;
|
|
3780
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
3781
|
+
}
|
|
3782
|
+
function compileFieldValidator(field) {
|
|
3783
|
+
if (isZodSchema(field.validation)) return field.validation;
|
|
3784
|
+
return legacySpecToZod(isPlainObject(field.validation) ? field.validation : {});
|
|
3785
|
+
}
|
|
3786
|
+
function legacySpecToZod(spec) {
|
|
3787
|
+
switch (spec.type) {
|
|
3788
|
+
case "number": {
|
|
3789
|
+
let s = z.number();
|
|
3790
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3791
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3792
|
+
return narrowEnum(s, spec.enum);
|
|
3793
|
+
}
|
|
3794
|
+
case "boolean":
|
|
3795
|
+
return narrowEnum(z.boolean(), spec.enum);
|
|
3796
|
+
case "datetime":
|
|
3797
|
+
return z.string().datetime();
|
|
3798
|
+
case "array": {
|
|
3799
|
+
let s = z.array(z.unknown());
|
|
3800
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3801
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3802
|
+
return s;
|
|
3803
|
+
}
|
|
3804
|
+
case "object":
|
|
3805
|
+
return z.record(z.string(), z.unknown());
|
|
3806
|
+
case "string":
|
|
3807
|
+
default: {
|
|
3808
|
+
let s = z.string();
|
|
3809
|
+
if (spec.min !== void 0) s = s.min(spec.min);
|
|
3810
|
+
if (spec.max !== void 0) s = s.max(spec.max);
|
|
3811
|
+
if (spec.pattern !== void 0) s = s.regex(new RegExp(spec.pattern));
|
|
3812
|
+
return narrowEnum(s, spec.enum);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
function narrowEnum(base, enumValues) {
|
|
3817
|
+
if (!enumValues || enumValues.length === 0) return base;
|
|
3818
|
+
return base.refine((v) => enumValues.some((c) => Object.is(c, v)), {
|
|
3819
|
+
message: "value not in enum"
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
var machine = setup({
|
|
3720
3823
|
types: {},
|
|
3721
3824
|
guards: {
|
|
3722
3825
|
allRequiredCaptured: ({ context }) => context.requiredFields.every((n) => context.fieldsCaptured.has(n)),
|
|
@@ -3811,10 +3914,277 @@ setup({
|
|
|
3811
3914
|
}
|
|
3812
3915
|
}
|
|
3813
3916
|
});
|
|
3917
|
+
var ProcessSchemaPrimitive = class extends BasePrimitive {
|
|
3918
|
+
primitiveId = PrimitiveId("process-schema");
|
|
3919
|
+
version = SemanticVersion("0.2.0");
|
|
3920
|
+
schema = null;
|
|
3921
|
+
compiledFields = /* @__PURE__ */ new Map();
|
|
3922
|
+
actor;
|
|
3923
|
+
lastSnapshot = null;
|
|
3924
|
+
lastBlockedReason;
|
|
3925
|
+
constructor() {
|
|
3926
|
+
super();
|
|
3927
|
+
this.actor = createActor(machine);
|
|
3928
|
+
this.actor.start();
|
|
3929
|
+
}
|
|
3930
|
+
async loadSchema(schema) {
|
|
3931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3932
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
3933
|
+
for (const field of schema.fields) {
|
|
3934
|
+
if (seen.has(field.name)) throw new Error(`duplicate process field: ${field.name}`);
|
|
3935
|
+
seen.add(field.name);
|
|
3936
|
+
compiled.set(field.name, { ...field, validator: compileFieldValidator(field) });
|
|
3937
|
+
}
|
|
3938
|
+
for (const required of schema.completionGate.requiredFields) {
|
|
3939
|
+
if (!seen.has(required)) {
|
|
3940
|
+
throw new Error(`completion gate references unknown field: ${required}`);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
this.schema = schema;
|
|
3944
|
+
this.compiledFields = compiled;
|
|
3945
|
+
this.lastBlockedReason = void 0;
|
|
3946
|
+
this.actor.send({
|
|
3947
|
+
type: "LOAD",
|
|
3948
|
+
requiredFields: schema.completionGate.requiredFields,
|
|
3949
|
+
requiresAck: schema.completionGate.backendAck !== void 0
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
async onCallStart(_ctx) {
|
|
3953
|
+
if (this.schema === null) return;
|
|
3954
|
+
this.lastBlockedReason = void 0;
|
|
3955
|
+
this.actor.send({
|
|
3956
|
+
type: "LOAD",
|
|
3957
|
+
requiredFields: this.schema.completionGate.requiredFields,
|
|
3958
|
+
requiresAck: this.schema.completionGate.backendAck !== void 0
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
async onCallEnd(_ctx, outcome) {
|
|
3962
|
+
if (this.schema === null) return;
|
|
3963
|
+
this.lastSnapshot = this.exportSnapshot();
|
|
3964
|
+
if (outcome.processCompleted) {
|
|
3965
|
+
this.actor.send({ type: "COMPLETE" });
|
|
3966
|
+
} else {
|
|
3967
|
+
const gate = this.canCallEnd();
|
|
3968
|
+
if (!gate.ok) this.lastBlockedReason = gate.reason;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
getCurrentState() {
|
|
3972
|
+
const snap = this.actor.getSnapshot();
|
|
3973
|
+
const ctx = snap.context;
|
|
3974
|
+
const blockedReason = this.computeBlockedReason(snap);
|
|
3975
|
+
return {
|
|
3976
|
+
currentNode: this.currentNodeName(snap),
|
|
3977
|
+
fieldsCaptured: new Map(ctx.fieldsCaptured),
|
|
3978
|
+
backendAckReceived: ctx.backendAckReceived,
|
|
3979
|
+
...blockedReason === void 0 ? {} : { blockedReason },
|
|
3980
|
+
...ctx.backendAckReceivedAt === void 0 ? {} : { backendAckReceivedAt: ctx.backendAckReceivedAt }
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
getFieldStatus() {
|
|
3984
|
+
if (this.schema === null) return [];
|
|
3985
|
+
const ctx = this.actor.getSnapshot().context;
|
|
3986
|
+
return this.schema.fields.map((field) => {
|
|
3987
|
+
const captured = ctx.fieldsCaptured.has(field.name);
|
|
3988
|
+
const value = ctx.fieldsCaptured.get(field.name);
|
|
3989
|
+
const validator = this.compiledFields.get(field.name)?.validator;
|
|
3990
|
+
const valid = captured && validator ? validator.safeParse(value).success : !captured;
|
|
3991
|
+
return { name: field.name, captured, value, required: field.required, valid };
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
getCompletionPercentage() {
|
|
3995
|
+
if (this.schema === null) return 0;
|
|
3996
|
+
const requiredCount = this.schema.completionGate.requiredFields.length;
|
|
3997
|
+
const requiresAck = this.schema.completionGate.backendAck !== void 0;
|
|
3998
|
+
const denom = requiredCount + (requiresAck ? 1 : 0);
|
|
3999
|
+
if (denom === 0) return 100;
|
|
4000
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4001
|
+
const capturedRequired = this.schema.completionGate.requiredFields.filter(
|
|
4002
|
+
(n) => ctx.fieldsCaptured.has(n)
|
|
4003
|
+
).length;
|
|
4004
|
+
const ackCount = ctx.backendAckReceived ? 1 : 0;
|
|
4005
|
+
return Math.round((capturedRequired + ackCount) / denom * 100);
|
|
4006
|
+
}
|
|
4007
|
+
canCallEnd() {
|
|
4008
|
+
if (this.schema === null) return { ok: false, reason: "schema_not_loaded", missingFields: [] };
|
|
4009
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4010
|
+
const missingFields = this.schema.completionGate.requiredFields.filter(
|
|
4011
|
+
(n) => !ctx.fieldsCaptured.has(n)
|
|
4012
|
+
);
|
|
4013
|
+
if (missingFields.length > 0) {
|
|
4014
|
+
return {
|
|
4015
|
+
ok: false,
|
|
4016
|
+
reason: `${missingFields.length} required field(s) missing`,
|
|
4017
|
+
missingFields
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
if (this.schema.completionGate.backendAck !== void 0 && !ctx.backendAckReceived) {
|
|
4021
|
+
return { ok: false, reason: "backend acknowledgement pending", missingFields: [] };
|
|
4022
|
+
}
|
|
4023
|
+
return { ok: true };
|
|
4024
|
+
}
|
|
4025
|
+
captureField(f) {
|
|
4026
|
+
if (this.schema === null) return { ok: false, reason: "schema_not_loaded" };
|
|
4027
|
+
const field = this.compiledFields.get(f.name);
|
|
4028
|
+
if (field === void 0) return { ok: false, reason: `unknown field: ${f.name}` };
|
|
4029
|
+
const parsed = field.validator.safeParse(f.value);
|
|
4030
|
+
if (!parsed.success) {
|
|
4031
|
+
const reason = parsed.error.issues[0]?.message ?? "invalid value";
|
|
4032
|
+
this.lastBlockedReason = `${f.name}: ${reason}`;
|
|
4033
|
+
return { ok: false, reason };
|
|
4034
|
+
}
|
|
4035
|
+
const before = this.actor.getSnapshot().context.fieldsCaptured.get(f.name);
|
|
4036
|
+
const clonedValue = structuredClone(parsed.data);
|
|
4037
|
+
this.actor.send({ type: "CAPTURE", name: f.name, value: clonedValue });
|
|
4038
|
+
this.lastBlockedReason = void 0;
|
|
4039
|
+
return { ok: true, advanced: !Object.is(before, parsed.data) };
|
|
4040
|
+
}
|
|
4041
|
+
recordBackendAck(receivedAt = /* @__PURE__ */ new Date()) {
|
|
4042
|
+
this.actor.send({ type: "ACK", receivedAt });
|
|
4043
|
+
}
|
|
4044
|
+
exportSnapshot() {
|
|
4045
|
+
const ctx = this.actor.getSnapshot().context;
|
|
4046
|
+
return {
|
|
4047
|
+
schemaId: this.schema?.id ?? "unloaded",
|
|
4048
|
+
currentNode: this.currentNodeName(this.actor.getSnapshot()),
|
|
4049
|
+
fieldsCaptured: Object.fromEntries(ctx.fieldsCaptured),
|
|
4050
|
+
backendAckReceived: ctx.backendAckReceived,
|
|
4051
|
+
...ctx.backendAckReceivedAt === void 0 ? {} : { backendAckReceivedAt: new Date(ctx.backendAckReceivedAt) },
|
|
4052
|
+
// Defensive copy — callers shouldn't be able to mutate the live array.
|
|
4053
|
+
lifecycle: ctx.lifecycle.slice()
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Record a side-channel lifecycle event. Does not advance the state machine.
|
|
4058
|
+
* Triggers the snapshot subscription so the dashboard sees the event in
|
|
4059
|
+
* real time alongside any state transitions.
|
|
4060
|
+
*/
|
|
4061
|
+
emitLifecycle(event) {
|
|
4062
|
+
this.actor.send({ type: "EMIT_LIFECYCLE", event });
|
|
4063
|
+
}
|
|
4064
|
+
hydrateSnapshot(snapshot) {
|
|
4065
|
+
if (this.schema === null) {
|
|
4066
|
+
throw new Error("cannot hydrate process state before schema is loaded");
|
|
4067
|
+
}
|
|
4068
|
+
if (snapshot.schemaId !== this.schema.id) {
|
|
4069
|
+
throw new Error(`snapshot schema mismatch: ${snapshot.schemaId} !== ${this.schema.id}`);
|
|
4070
|
+
}
|
|
4071
|
+
this.actor.send({
|
|
4072
|
+
type: "LOAD",
|
|
4073
|
+
requiredFields: this.schema.completionGate.requiredFields,
|
|
4074
|
+
requiresAck: this.schema.completionGate.backendAck !== void 0
|
|
4075
|
+
});
|
|
4076
|
+
for (const [name, value] of Object.entries(snapshot.fieldsCaptured)) {
|
|
4077
|
+
this.actor.send({ type: "CAPTURE", name, value });
|
|
4078
|
+
}
|
|
4079
|
+
if (snapshot.backendAckReceived) {
|
|
4080
|
+
this.actor.send({ type: "ACK", receivedAt: snapshot.backendAckReceivedAt ?? /* @__PURE__ */ new Date() });
|
|
4081
|
+
}
|
|
4082
|
+
for (const ev of snapshot.lifecycle) {
|
|
4083
|
+
this.actor.send({ type: "EMIT_LIFECYCLE", event: ev });
|
|
4084
|
+
}
|
|
4085
|
+
this.lastSnapshot = snapshot;
|
|
4086
|
+
}
|
|
4087
|
+
getLastSnapshot() {
|
|
4088
|
+
return this.lastSnapshot;
|
|
4089
|
+
}
|
|
4090
|
+
/** @internal — for XState devtools / Stately Inspector. */
|
|
4091
|
+
getActor() {
|
|
4092
|
+
return this.actor;
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Fire-and-forget subscription to state-machine transitions. Listener receives
|
|
4096
|
+
* a fresh `ProcessSnapshot` on every actor transition (capture, ack, gate).
|
|
4097
|
+
*
|
|
4098
|
+
* Hot-path contract: the listener is invoked synchronously inside the actor
|
|
4099
|
+
* subscription; callers MUST NOT await any I/O inside it. For external fan-out
|
|
4100
|
+
* (BullMQ, SSE), enqueue work without awaiting — `queue.add(...).catch(noop)`.
|
|
4101
|
+
*
|
|
4102
|
+
* Returns an unsubscribe function.
|
|
4103
|
+
*/
|
|
4104
|
+
onSnapshot(listener) {
|
|
4105
|
+
const sub = this.actor.subscribe(() => {
|
|
4106
|
+
try {
|
|
4107
|
+
listener(this.exportSnapshot());
|
|
4108
|
+
} catch (err) {
|
|
4109
|
+
console.error("[process-schema] subscriber threw", err);
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
return () => sub.unsubscribe();
|
|
4113
|
+
}
|
|
4114
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
4115
|
+
currentNodeName(snap) {
|
|
4116
|
+
const value = snap.value;
|
|
4117
|
+
return typeof value === "string" ? value : Object.keys(value)[0] ?? "unloaded";
|
|
4118
|
+
}
|
|
4119
|
+
computeBlockedReason(snap) {
|
|
4120
|
+
if (this.schema === null) return "schema_not_loaded";
|
|
4121
|
+
if (this.lastBlockedReason !== void 0) return this.lastBlockedReason;
|
|
4122
|
+
const ctx = snap.context;
|
|
4123
|
+
const missing = this.schema.completionGate.requiredFields.filter((n) => !ctx.fieldsCaptured.has(n));
|
|
4124
|
+
if (missing.length > 0) return `${missing.length} required field(s) missing`;
|
|
4125
|
+
if (this.schema.completionGate.backendAck !== void 0 && !ctx.backendAckReceived) {
|
|
4126
|
+
return "backend acknowledgement pending";
|
|
4127
|
+
}
|
|
4128
|
+
return void 0;
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
var DEFAULT_SCHEMA_ID = "process";
|
|
4132
|
+
function buildProcessSchema(def, schemaId = DEFAULT_SCHEMA_ID) {
|
|
4133
|
+
const fields = Object.entries(def.collect).map(([name, field]) => ({
|
|
4134
|
+
name,
|
|
4135
|
+
required: field.required === true,
|
|
4136
|
+
validation: fieldToZod(field)
|
|
4137
|
+
}));
|
|
4138
|
+
const requiredFields = Object.entries(def.collect).filter(([, f]) => f.required === true).map(([n]) => n);
|
|
4139
|
+
const gate = {
|
|
4140
|
+
requiredFields,
|
|
4141
|
+
...def.backendAck ? {
|
|
4142
|
+
backendAck: {
|
|
4143
|
+
url: def.backendAck.url,
|
|
4144
|
+
timeoutMs: def.backendAck.timeoutMs ?? 3e4
|
|
4145
|
+
}
|
|
4146
|
+
} : {}
|
|
4147
|
+
};
|
|
4148
|
+
return { id: schemaId, fields, completionGate: gate };
|
|
4149
|
+
}
|
|
4150
|
+
function fieldToZod(field) {
|
|
4151
|
+
switch (field.type) {
|
|
4152
|
+
case "string":
|
|
4153
|
+
case "email":
|
|
4154
|
+
case "phone": {
|
|
4155
|
+
let s = z.string();
|
|
4156
|
+
if (field.type === "email") s = s.email();
|
|
4157
|
+
if (field.pattern) s = s.regex(field.pattern);
|
|
4158
|
+
if (field.min !== void 0) s = s.min(field.min);
|
|
4159
|
+
if (field.max !== void 0) s = s.max(field.max);
|
|
4160
|
+
return narrowEnum2(s, field.enum);
|
|
4161
|
+
}
|
|
4162
|
+
case "string[]": {
|
|
4163
|
+
let arr = z.array(z.string());
|
|
4164
|
+
if (field.min !== void 0) arr = arr.min(field.min);
|
|
4165
|
+
if (field.max !== void 0) arr = arr.max(field.max);
|
|
4166
|
+
return arr;
|
|
4167
|
+
}
|
|
4168
|
+
case "number": {
|
|
4169
|
+
let n = z.number();
|
|
4170
|
+
if (field.min !== void 0) n = n.min(field.min);
|
|
4171
|
+
if (field.max !== void 0) n = n.max(field.max);
|
|
4172
|
+
return n;
|
|
4173
|
+
}
|
|
4174
|
+
case "boolean":
|
|
4175
|
+
return z.boolean();
|
|
4176
|
+
case "datetime":
|
|
4177
|
+
return z.union([z.date(), z.string().datetime()]);
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
function narrowEnum2(base, enumValues) {
|
|
4181
|
+
if (!enumValues || enumValues.length === 0) return base;
|
|
4182
|
+
return base.refine((v) => enumValues.includes(v), { message: "value not in enum" });
|
|
4183
|
+
}
|
|
3814
4184
|
|
|
3815
4185
|
// src/runtime/process.ts
|
|
3816
4186
|
function createProcessRuntime(def) {
|
|
3817
|
-
{
|
|
4187
|
+
if (!def) {
|
|
3818
4188
|
return {
|
|
3819
4189
|
augmentPrompt: (p) => p,
|
|
3820
4190
|
ingest: async () => {
|
|
@@ -3830,6 +4200,278 @@ function createProcessRuntime(def) {
|
|
|
3830
4200
|
}
|
|
3831
4201
|
};
|
|
3832
4202
|
}
|
|
4203
|
+
const primitive = new ProcessSchemaPrimitive();
|
|
4204
|
+
const schema = buildProcessSchema(def);
|
|
4205
|
+
void primitive.loadSchema(schema);
|
|
4206
|
+
let completed = false;
|
|
4207
|
+
const getOpenAi = () => defaultOpenAI();
|
|
4208
|
+
return {
|
|
4209
|
+
augmentPrompt(basePrompt) {
|
|
4210
|
+
const lines = Object.entries(def.collect).map(([name, field]) => {
|
|
4211
|
+
const parts = [`type=${field.type}`];
|
|
4212
|
+
if (field.required) parts.push("required");
|
|
4213
|
+
if (field.enum) parts.push(`one of: ${field.enum.join("|")}`);
|
|
4214
|
+
const ask = field.ask ?? `the ${name}`;
|
|
4215
|
+
return `- ${name} (${parts.join(", ")}) \u2014 ask: "${ask}"`;
|
|
4216
|
+
});
|
|
4217
|
+
return [
|
|
4218
|
+
basePrompt,
|
|
4219
|
+
"",
|
|
4220
|
+
"You are collecting the following structured information from the caller:",
|
|
4221
|
+
...lines,
|
|
4222
|
+
"",
|
|
4223
|
+
"Ask for ONE missing field at a time. Acknowledge captured values. Do not invent values."
|
|
4224
|
+
].join("\n");
|
|
4225
|
+
},
|
|
4226
|
+
async ingest(utterance, ctx) {
|
|
4227
|
+
if (completed) return;
|
|
4228
|
+
const status = primitive.getFieldStatus();
|
|
4229
|
+
const remaining = Object.entries(def.collect).filter(([name]) => {
|
|
4230
|
+
const s = status.find((f) => f.name === name);
|
|
4231
|
+
return !s || !s.captured;
|
|
4232
|
+
});
|
|
4233
|
+
if (remaining.length > 0) {
|
|
4234
|
+
let extractions;
|
|
4235
|
+
try {
|
|
4236
|
+
extractions = await extractFields(getOpenAi(), utterance, remaining);
|
|
4237
|
+
} catch (err) {
|
|
4238
|
+
console.warn("[process] extractor failed", err);
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
for (const { name, value } of extractions) {
|
|
4242
|
+
const field = def.collect[name];
|
|
4243
|
+
if (!field) continue;
|
|
4244
|
+
const coerced = coerce(value, field);
|
|
4245
|
+
if (coerced === INVALID) {
|
|
4246
|
+
console.debug("[process] rejected capture (coerce failed)", { name, value });
|
|
4247
|
+
continue;
|
|
4248
|
+
}
|
|
4249
|
+
const result = primitive.captureField({ name, value: coerced });
|
|
4250
|
+
if (!result.ok) {
|
|
4251
|
+
console.debug("[process] primitive rejected capture", { name, reason: result.reason });
|
|
4252
|
+
continue;
|
|
4253
|
+
}
|
|
4254
|
+
if (def.onField) {
|
|
4255
|
+
try {
|
|
4256
|
+
await def.onField(
|
|
4257
|
+
name,
|
|
4258
|
+
coerced,
|
|
4259
|
+
ctx
|
|
4260
|
+
);
|
|
4261
|
+
} catch (err) {
|
|
4262
|
+
console.error("[process] onField threw", err);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
await maybeComplete(def, primitive, ctx, () => {
|
|
4268
|
+
completed = true;
|
|
4269
|
+
});
|
|
4270
|
+
},
|
|
4271
|
+
getData() {
|
|
4272
|
+
const out = {};
|
|
4273
|
+
for (const f of primitive.getFieldStatus()) {
|
|
4274
|
+
if (f.captured) out[f.name] = f.value;
|
|
4275
|
+
}
|
|
4276
|
+
return out;
|
|
4277
|
+
},
|
|
4278
|
+
isComplete() {
|
|
4279
|
+
return completed;
|
|
4280
|
+
},
|
|
4281
|
+
captureField(name, value) {
|
|
4282
|
+
const field = def.collect[name];
|
|
4283
|
+
if (!field) return false;
|
|
4284
|
+
const coerced = coerce(value, field);
|
|
4285
|
+
if (coerced === INVALID) return false;
|
|
4286
|
+
const result = primitive.captureField({ name, value: coerced });
|
|
4287
|
+
return result.ok;
|
|
4288
|
+
},
|
|
4289
|
+
recordEvent({ kind, source, data }) {
|
|
4290
|
+
primitive.emitLifecycle({
|
|
4291
|
+
id: randomUUID(),
|
|
4292
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4293
|
+
kind,
|
|
4294
|
+
source: source ?? "agent",
|
|
4295
|
+
data: data ?? {}
|
|
4296
|
+
});
|
|
4297
|
+
},
|
|
4298
|
+
getSnapshot() {
|
|
4299
|
+
return primitive.exportSnapshot();
|
|
4300
|
+
},
|
|
4301
|
+
getSchema() {
|
|
4302
|
+
return schema;
|
|
4303
|
+
},
|
|
4304
|
+
subscribe(listener) {
|
|
4305
|
+
return primitive.onSnapshot(listener);
|
|
4306
|
+
}
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
var INVALID = /* @__PURE__ */ Symbol("invalid");
|
|
4310
|
+
function coerce(value, field) {
|
|
4311
|
+
switch (field.type) {
|
|
4312
|
+
case "string":
|
|
4313
|
+
case "email":
|
|
4314
|
+
case "phone":
|
|
4315
|
+
return typeof value === "string" ? value : INVALID;
|
|
4316
|
+
case "string[]":
|
|
4317
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
|
|
4318
|
+
if (typeof value === "string") return [value];
|
|
4319
|
+
return INVALID;
|
|
4320
|
+
case "number": {
|
|
4321
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4322
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
4323
|
+
const n = Number(value);
|
|
4324
|
+
if (Number.isFinite(n)) return n;
|
|
4325
|
+
}
|
|
4326
|
+
return INVALID;
|
|
4327
|
+
}
|
|
4328
|
+
case "boolean":
|
|
4329
|
+
if (typeof value === "boolean") return value;
|
|
4330
|
+
if (typeof value === "string") {
|
|
4331
|
+
const v = value.toLowerCase();
|
|
4332
|
+
if (["true", "yes", "y"].includes(v)) return true;
|
|
4333
|
+
if (["false", "no", "n"].includes(v)) return false;
|
|
4334
|
+
}
|
|
4335
|
+
return INVALID;
|
|
4336
|
+
case "datetime":
|
|
4337
|
+
if (value instanceof Date) return value;
|
|
4338
|
+
if (typeof value === "string") {
|
|
4339
|
+
const d = new Date(value);
|
|
4340
|
+
if (!Number.isNaN(d.getTime())) return d;
|
|
4341
|
+
}
|
|
4342
|
+
return INVALID;
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
async function maybeComplete(def, primitive, ctx, markComplete) {
|
|
4346
|
+
const data = {};
|
|
4347
|
+
for (const f of primitive.getFieldStatus()) if (f.captured) data[f.name] = f.value;
|
|
4348
|
+
const strategyOk = await checkStrategy(def, data, ctx);
|
|
4349
|
+
if (!strategyOk) return;
|
|
4350
|
+
const missingRequired = primitive.getFieldStatus().filter((f) => f.required && !f.captured);
|
|
4351
|
+
if (missingRequired.length > 0) return;
|
|
4352
|
+
if (def.backendAck && !primitive.exportSnapshot().backendAckReceived) {
|
|
4353
|
+
try {
|
|
4354
|
+
const res = await fetchWithTimeout(
|
|
4355
|
+
def.backendAck.url,
|
|
4356
|
+
{
|
|
4357
|
+
method: "POST",
|
|
4358
|
+
headers: { "content-type": "application/json", ...def.backendAck.headers ?? {} },
|
|
4359
|
+
body: JSON.stringify(data)
|
|
4360
|
+
},
|
|
4361
|
+
def.backendAck.timeoutMs ?? 3e4
|
|
4362
|
+
);
|
|
4363
|
+
if (!res.ok) {
|
|
4364
|
+
console.warn("[process] backend ACK non-2xx; not completing", res.status);
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
primitive.recordBackendAck(/* @__PURE__ */ new Date());
|
|
4368
|
+
} catch (err) {
|
|
4369
|
+
console.warn("[process] backend ACK failed; not completing", err);
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
const finalGate = primitive.canCallEnd();
|
|
4374
|
+
if (!finalGate.ok) return;
|
|
4375
|
+
markComplete();
|
|
4376
|
+
if (def.onComplete) {
|
|
4377
|
+
try {
|
|
4378
|
+
await def.onComplete(data, ctx);
|
|
4379
|
+
} catch (err) {
|
|
4380
|
+
console.error("[process] onComplete threw", err);
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
async function checkStrategy(def, data, ctx) {
|
|
4385
|
+
const strategy = def.completeWhen ?? "all-required-captured";
|
|
4386
|
+
if (strategy === "all-required-captured") {
|
|
4387
|
+
return Object.entries(def.collect).every(([name, f]) => !f.required || data[name] !== void 0);
|
|
4388
|
+
}
|
|
4389
|
+
if (strategy === "all-captured") {
|
|
4390
|
+
return Object.keys(def.collect).every((name) => data[name] !== void 0);
|
|
4391
|
+
}
|
|
4392
|
+
if (typeof strategy === "object" && "custom" in strategy) {
|
|
4393
|
+
return Promise.resolve(strategy.custom(data, ctx));
|
|
4394
|
+
}
|
|
4395
|
+
return false;
|
|
4396
|
+
}
|
|
4397
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
4398
|
+
const ctrl = new AbortController();
|
|
4399
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
4400
|
+
try {
|
|
4401
|
+
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
4402
|
+
} finally {
|
|
4403
|
+
clearTimeout(t);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
async function extractFields(client, utterance, remaining) {
|
|
4407
|
+
const fieldLines = remaining.map(([name, f]) => {
|
|
4408
|
+
const parts = [`type=${f.type}`];
|
|
4409
|
+
if (f.pattern) parts.push(`pattern=${f.pattern.source}`);
|
|
4410
|
+
if (f.enum) parts.push(`enum=${JSON.stringify(f.enum)}`);
|
|
4411
|
+
if (f.extract) parts.push(`hint=${f.extract}`);
|
|
4412
|
+
return `- ${name} (${parts.join(", ")})${f.required ? " [required]" : ""}`;
|
|
4413
|
+
});
|
|
4414
|
+
const prompt = [
|
|
4415
|
+
"Schema fields still to capture:",
|
|
4416
|
+
...fieldLines,
|
|
4417
|
+
"",
|
|
4418
|
+
`Only emit values for: ${remaining.map(([n]) => n).join(", ")}`,
|
|
4419
|
+
"",
|
|
4420
|
+
`Latest caller utterance:
|
|
4421
|
+
"""
|
|
4422
|
+
${utterance}
|
|
4423
|
+
"""`,
|
|
4424
|
+
"",
|
|
4425
|
+
'Respond as JSON: {"fields": [{"name": "<field>", "value": <value>}, ...]}.',
|
|
4426
|
+
"Omit any field you are not confident about. Empty array if nothing to extract.",
|
|
4427
|
+
"For datetime fields use ISO 8601. For arrays return arrays. For booleans return true/false."
|
|
4428
|
+
].join("\n");
|
|
4429
|
+
const ctrl = new AbortController();
|
|
4430
|
+
const timer = setTimeout(() => ctrl.abort(), 4e3);
|
|
4431
|
+
try {
|
|
4432
|
+
const completion = await client.chat.completions.create(
|
|
4433
|
+
{
|
|
4434
|
+
model: process.env["VOICELAYER_EXTRACTOR_MODEL"] ?? "gpt-4o-mini",
|
|
4435
|
+
response_format: { type: "json_object" },
|
|
4436
|
+
temperature: 0,
|
|
4437
|
+
messages: [
|
|
4438
|
+
{
|
|
4439
|
+
role: "system",
|
|
4440
|
+
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."
|
|
4441
|
+
},
|
|
4442
|
+
{ role: "user", content: prompt }
|
|
4443
|
+
]
|
|
4444
|
+
},
|
|
4445
|
+
{ signal: ctrl.signal }
|
|
4446
|
+
);
|
|
4447
|
+
const raw = completion.choices[0]?.message?.content ?? "{}";
|
|
4448
|
+
return parseExtractorResponse(raw, new Set(remaining.map(([n]) => n)));
|
|
4449
|
+
} finally {
|
|
4450
|
+
clearTimeout(timer);
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
function parseExtractorResponse(raw, allowed) {
|
|
4454
|
+
let parsed;
|
|
4455
|
+
try {
|
|
4456
|
+
parsed = JSON.parse(raw);
|
|
4457
|
+
} catch {
|
|
4458
|
+
return [];
|
|
4459
|
+
}
|
|
4460
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
4461
|
+
const fields = parsed.fields;
|
|
4462
|
+
if (!Array.isArray(fields)) return [];
|
|
4463
|
+
const out = [];
|
|
4464
|
+
for (const item of fields) {
|
|
4465
|
+
if (!item || typeof item !== "object") continue;
|
|
4466
|
+
const rawName = item.name;
|
|
4467
|
+
const value = item.value;
|
|
4468
|
+
if (typeof rawName !== "string") continue;
|
|
4469
|
+
const name = allowed.has(rawName) ? rawName : [...allowed].find((a) => a.trim() === rawName.trim());
|
|
4470
|
+
if (name === void 0) continue;
|
|
4471
|
+
if (value === null || value === void 0) continue;
|
|
4472
|
+
out.push({ name, value });
|
|
4473
|
+
}
|
|
4474
|
+
return out;
|
|
3833
4475
|
}
|
|
3834
4476
|
|
|
3835
4477
|
// src/runtime/session-adapter.ts
|
|
@@ -4075,6 +4717,29 @@ function createTextSessionAdapter(transport) {
|
|
|
4075
4717
|
return { adapter, push, setState, close };
|
|
4076
4718
|
}
|
|
4077
4719
|
|
|
4720
|
+
// src/flow-runtime.ts
|
|
4721
|
+
init_define_tool();
|
|
4722
|
+
function processSchemaToDefinition(schema) {
|
|
4723
|
+
const required = new Set(schema.completionGate.requiredFields);
|
|
4724
|
+
const collect = {};
|
|
4725
|
+
for (const f of schema.fields) {
|
|
4726
|
+
collect[f.name] = {
|
|
4727
|
+
type: f.type,
|
|
4728
|
+
required: required.has(f.name) || f.required,
|
|
4729
|
+
...f.ask !== void 0 ? { ask: f.ask } : {},
|
|
4730
|
+
...f.pattern !== void 0 ? { pattern: new RegExp(f.pattern) } : {},
|
|
4731
|
+
...f.min !== void 0 ? { min: f.min } : {},
|
|
4732
|
+
...f.max !== void 0 ? { max: f.max } : {},
|
|
4733
|
+
...f.enum !== void 0 ? { enum: f.enum } : {}
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
return {
|
|
4737
|
+
collect,
|
|
4738
|
+
completeWhen: "all-required-captured",
|
|
4739
|
+
...schema.completionGate.backendAck !== void 0 ? { backendAck: schema.completionGate.backendAck } : {}
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4078
4743
|
// src/runtime/text-session.ts
|
|
4079
4744
|
function buildTextContext(adapter, call, processRt) {
|
|
4080
4745
|
return {
|
|
@@ -4092,12 +4757,24 @@ function buildTextContext(adapter, call, processRt) {
|
|
|
4092
4757
|
get process() {
|
|
4093
4758
|
return { data: processRt.getData(), complete: processRt.isComplete() };
|
|
4094
4759
|
},
|
|
4760
|
+
// Voice-only actions, stubbed so a flow containing them can still be dry-run
|
|
4761
|
+
// over text. Without these the handoff node threw "deps.ctx.handoff is not
|
|
4762
|
+
// a function" mid-trace (seen in the builder's dry run); the graph now
|
|
4763
|
+
// reaches its handoff outcome cleanly and the trace says why nothing dialed.
|
|
4764
|
+
handoff: async (to) => {
|
|
4765
|
+
processRt.recordEvent({
|
|
4766
|
+
kind: "handoff.simulated",
|
|
4767
|
+
data: { to, reason: "text dry run \u2014 no telephony leg to transfer" }
|
|
4768
|
+
});
|
|
4769
|
+
},
|
|
4770
|
+
endCall: async () => {
|
|
4771
|
+
},
|
|
4095
4772
|
// Voice-only escape hatch is absent on text; left undefined.
|
|
4096
4773
|
livekit: { session: {}, job: {} }
|
|
4097
4774
|
};
|
|
4098
4775
|
}
|
|
4099
4776
|
function runTextSession(opts) {
|
|
4100
|
-
const processRt = opts.processRt ?? createProcessRuntime();
|
|
4777
|
+
const processRt = opts.processRt ?? createProcessRuntime(void 0);
|
|
4101
4778
|
const handle = createTextSessionAdapter({
|
|
4102
4779
|
send: opts.onAgentText,
|
|
4103
4780
|
...opts.generate ? { generate: opts.generate } : {}
|
|
@@ -4133,14 +4810,51 @@ function runTextSession(opts) {
|
|
|
4133
4810
|
end: () => handle.close()
|
|
4134
4811
|
};
|
|
4135
4812
|
}
|
|
4813
|
+
function deriveCollectFromProgram(program) {
|
|
4814
|
+
const collect = {};
|
|
4815
|
+
for (const [nodeId, node] of Object.entries(program.nodes)) {
|
|
4816
|
+
const cfg = node.config;
|
|
4817
|
+
if (node.type === "ask") {
|
|
4818
|
+
const name = typeof cfg.slot === "string" && cfg.slot ? cfg.slot : nodeId;
|
|
4819
|
+
const type = cfg.type;
|
|
4820
|
+
collect[name] = {
|
|
4821
|
+
type: type === "number" || type === "boolean" || type === "datetime" || type === "string[]" ? type : "string",
|
|
4822
|
+
...cfg.required !== false ? { required: true } : {},
|
|
4823
|
+
...typeof cfg.ask === "string" ? { ask: cfg.ask } : {},
|
|
4824
|
+
...typeof cfg.pattern === "string" ? { pattern: new RegExp(cfg.pattern) } : {}
|
|
4825
|
+
};
|
|
4826
|
+
}
|
|
4827
|
+
if (cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
4828
|
+
for (const slot of Object.keys(cfg.outputAs)) {
|
|
4829
|
+
collect[slot] ??= { type: "string" };
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
if (Array.isArray(cfg.assignments)) {
|
|
4833
|
+
for (const a of cfg.assignments) {
|
|
4834
|
+
if (a && typeof a.name === "string" && a.name.trim()) collect[a.name.trim()] ??= { type: "string" };
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
return { collect, completeWhen: "all-required-captured" };
|
|
4839
|
+
}
|
|
4840
|
+
function definitionFor(opts) {
|
|
4841
|
+
if (opts.schema) {
|
|
4842
|
+
try {
|
|
4843
|
+
return processSchemaToDefinition(opts.schema);
|
|
4844
|
+
} catch {
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
return deriveCollectFromProgram(opts.program);
|
|
4848
|
+
}
|
|
4136
4849
|
async function runTextTranscript(opts) {
|
|
4137
4850
|
const replies = [];
|
|
4138
4851
|
const trace = [];
|
|
4852
|
+
const processRt = opts.processRt ?? createProcessRuntime(definitionFor(opts));
|
|
4139
4853
|
const session = runTextSession({
|
|
4140
4854
|
program: opts.program,
|
|
4141
4855
|
onAgentText: (t) => void replies.push(t),
|
|
4142
4856
|
onTrace: (e) => void trace.push(e),
|
|
4143
|
-
...
|
|
4857
|
+
...processRt ? { processRt } : {},
|
|
4144
4858
|
...opts.generate ? { generate: opts.generate } : {},
|
|
4145
4859
|
...opts.call ? { call: opts.call } : {},
|
|
4146
4860
|
...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {}
|