vent-hq 0.13.2 → 0.15.0
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/README.md +22 -13
- package/dist/index.mjs +695 -954
- package/dist/{package-OH7X3UKK.mjs → package-WDXEE7G3.mjs} +10 -4
- package/package.json +9 -3
package/dist/index.mjs
CHANGED
|
@@ -13,156 +13,103 @@ import * as fs3 from "node:fs/promises";
|
|
|
13
13
|
import * as fs from "node:fs/promises";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
await fs.rm(CREDENTIALS_FILE);
|
|
37
|
-
} catch {
|
|
38
|
-
}
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
|
|
18
|
+
// ../../node_modules/.pnpm/jose@6.2.15/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
19
|
+
var encoder = new TextEncoder();
|
|
20
|
+
var decoder = new TextDecoder();
|
|
21
|
+
var strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
22
|
+
var UINT32_RANGE = 2 ** 32;
|
|
23
|
+
function decodeBase64(encoded, url = false) {
|
|
24
|
+
if (Uint8Array.fromBase64)
|
|
25
|
+
return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
|
|
26
|
+
if (url) {
|
|
27
|
+
if (encoded.includes("+") || encoded.includes("/"))
|
|
28
|
+
throw new TypeError("Invalid base64url");
|
|
29
|
+
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
30
|
+
}
|
|
31
|
+
const binary = atob(encoded), bytes = new Uint8Array(binary.length);
|
|
32
|
+
for (let i = 0; i < binary.length; i++)
|
|
33
|
+
bytes[i] = binary.charCodeAt(i);
|
|
34
|
+
return bytes;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
//
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
37
|
+
// ../../node_modules/.pnpm/jose@6.2.15/node_modules/jose/dist/webapi/util/errors.js
|
|
38
|
+
var JOSEError = class extends Error {
|
|
39
|
+
static code = "ERR_JOSE_GENERIC";
|
|
40
|
+
code = "ERR_JOSE_GENERIC";
|
|
41
|
+
constructor(message, options) {
|
|
42
|
+
super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
|
|
47
43
|
}
|
|
48
44
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
body = await res.json();
|
|
63
|
-
} catch {
|
|
64
|
-
body = await res.text();
|
|
65
|
-
}
|
|
66
|
-
throw new ApiError(res.status, body);
|
|
45
|
+
var JWTInvalid = class extends JOSEError {
|
|
46
|
+
static code = "ERR_JWT_INVALID";
|
|
47
|
+
code = "ERR_JWT_INVALID";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ../../node_modules/.pnpm/jose@6.2.15/node_modules/jose/dist/webapi/util/base64url.js
|
|
51
|
+
var invalid = "The input to be decoded is not correctly encoded.";
|
|
52
|
+
function decode(input) {
|
|
53
|
+
try {
|
|
54
|
+
return decodeBase64(typeof input == "string" ? input : decoder.decode(input), true);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new TypeError(invalid, { cause });
|
|
67
57
|
}
|
|
68
|
-
return res;
|
|
69
58
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
});
|
|
80
|
-
return res.json();
|
|
59
|
+
|
|
60
|
+
// ../../node_modules/.pnpm/jose@6.2.15/node_modules/jose/dist/webapi/lib/validate.js
|
|
61
|
+
function isObject(input) {
|
|
62
|
+
if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
|
|
63
|
+
return false;
|
|
64
|
+
const prototype = Object.getPrototypeOf(input);
|
|
65
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
81
66
|
}
|
|
82
67
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
} catch (err) {
|
|
101
|
-
if (err.name === "AbortError") throw err;
|
|
102
|
-
retries++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (!res.ok) {
|
|
106
|
-
const body = await res.text();
|
|
107
|
-
throw new Error(`SSE stream failed (${res.status}): ${body}`);
|
|
108
|
-
}
|
|
109
|
-
if (!res.body) {
|
|
110
|
-
throw new Error("SSE stream returned no body");
|
|
111
|
-
}
|
|
112
|
-
const reader = res.body.getReader();
|
|
113
|
-
const decoder = new TextDecoder();
|
|
114
|
-
let buffer = "";
|
|
115
|
-
try {
|
|
116
|
-
while (true) {
|
|
117
|
-
let readResult;
|
|
118
|
-
try {
|
|
119
|
-
readResult = await reader.read();
|
|
120
|
-
} catch (err) {
|
|
121
|
-
if (err.name === "AbortError") throw err;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
const { done, value } = readResult;
|
|
125
|
-
if (done) {
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
129
|
-
buffer += chunk;
|
|
130
|
-
const lines = buffer.split("\n");
|
|
131
|
-
buffer = lines.pop();
|
|
132
|
-
for (const line of lines) {
|
|
133
|
-
if (line.startsWith("data: ")) {
|
|
134
|
-
const raw = line.slice(6);
|
|
135
|
-
try {
|
|
136
|
-
const event = JSON.parse(raw);
|
|
137
|
-
if (event.id && seenIds.has(event.id)) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (event.id) seenIds.add(event.id);
|
|
141
|
-
yield event;
|
|
142
|
-
if (event.event_type === "run_complete") {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} finally {
|
|
151
|
-
reader.releaseLock();
|
|
152
|
-
}
|
|
153
|
-
retries++;
|
|
68
|
+
// ../../node_modules/.pnpm/jose@6.2.15/node_modules/jose/dist/webapi/util/decode_jwt.js
|
|
69
|
+
function decodeJwt(jwt) {
|
|
70
|
+
if (typeof jwt != "string")
|
|
71
|
+
throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string");
|
|
72
|
+
const { 1: encodedPayload, length } = jwt.split(".");
|
|
73
|
+
if (length === 5)
|
|
74
|
+
throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded");
|
|
75
|
+
if (length !== 3)
|
|
76
|
+
throw new JWTInvalid("Invalid JWT");
|
|
77
|
+
if (!encodedPayload)
|
|
78
|
+
throw new JWTInvalid("JWTs must contain a payload");
|
|
79
|
+
let claimsSetBytes;
|
|
80
|
+
try {
|
|
81
|
+
claimsSetBytes = decode(encodedPayload);
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
throw new JWTInvalid("Failed to base64url decode the payload", { cause });
|
|
154
84
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
85
|
+
let claimsSet;
|
|
86
|
+
try {
|
|
87
|
+
claimsSet = JSON.parse(strictDecoder.decode(claimsSetBytes));
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
throw new JWTInvalid("Failed to parse the decoded payload as JSON", { cause });
|
|
90
|
+
}
|
|
91
|
+
if (!isObject(claimsSet))
|
|
92
|
+
throw new JWTInvalid("Invalid JWT Claims Set");
|
|
93
|
+
return claimsSet;
|
|
159
94
|
}
|
|
160
95
|
|
|
161
|
-
// src/lib/output.ts
|
|
162
|
-
import { writeFileSync } from "node:fs";
|
|
163
|
-
|
|
164
96
|
// ../shared/src/constants.ts
|
|
165
|
-
var
|
|
97
|
+
var DEFAULT_AGENT_PORT = 3001;
|
|
98
|
+
var CALLER_ACCENTS = [
|
|
99
|
+
"american",
|
|
100
|
+
"british",
|
|
101
|
+
"australian",
|
|
102
|
+
"filipino",
|
|
103
|
+
"spanish_mexican",
|
|
104
|
+
"spanish_peninsular",
|
|
105
|
+
"spanish_colombian",
|
|
106
|
+
"spanish_argentine",
|
|
107
|
+
"german",
|
|
108
|
+
"french",
|
|
109
|
+
"italian",
|
|
110
|
+
"dutch",
|
|
111
|
+
"japanese"
|
|
112
|
+
];
|
|
166
113
|
|
|
167
114
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
168
115
|
var external_exports = {};
|
|
@@ -4205,116 +4152,92 @@ var coerce = {
|
|
|
4205
4152
|
};
|
|
4206
4153
|
var NEVER = INVALID;
|
|
4207
4154
|
|
|
4208
|
-
// ../shared/src/schemas.ts
|
|
4209
|
-
var
|
|
4210
|
-
pace: external_exports.enum(["slow", "normal", "fast"]).optional(),
|
|
4211
|
-
clarity: external_exports.enum(["clear", "vague", "rambling"]).optional(),
|
|
4212
|
-
disfluencies: external_exports.boolean().optional(),
|
|
4213
|
-
cooperation: external_exports.enum(["cooperative", "reluctant", "hostile"]).optional(),
|
|
4214
|
-
emotion: external_exports.enum(["neutral", "cheerful", "confused", "frustrated", "skeptical", "rushed"]).optional(),
|
|
4215
|
-
memory: external_exports.enum(["reliable", "unreliable"]).optional(),
|
|
4216
|
-
intent_clarity: external_exports.enum(["clear", "indirect", "vague"]).optional(),
|
|
4217
|
-
confirmation_style: external_exports.enum(["explicit", "vague"]).optional()
|
|
4218
|
-
}).optional();
|
|
4219
|
-
var CallerAudioEffectsSchema = external_exports.object({
|
|
4220
|
-
noise: external_exports.object({
|
|
4221
|
-
type: external_exports.enum(["babble", "white", "pink"]),
|
|
4222
|
-
snr_db: external_exports.number().min(0).max(40).default(10)
|
|
4223
|
-
}).optional(),
|
|
4224
|
-
speed: external_exports.number().min(0.5).max(2).optional(),
|
|
4225
|
-
speakerphone: external_exports.boolean().optional(),
|
|
4226
|
-
mic_distance: external_exports.enum(["close", "normal", "far"]).optional(),
|
|
4227
|
-
clarity: external_exports.number().min(0).max(1).optional(),
|
|
4228
|
-
accent: external_exports.string().optional(),
|
|
4229
|
-
packet_loss: external_exports.number().min(0).max(0.3).optional(),
|
|
4230
|
-
jitter_ms: external_exports.number().min(0).max(100).optional()
|
|
4231
|
-
});
|
|
4232
|
-
var ConversationCallSpecSchema = external_exports.object({
|
|
4233
|
-
name: external_exports.string().optional(),
|
|
4234
|
-
caller_prompt: external_exports.string().min(1),
|
|
4235
|
-
max_turns: external_exports.number().int().min(1).max(50).default(6),
|
|
4236
|
-
silence_threshold_ms: external_exports.number().int().min(200).max(1e4).optional(),
|
|
4237
|
-
persona: CallerPersonaSchema,
|
|
4238
|
-
caller_audio: CallerAudioEffectsSchema.optional(),
|
|
4239
|
-
/** ISO 639-1 language code for multilingual calls (e.g., "es", "fr", "de"). Caller speaks this language, STT transcribes it, judge evaluates in it. */
|
|
4240
|
-
language: external_exports.string().min(2).max(5).optional(),
|
|
4241
|
-
/** Caller voice gender (English only; default female). Ignored if caller_audio.accent is set or language is non-English. */
|
|
4242
|
-
voice: external_exports.enum(["male", "female"]).optional()
|
|
4243
|
-
});
|
|
4244
|
-
var CallSpecSchema = external_exports.object({
|
|
4245
|
-
call: ConversationCallSpecSchema
|
|
4246
|
-
});
|
|
4247
|
-
var AdapterTypeSchema = external_exports.enum(["websocket", "livekit", "vapi", "retell", "elevenlabs", "bland"]);
|
|
4248
|
-
var PlatformProviderSchema = external_exports.enum(["bland", "livekit", "vapi", "retell", "elevenlabs"]);
|
|
4249
|
-
var ObservedToolCallSchema = external_exports.object({
|
|
4250
|
-
name: external_exports.string(),
|
|
4251
|
-
arguments: external_exports.record(external_exports.unknown()),
|
|
4252
|
-
result: external_exports.unknown().optional(),
|
|
4253
|
-
successful: external_exports.boolean().optional(),
|
|
4254
|
-
provider_tool_type: external_exports.string().optional(),
|
|
4255
|
-
timestamp_ms: external_exports.number().optional(),
|
|
4256
|
-
latency_ms: external_exports.number().optional(),
|
|
4257
|
-
turn_index: external_exports.number().int().min(0).optional()
|
|
4258
|
-
});
|
|
4259
|
-
var ToolCallMetricsSchema = external_exports.object({
|
|
4260
|
-
total: external_exports.number().int().min(0),
|
|
4261
|
-
successful: external_exports.number().int().min(0),
|
|
4262
|
-
failed: external_exports.number().int().min(0),
|
|
4263
|
-
mean_latency_ms: external_exports.number().optional(),
|
|
4264
|
-
names: external_exports.array(external_exports.string())
|
|
4265
|
-
});
|
|
4155
|
+
// ../shared/src/schemas/platforms.ts
|
|
4156
|
+
var PLATFORM_PROVIDERS = ["vapi", "retell", "elevenlabs", "livekit", "bland"];
|
|
4266
4157
|
var BasePlatformSchema = external_exports.object({
|
|
4158
|
+
/** Max concurrent calls (platform-dependent) */
|
|
4267
4159
|
max_concurrency: external_exports.number().int().min(1).optional()
|
|
4268
4160
|
});
|
|
4269
4161
|
var BlandPlatformSchema = BasePlatformSchema.extend({
|
|
4270
4162
|
provider: external_exports.literal("bland"),
|
|
4163
|
+
/** Bland API key (the CLI reads BLAND_API_KEY from the user's env) */
|
|
4271
4164
|
bland_api_key: external_exports.string().optional(),
|
|
4165
|
+
/** Bland pathway ID (the CLI reads BLAND_PATHWAY_ID from the user's env) */
|
|
4272
4166
|
bland_pathway_id: external_exports.string().optional(),
|
|
4167
|
+
/** Bland persona ID — reusable agent preset (the CLI reads BLAND_PERSONA_ID from the user's env) */
|
|
4273
4168
|
persona_id: external_exports.string().optional(),
|
|
4169
|
+
/** Task prompt — used instead of pathway_id for simple agents */
|
|
4274
4170
|
task: external_exports.string().optional(),
|
|
4171
|
+
/** Tool definitions (inline objects) or tool IDs (TL-xxx strings) */
|
|
4275
4172
|
tools: external_exports.array(external_exports.unknown()).optional(),
|
|
4173
|
+
/** Voice name ("maya", "josh") or custom voice clone UUID */
|
|
4276
4174
|
voice: external_exports.string().optional(),
|
|
4175
|
+
/** Model: "base" (full features), "turbo" (fastest, limited features) */
|
|
4277
4176
|
model: external_exports.string().optional(),
|
|
4177
|
+
/** Opening sentence — overrides any greeting in the task/pathway */
|
|
4278
4178
|
first_sentence: external_exports.string().optional(),
|
|
4179
|
+
/** If true, agent waits for callee to speak first (default: false) */
|
|
4279
4180
|
wait_for_greeting: external_exports.boolean().optional(),
|
|
4181
|
+
/** Max call duration in minutes (default: 30) */
|
|
4280
4182
|
max_duration: external_exports.number().optional(),
|
|
4183
|
+
/** Temperature 0-1 (default: 0.7) */
|
|
4281
4184
|
temperature: external_exports.number().min(0).max(1).optional(),
|
|
4185
|
+
/** Language code e.g. "babel-en", "babel-es" */
|
|
4282
4186
|
language: external_exports.string().optional(),
|
|
4187
|
+
/** How quickly agent stops speaking when interrupted, in ms (default: 500) */
|
|
4283
4188
|
interruption_threshold: external_exports.number().optional(),
|
|
4189
|
+
/** When true, agent ignores user interruptions entirely */
|
|
4284
4190
|
block_interruptions: external_exports.boolean().optional(),
|
|
4191
|
+
/** When true, enable Bland's noise filtering on caller audio */
|
|
4285
4192
|
noise_cancellation: external_exports.boolean().optional(),
|
|
4193
|
+
/** Background audio: "office", "cafe", "restaurant", "none", or null (default phone static) */
|
|
4286
4194
|
background_track: external_exports.string().nullable().optional(),
|
|
4195
|
+
/** Boost transcription accuracy for specific words. Supports "word:boost_factor" */
|
|
4287
4196
|
keywords: external_exports.array(external_exports.string()).optional(),
|
|
4197
|
+
/** Key-value pairs accessible as {{variable}} in agent prompts/pathways */
|
|
4288
4198
|
request_data: external_exports.record(external_exports.unknown()).optional(),
|
|
4199
|
+
/** Pronunciation overrides */
|
|
4289
4200
|
pronunciation_guide: external_exports.array(external_exports.object({
|
|
4290
4201
|
word: external_exports.string(),
|
|
4291
4202
|
pronunciation: external_exports.string(),
|
|
4292
4203
|
case_sensitive: external_exports.boolean().optional(),
|
|
4293
4204
|
spaced: external_exports.boolean().optional()
|
|
4294
4205
|
})).optional(),
|
|
4206
|
+
/** Start pathway from a specific node instead of the default */
|
|
4295
4207
|
start_node_id: external_exports.string().optional(),
|
|
4208
|
+
/** Specific pathway version to test (default: production) */
|
|
4296
4209
|
pathway_version: external_exports.number().optional()
|
|
4297
4210
|
});
|
|
4298
4211
|
var LiveKitPlatformSchema = BasePlatformSchema.extend({
|
|
4299
4212
|
provider: external_exports.literal("livekit"),
|
|
4213
|
+
/** LiveKit API key (the CLI reads LIVEKIT_API_KEY from the user's env) */
|
|
4300
4214
|
livekit_api_key: external_exports.string().optional(),
|
|
4215
|
+
/** LiveKit API secret (the CLI reads LIVEKIT_API_SECRET from the user's env) */
|
|
4301
4216
|
livekit_api_secret: external_exports.string().optional(),
|
|
4217
|
+
/** LiveKit server URL, e.g. wss://your-app.livekit.cloud (the CLI reads LIVEKIT_URL from the user's env) */
|
|
4302
4218
|
livekit_url: external_exports.string().optional(),
|
|
4219
|
+
/** Explicit agent dispatch — agent_name from WorkerOptions. Omit for automatic dispatch. */
|
|
4303
4220
|
livekit_agent_name: external_exports.string().optional()
|
|
4304
4221
|
}).strict();
|
|
4305
4222
|
var VapiPlatformSchema = BasePlatformSchema.extend({
|
|
4306
4223
|
provider: external_exports.literal("vapi"),
|
|
4224
|
+
/** Vapi API key (the CLI reads VAPI_API_KEY from the user's env) */
|
|
4307
4225
|
vapi_api_key: external_exports.string().optional(),
|
|
4226
|
+
/** Vapi assistant ID (the CLI reads VAPI_ASSISTANT_ID or VAPI_AGENT_ID from the user's env) */
|
|
4308
4227
|
vapi_assistant_id: external_exports.string().optional()
|
|
4309
4228
|
});
|
|
4310
4229
|
var RetellPlatformSchema = BasePlatformSchema.extend({
|
|
4311
4230
|
provider: external_exports.literal("retell"),
|
|
4231
|
+
/** Retell API key (the CLI reads RETELL_API_KEY from the user's env) */
|
|
4312
4232
|
retell_api_key: external_exports.string().optional(),
|
|
4233
|
+
/** Retell agent ID (the CLI reads RETELL_AGENT_ID from the user's env) */
|
|
4313
4234
|
retell_agent_id: external_exports.string().optional()
|
|
4314
4235
|
});
|
|
4315
4236
|
var ElevenLabsPlatformSchema = BasePlatformSchema.extend({
|
|
4316
4237
|
provider: external_exports.literal("elevenlabs"),
|
|
4238
|
+
/** ElevenLabs API key (the CLI reads ELEVENLABS_API_KEY from the user's env) */
|
|
4317
4239
|
elevenlabs_api_key: external_exports.string().optional(),
|
|
4240
|
+
/** ElevenLabs agent ID (the CLI reads ELEVENLABS_AGENT_ID from the user's env) */
|
|
4318
4241
|
elevenlabs_agent_id: external_exports.string().optional()
|
|
4319
4242
|
});
|
|
4320
4243
|
var PlatformConfigSchema = external_exports.discriminatedUnion("provider", [
|
|
@@ -4324,103 +4247,118 @@ var PlatformConfigSchema = external_exports.discriminatedUnion("provider", [
|
|
|
4324
4247
|
RetellPlatformSchema,
|
|
4325
4248
|
ElevenLabsPlatformSchema
|
|
4326
4249
|
]);
|
|
4327
|
-
var
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4250
|
+
var REQUIRED_PLATFORM_FIELDS = {
|
|
4251
|
+
vapi: [["vapi_api_key"], ["vapi_assistant_id"]],
|
|
4252
|
+
retell: [["retell_api_key"], ["retell_agent_id"]],
|
|
4253
|
+
elevenlabs: [["elevenlabs_api_key"], ["elevenlabs_agent_id"]],
|
|
4254
|
+
livekit: [["livekit_api_key"], ["livekit_api_secret"], ["livekit_url"]],
|
|
4255
|
+
// Each test call creates a Bland web agent, which has no persona field: the
|
|
4256
|
+
// instructions must come from a pathway or a task prompt, so a persona_id
|
|
4257
|
+
// alone gives the agent no instructions.
|
|
4258
|
+
bland: [["bland_api_key"], ["bland_pathway_id", "task"]]
|
|
4259
|
+
};
|
|
4260
|
+
function missingPlatformFields(platform) {
|
|
4261
|
+
const values = platform;
|
|
4262
|
+
const requirements = REQUIRED_PLATFORM_FIELDS[platform.provider];
|
|
4263
|
+
return requirements.filter((anyOf) => anyOf.every((field) => !values[field]));
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
// ../shared/src/schemas/suite.ts
|
|
4267
|
+
var AdapterTypeSchema = external_exports.enum(["websocket", ...PLATFORM_PROVIDERS]);
|
|
4268
|
+
var CallerPersonaSchema = external_exports.object({
|
|
4269
|
+
pace: external_exports.enum(["slow", "normal", "fast"]).optional(),
|
|
4270
|
+
clarity: external_exports.enum(["clear", "vague", "rambling"]).optional(),
|
|
4271
|
+
disfluencies: external_exports.boolean().optional(),
|
|
4272
|
+
cooperation: external_exports.enum(["cooperative", "reluctant", "hostile"]).optional(),
|
|
4273
|
+
emotion: external_exports.enum(["neutral", "cheerful", "confused", "frustrated", "skeptical", "rushed"]).optional(),
|
|
4274
|
+
memory: external_exports.enum(["reliable", "unreliable"]).optional(),
|
|
4275
|
+
intent_clarity: external_exports.enum(["clear", "indirect", "vague"]).optional(),
|
|
4276
|
+
confirmation_style: external_exports.enum(["explicit", "vague"]).optional()
|
|
4335
4277
|
});
|
|
4336
|
-
var
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4278
|
+
var CallerAudioEffectsSchema = external_exports.object({
|
|
4279
|
+
noise: external_exports.object({
|
|
4280
|
+
type: external_exports.enum(["babble", "white", "pink"]),
|
|
4281
|
+
snr_db: external_exports.number().min(0).max(40).default(10)
|
|
4282
|
+
}).optional(),
|
|
4283
|
+
speed: external_exports.number().min(0.5).max(2).optional(),
|
|
4284
|
+
speakerphone: external_exports.boolean().optional(),
|
|
4285
|
+
mic_distance: external_exports.enum(["close", "normal", "far"]).optional(),
|
|
4286
|
+
clarity: external_exports.number().min(0).max(1).optional(),
|
|
4287
|
+
accent: external_exports.enum(CALLER_ACCENTS).optional(),
|
|
4288
|
+
packet_loss: external_exports.number().min(0).max(0.3).optional(),
|
|
4289
|
+
jitter_ms: external_exports.number().min(0).max(100).optional()
|
|
4290
|
+
});
|
|
4291
|
+
var ConversationCallSpecSchema = external_exports.object({
|
|
4292
|
+
name: external_exports.string().optional(),
|
|
4293
|
+
caller_prompt: external_exports.string().min(1),
|
|
4294
|
+
max_turns: external_exports.number().int().min(1).max(50).default(6),
|
|
4295
|
+
persona: CallerPersonaSchema.optional(),
|
|
4296
|
+
/** Audio effects applied to all caller audio (speakerphone, speed, noise, accent, etc.) */
|
|
4297
|
+
caller_audio: CallerAudioEffectsSchema.optional(),
|
|
4298
|
+
/** ISO 639-1 language code for multilingual calls (e.g., "es", "fr", "de"). The caller speaks it and Vent transcribes in it. */
|
|
4299
|
+
language: external_exports.string().min(2).max(5).optional(),
|
|
4300
|
+
/** Caller voice gender (English only). "male" → aura-2-apollo-en, "female" → aura-2-thalia-en (default). Ignored if caller_audio.accent is set or language is non-English. */
|
|
4301
|
+
voice: external_exports.enum(["male", "female"]).optional()
|
|
4302
|
+
});
|
|
4303
|
+
|
|
4304
|
+
// ../shared/src/schemas/call-result.ts
|
|
4305
|
+
var ComponentLatencySchema = external_exports.object({
|
|
4306
|
+
stt_ms: external_exports.number().optional(),
|
|
4307
|
+
llm_ms: external_exports.number().optional(),
|
|
4308
|
+
tts_ms: external_exports.number().optional(),
|
|
4309
|
+
speech_duration_ms: external_exports.number().optional()
|
|
4340
4310
|
});
|
|
4341
4311
|
var ConversationTurnSchema = external_exports.object({
|
|
4342
4312
|
role: external_exports.enum(["caller", "agent"]),
|
|
4343
4313
|
text: external_exports.string(),
|
|
4344
4314
|
timestamp_ms: external_exports.number(),
|
|
4315
|
+
/** Caller decision mode that produced this turn, or the silent wait mode that led into an agent turn. */
|
|
4345
4316
|
caller_decision_mode: external_exports.enum(["continue", "wait", "closing", "end_now"]).optional(),
|
|
4346
4317
|
audio_duration_ms: external_exports.number().optional(),
|
|
4318
|
+
/** Time to first byte: from the caller's audio being sent to the first agent audio arriving (ms). */
|
|
4347
4319
|
ttfb_ms: external_exports.number().optional(),
|
|
4320
|
+
/** Time to first word: from the caller's audio being sent to the agent starting to speak, by voice detection (ms). */
|
|
4348
4321
|
ttfw_ms: external_exports.number().optional(),
|
|
4322
|
+
/** Dead audio before speech starts: ttfw_ms - ttfb_ms */
|
|
4349
4323
|
silence_pad_ms: external_exports.number().optional(),
|
|
4350
4324
|
stt_confidence: external_exports.number().optional(),
|
|
4325
|
+
/** Harness TTS synthesis time for this turn's caller audio (ms) */
|
|
4351
4326
|
tts_ms: external_exports.number().optional(),
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
stt_ms: external_exports.number().optional(),
|
|
4355
|
-
llm_ms: external_exports.number().optional(),
|
|
4356
|
-
tts_ms: external_exports.number().optional(),
|
|
4357
|
-
speech_duration_ms: external_exports.number().optional()
|
|
4358
|
-
}).optional(),
|
|
4359
|
-
platform_transcript: external_exports.string().optional()
|
|
4327
|
+
/** Harness STT transcription time for this turn's agent audio (ms) */
|
|
4328
|
+
stt_ms: external_exports.number().optional()
|
|
4360
4329
|
});
|
|
4361
|
-
var
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
p95_ttfb_ms: external_exports.number(),
|
|
4366
|
-
p99_ttfb_ms: external_exports.number(),
|
|
4367
|
-
first_turn_ttfb_ms: external_exports.number(),
|
|
4368
|
-
ttfw_per_turn_ms: external_exports.array(external_exports.number()).optional(),
|
|
4369
|
-
p50_ttfw_ms: external_exports.number().optional(),
|
|
4370
|
-
p90_ttfw_ms: external_exports.number().optional(),
|
|
4371
|
-
p95_ttfw_ms: external_exports.number().optional(),
|
|
4372
|
-
p99_ttfw_ms: external_exports.number().optional(),
|
|
4373
|
-
first_turn_ttfw_ms: external_exports.number().optional(),
|
|
4374
|
-
mean_silence_pad_ms: external_exports.number().optional(),
|
|
4375
|
-
mouth_to_ear_est_ms: external_exports.number().optional(),
|
|
4376
|
-
drift_slope_ms_per_turn: external_exports.number().optional()
|
|
4330
|
+
var PlatformTurnSchema = external_exports.object({
|
|
4331
|
+
role: external_exports.enum(["caller", "agent"]),
|
|
4332
|
+
/** Agent turns: the text the agent's LLM wrote. Caller turns: what the agent's STT heard the caller say. */
|
|
4333
|
+
text: external_exports.string()
|
|
4377
4334
|
});
|
|
4378
|
-
var
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4335
|
+
var ObservedToolCallSchema = external_exports.object({
|
|
4336
|
+
name: external_exports.string(),
|
|
4337
|
+
arguments: external_exports.record(external_exports.unknown()),
|
|
4338
|
+
result: external_exports.unknown().optional(),
|
|
4339
|
+
successful: external_exports.boolean().optional(),
|
|
4340
|
+
provider_tool_type: external_exports.string().optional(),
|
|
4341
|
+
timestamp_ms: external_exports.number().optional(),
|
|
4342
|
+
latency_ms: external_exports.number().optional(),
|
|
4343
|
+
turn_index: external_exports.number().int().min(0).optional()
|
|
4383
4344
|
});
|
|
4384
4345
|
var SignalQualityMetricsSchema = external_exports.object({
|
|
4346
|
+
/** Mean signal-to-noise ratio across turns (dB). >20 good, <10 bad */
|
|
4385
4347
|
mean_snr_db: external_exports.number(),
|
|
4348
|
+
/** Max clipping ratio across turns (0-1). >0.01 = distortion */
|
|
4386
4349
|
max_clipping_ratio: external_exports.number(),
|
|
4350
|
+
/** Mean energy consistency across turns (0-1). <0.5 = unstable volume */
|
|
4387
4351
|
energy_consistency: external_exports.number(),
|
|
4352
|
+
/** Total sudden volume drops across all turns */
|
|
4388
4353
|
sudden_drops: external_exports.number().int().min(0),
|
|
4354
|
+
/** Total sudden volume spikes across all turns */
|
|
4389
4355
|
sudden_spikes: external_exports.number().int().min(0),
|
|
4356
|
+
/** All turns have clean start/end (no clicks) */
|
|
4390
4357
|
clean_edges: external_exports.boolean()
|
|
4391
4358
|
});
|
|
4392
|
-
var
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
tts_ms: external_exports.number().optional(),
|
|
4396
|
-
speech_duration_ms: external_exports.number().optional()
|
|
4397
|
-
});
|
|
4398
|
-
var ComponentLatencyMetricsSchema = external_exports.object({
|
|
4399
|
-
per_turn: external_exports.array(ComponentLatencySchema),
|
|
4400
|
-
mean_stt_ms: external_exports.number().optional(),
|
|
4401
|
-
mean_llm_ms: external_exports.number().optional(),
|
|
4402
|
-
mean_tts_ms: external_exports.number().optional(),
|
|
4403
|
-
p95_stt_ms: external_exports.number().optional(),
|
|
4404
|
-
p95_llm_ms: external_exports.number().optional(),
|
|
4405
|
-
p95_tts_ms: external_exports.number().optional(),
|
|
4406
|
-
bottleneck: external_exports.enum(["stt", "llm", "tts"]).optional()
|
|
4407
|
-
});
|
|
4408
|
-
var UsageEntrySchema = external_exports.object({
|
|
4409
|
-
type: external_exports.string(),
|
|
4410
|
-
provider: external_exports.string(),
|
|
4411
|
-
model: external_exports.string(),
|
|
4412
|
-
input_tokens: external_exports.number().int().optional(),
|
|
4413
|
-
output_tokens: external_exports.number().int().optional()
|
|
4414
|
-
});
|
|
4415
|
-
var CostBreakdownSchema = external_exports.object({
|
|
4416
|
-
stt_usd: external_exports.number().optional(),
|
|
4417
|
-
llm_usd: external_exports.number().optional(),
|
|
4418
|
-
tts_usd: external_exports.number().optional(),
|
|
4419
|
-
transport_usd: external_exports.number().optional(),
|
|
4420
|
-
platform_usd: external_exports.number().optional(),
|
|
4421
|
-
total_usd: external_exports.number().optional(),
|
|
4422
|
-
llm_prompt_tokens: external_exports.number().int().optional(),
|
|
4423
|
-
llm_completion_tokens: external_exports.number().int().optional()
|
|
4359
|
+
var CallCostSchema = external_exports.object({
|
|
4360
|
+
total_usd: external_exports.number(),
|
|
4361
|
+
breakdown: external_exports.record(external_exports.number()).optional()
|
|
4424
4362
|
});
|
|
4425
4363
|
var ProviderWarningSchema = external_exports.object({
|
|
4426
4364
|
message: external_exports.string().optional(),
|
|
@@ -4436,303 +4374,333 @@ var CallTransferSchema = external_exports.object({
|
|
|
4436
4374
|
});
|
|
4437
4375
|
var CallMetadataSchema = external_exports.object({
|
|
4438
4376
|
platform: external_exports.string(),
|
|
4377
|
+
/** Provider-visible call/conversation identifier for dashboard/API lookup. */
|
|
4439
4378
|
provider_call_id: external_exports.string().optional(),
|
|
4379
|
+
/** Secondary runtime/session identifier when distinct from the call ID. */
|
|
4440
4380
|
provider_session_id: external_exports.string().optional(),
|
|
4441
4381
|
ended_reason: external_exports.string().optional(),
|
|
4442
|
-
|
|
4443
|
-
cost_breakdown: CostBreakdownSchema.optional(),
|
|
4382
|
+
cost: CallCostSchema.optional(),
|
|
4444
4383
|
recording_url: external_exports.string().optional(),
|
|
4384
|
+
/** Provider-specific alternate recording artifacts keyed by variant name. */
|
|
4445
4385
|
recording_variants: external_exports.record(external_exports.string()).optional(),
|
|
4386
|
+
/** Provider-specific debug and deep-link URLs keyed by purpose. */
|
|
4446
4387
|
provider_debug_urls: external_exports.record(external_exports.string()).optional(),
|
|
4447
|
-
|
|
4448
|
-
usage: external_exports.array(UsageEntrySchema).optional(),
|
|
4388
|
+
/** Provider/runtime warnings retained in a normalized structure. */
|
|
4449
4389
|
provider_warnings: external_exports.array(ProviderWarningSchema).optional(),
|
|
4450
|
-
|
|
4390
|
+
/** Call transfers that occurred during the call */
|
|
4451
4391
|
transfers: external_exports.array(CallTransferSchema).optional()
|
|
4452
4392
|
});
|
|
4453
4393
|
var ConversationMetricsSchema = external_exports.object({
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
tool_calls: ToolCallMetricsSchema.optional(),
|
|
4394
|
+
/** How long Vent's connection to the platform took to open; feeds the mouth-to-ear estimate. */
|
|
4395
|
+
connect_latency_ms: external_exports.number().optional(),
|
|
4396
|
+
/** Raw audio signal quality (SNR, clipping, energy), aggregated across agent turns. */
|
|
4458
4397
|
signal_quality: SignalQualityMetricsSchema.optional(),
|
|
4459
|
-
|
|
4460
|
-
|
|
4398
|
+
/** Every component timing the platform reported (STT/LLM/TTS). It can report
|
|
4399
|
+
* more than Vent has agent turns, so this is the full list. */
|
|
4400
|
+
/** The platform's STT/LLM/TTS latencies for the call; the result shows their means. */
|
|
4401
|
+
component_latency: external_exports.object({ per_turn: external_exports.array(ComponentLatencySchema) }).optional()
|
|
4461
4402
|
});
|
|
4462
4403
|
var ConversationCallResultSchema = external_exports.object({
|
|
4463
4404
|
name: external_exports.string().optional(),
|
|
4464
4405
|
caller_prompt: external_exports.string(),
|
|
4465
4406
|
status: external_exports.enum(["completed", "error"]),
|
|
4407
|
+
/** How a completed call ended, when Vent ended it or the connection closed. */
|
|
4408
|
+
end_note: external_exports.string().optional(),
|
|
4409
|
+
/** Vent's record of the call: what Vent's caller said, and what Vent heard the agent say. */
|
|
4466
4410
|
transcript: external_exports.array(ConversationTurnSchema),
|
|
4411
|
+
/** The agent platform's own record of the call, when the platform provides one. */
|
|
4412
|
+
platform_transcript: external_exports.array(PlatformTurnSchema).optional(),
|
|
4467
4413
|
observed_tool_calls: external_exports.array(ObservedToolCallSchema).optional(),
|
|
4468
4414
|
duration_ms: external_exports.number(),
|
|
4469
4415
|
metrics: ConversationMetricsSchema,
|
|
4470
4416
|
error: external_exports.string().optional(),
|
|
4417
|
+
/** Platform call metadata (cost, ended reason, recording, analysis) */
|
|
4471
4418
|
call_metadata: CallMetadataSchema.optional()
|
|
4472
4419
|
});
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4420
|
+
|
|
4421
|
+
// ../shared/src/schemas/internal.ts
|
|
4422
|
+
var RefreshTokenRequestSchema = external_exports.object({
|
|
4423
|
+
refresh_token: external_exports.string().regex(/^vent_[0-9a-f]{48}$/)
|
|
4424
|
+
});
|
|
4425
|
+
var AccessCredentialsSchema = external_exports.object({
|
|
4426
|
+
access_token: external_exports.string().min(1),
|
|
4427
|
+
refresh_token: RefreshTokenRequestSchema.shape.refresh_token
|
|
4481
4428
|
});
|
|
4482
|
-
var
|
|
4483
|
-
var
|
|
4429
|
+
var RUN_TERMINAL_STATUSES = ["pass", "fail", "cancelled"];
|
|
4430
|
+
var WorkerResultCallbackSchema = external_exports.object({
|
|
4484
4431
|
run_id: external_exports.string().uuid(),
|
|
4485
|
-
status: external_exports.enum(
|
|
4432
|
+
status: external_exports.enum(RUN_TERMINAL_STATUSES),
|
|
4486
4433
|
conversation_result: ConversationCallResultSchema,
|
|
4487
|
-
aggregate: RunAggregateV2Schema,
|
|
4488
4434
|
error_text: external_exports.string().optional()
|
|
4489
4435
|
});
|
|
4436
|
+
var RunFailedCallbackSchema = external_exports.object({
|
|
4437
|
+
run_id: external_exports.string().uuid(),
|
|
4438
|
+
error_text: external_exports.string().max(2e3)
|
|
4439
|
+
});
|
|
4490
4440
|
var RunEventCallbackSchema = external_exports.object({
|
|
4491
4441
|
run_id: external_exports.string().uuid(),
|
|
4492
4442
|
event_type: external_exports.string().min(1).max(64),
|
|
4493
4443
|
message: external_exports.string().max(2e3),
|
|
4494
4444
|
metadata_json: external_exports.record(external_exports.unknown()).optional()
|
|
4495
4445
|
});
|
|
4496
|
-
var
|
|
4446
|
+
var RunJobSchema = external_exports.object({
|
|
4497
4447
|
run_id: external_exports.string().uuid(),
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4448
|
+
user_id: external_exports.string().min(1),
|
|
4449
|
+
adapter: AdapterTypeSchema,
|
|
4450
|
+
call_spec: external_exports.object({ call: ConversationCallSpecSchema }),
|
|
4451
|
+
agent_url: external_exports.string().optional(),
|
|
4452
|
+
agent_session_id: external_exports.string().uuid().optional()
|
|
4453
|
+
});
|
|
4454
|
+
var AgentSessionResponseSchema = external_exports.object({
|
|
4455
|
+
session_id: external_exports.string().uuid(),
|
|
4456
|
+
relay_token: external_exports.string(),
|
|
4457
|
+
api_url: external_exports.string(),
|
|
4458
|
+
agent_port: external_exports.number().int().min(1).max(65535),
|
|
4459
|
+
start_command: external_exports.string().nullable(),
|
|
4460
|
+
health_endpoint: external_exports.string()
|
|
4505
4461
|
});
|
|
4506
4462
|
|
|
4507
|
-
// ../shared/src/
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
call_metadata: formatCallMetadata(r.call_metadata, verbose)
|
|
4527
|
-
};
|
|
4528
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
4529
|
-
if (verbose) result.caller_prompt = r.caller_prompt;
|
|
4530
|
-
if (debug) result.debug = debug;
|
|
4531
|
-
return result;
|
|
4532
|
-
}
|
|
4533
|
-
function formatTranscript(turns, options) {
|
|
4534
|
-
if (!turns) return [];
|
|
4535
|
-
return turns.map((t) => {
|
|
4536
|
-
const turn = {
|
|
4537
|
-
role: t.role,
|
|
4538
|
-
text: t.text
|
|
4463
|
+
// ../shared/src/callback-auth.ts
|
|
4464
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4465
|
+
var RUNNER_CALLBACK_MAX_SKEW_MS = 5 * 6e4;
|
|
4466
|
+
|
|
4467
|
+
// src/lib/config.ts
|
|
4468
|
+
var CONFIG_DIR = path.join(homedir(), ".vent");
|
|
4469
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials");
|
|
4470
|
+
var API_BASE = process.env.VENT_API_URL ?? "https://api.venthq.dev";
|
|
4471
|
+
var refreshes = /* @__PURE__ */ new Map();
|
|
4472
|
+
async function readCredentials() {
|
|
4473
|
+
const accessToken = process.env.VENT_ACCESS_TOKEN;
|
|
4474
|
+
const refreshToken = process.env.VENT_REFRESH_TOKEN;
|
|
4475
|
+
if (accessToken || refreshToken) return { accessToken, refreshToken, environment: true };
|
|
4476
|
+
try {
|
|
4477
|
+
const parsed = AccessCredentialsSchema.safeParse(JSON.parse(await fs.readFile(CREDENTIALS_FILE, "utf-8")));
|
|
4478
|
+
if (parsed.success) return {
|
|
4479
|
+
accessToken: parsed.data.access_token,
|
|
4480
|
+
refreshToken: parsed.data.refresh_token,
|
|
4481
|
+
environment: false
|
|
4539
4482
|
};
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
if (options.verbose) {
|
|
4544
|
-
const debug = compactUnknownRecord({
|
|
4545
|
-
timestamp_ms: t.timestamp_ms,
|
|
4546
|
-
caller_decision_mode: t.caller_decision_mode,
|
|
4547
|
-
silence_pad_ms: t.silence_pad_ms,
|
|
4548
|
-
stt_confidence: t.stt_confidence,
|
|
4549
|
-
harness_tts_ms: t.tts_ms,
|
|
4550
|
-
harness_stt_ms: t.stt_ms,
|
|
4551
|
-
component_latency: t.component_latency,
|
|
4552
|
-
platform_transcript: t.platform_transcript
|
|
4553
|
-
});
|
|
4554
|
-
if (debug && Object.keys(debug).length > 0) {
|
|
4555
|
-
turn.debug = debug;
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
return turn;
|
|
4559
|
-
});
|
|
4560
|
-
}
|
|
4561
|
-
function formatLatency(latency, metrics, verbose) {
|
|
4562
|
-
const hasTtfw = metrics.mean_ttfw_ms != null && latency.p50_ttfw_ms != null && latency.p95_ttfw_ms != null;
|
|
4563
|
-
const responseTimeSource = hasTtfw ? "ttfw" : "ttfb";
|
|
4564
|
-
const result = {
|
|
4565
|
-
response_time_ms: hasTtfw ? metrics.mean_ttfw_ms : metrics.mean_ttfb_ms,
|
|
4566
|
-
response_time_source: responseTimeSource,
|
|
4567
|
-
p95_response_time_ms: hasTtfw ? latency.p95_ttfw_ms : latency.p95_ttfb_ms,
|
|
4568
|
-
first_response_time_ms: hasTtfw ? latency.first_turn_ttfw_ms ?? latency.first_turn_ttfb_ms : latency.first_turn_ttfb_ms
|
|
4569
|
-
};
|
|
4570
|
-
if (!verbose) return result;
|
|
4571
|
-
result.p50_response_time_ms = hasTtfw ? latency.p50_ttfw_ms : latency.p50_ttfb_ms;
|
|
4572
|
-
result.p90_response_time_ms = hasTtfw ? latency.p90_ttfw_ms ?? latency.p90_ttfb_ms : latency.p90_ttfb_ms;
|
|
4573
|
-
result.p99_response_time_ms = hasTtfw ? latency.p99_ttfw_ms ?? latency.p99_ttfb_ms : latency.p99_ttfb_ms;
|
|
4574
|
-
if (hasTtfw) {
|
|
4575
|
-
result.mean_ttfw_ms = metrics.mean_ttfw_ms;
|
|
4576
|
-
result.p50_ttfw_ms = latency.p50_ttfw_ms;
|
|
4577
|
-
result.p90_ttfw_ms = latency.p90_ttfw_ms ?? latency.p90_ttfb_ms;
|
|
4578
|
-
result.p95_ttfw_ms = latency.p95_ttfw_ms;
|
|
4579
|
-
result.p99_ttfw_ms = latency.p99_ttfw_ms ?? latency.p99_ttfb_ms;
|
|
4580
|
-
result.first_turn_ttfw_ms = latency.first_turn_ttfw_ms ?? latency.first_turn_ttfb_ms;
|
|
4581
|
-
}
|
|
4582
|
-
if (latency.drift_slope_ms_per_turn != null) result.drift_slope_ms_per_turn = latency.drift_slope_ms_per_turn;
|
|
4583
|
-
if (latency.mean_silence_pad_ms != null) result.mean_silence_pad_ms = latency.mean_silence_pad_ms;
|
|
4584
|
-
if (latency.mouth_to_ear_est_ms != null) result.mouth_to_ear_est_ms = latency.mouth_to_ear_est_ms;
|
|
4585
|
-
return result;
|
|
4483
|
+
} catch {
|
|
4484
|
+
}
|
|
4485
|
+
return null;
|
|
4586
4486
|
}
|
|
4587
|
-
function
|
|
4588
|
-
return
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
observed: (observed ?? []).map((c) => {
|
|
4595
|
-
const entry = {
|
|
4596
|
-
name: c.name,
|
|
4597
|
-
arguments: stripExecutionMessage(c.arguments),
|
|
4598
|
-
result: verbose ? c.result : truncateLargeArrays(c.result),
|
|
4599
|
-
successful: c.successful,
|
|
4600
|
-
latency_ms: c.latency_ms,
|
|
4601
|
-
turn_index: c.turn_index
|
|
4602
|
-
};
|
|
4603
|
-
if (c.provider_tool_type && c.provider_tool_type !== "custom") {
|
|
4604
|
-
entry.provider_tool_type = c.provider_tool_type;
|
|
4605
|
-
}
|
|
4606
|
-
return entry;
|
|
4607
|
-
})
|
|
4608
|
-
};
|
|
4487
|
+
function usableAccessToken(token, marginMs = 0) {
|
|
4488
|
+
if (!token) return false;
|
|
4489
|
+
try {
|
|
4490
|
+
return (decodeJwt(token).exp ?? 0) * 1e3 > Date.now() + marginMs;
|
|
4491
|
+
} catch {
|
|
4492
|
+
return false;
|
|
4493
|
+
}
|
|
4609
4494
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4495
|
+
async function refresh(source) {
|
|
4496
|
+
const refreshToken = source.refreshToken;
|
|
4497
|
+
const existing = refreshes.get(refreshToken);
|
|
4498
|
+
if (existing) return existing;
|
|
4499
|
+
const work = (async () => {
|
|
4500
|
+
let response;
|
|
4501
|
+
try {
|
|
4502
|
+
response = await fetch(`${API_BASE}/auth/refresh`, {
|
|
4503
|
+
method: "POST",
|
|
4504
|
+
headers: { "Content-Type": "application/json" },
|
|
4505
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
4506
|
+
signal: AbortSignal.timeout(1e4)
|
|
4507
|
+
});
|
|
4508
|
+
} catch {
|
|
4509
|
+
throw new Error("Could not refresh Vent sign-in. Check your connection and try again.");
|
|
4615
4510
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4511
|
+
if (!response.ok) {
|
|
4512
|
+
throw new Error(response.status === 401 ? "Vent credentials were rejected. Run `npx vent-hq login` again or replace VENT_REFRESH_TOKEN." : `Vent sign-in refresh failed (${response.status}). Try again.`);
|
|
4513
|
+
}
|
|
4514
|
+
const parsed = AccessCredentialsSchema.safeParse(await response.json());
|
|
4515
|
+
if (!parsed.success) throw new Error("Vent returned invalid sign-in credentials.");
|
|
4516
|
+
const credentials = parsed.data;
|
|
4517
|
+
if (source.environment) {
|
|
4518
|
+
process.env.VENT_ACCESS_TOKEN = credentials.access_token;
|
|
4519
|
+
process.env.VENT_REFRESH_TOKEN = credentials.refresh_token;
|
|
4520
|
+
} else {
|
|
4521
|
+
await saveCredentials(credentials);
|
|
4625
4522
|
}
|
|
4626
|
-
return
|
|
4523
|
+
return credentials.access_token;
|
|
4524
|
+
})();
|
|
4525
|
+
refreshes.set(refreshToken, work);
|
|
4526
|
+
try {
|
|
4527
|
+
return await work;
|
|
4528
|
+
} finally {
|
|
4529
|
+
refreshes.delete(refreshToken);
|
|
4627
4530
|
}
|
|
4628
|
-
return value;
|
|
4629
|
-
}
|
|
4630
|
-
function stripExecutionMessage(args) {
|
|
4631
|
-
if (!args || typeof args !== "object") return args;
|
|
4632
|
-
const { execution_message: _drop, ...rest } = args;
|
|
4633
|
-
return rest;
|
|
4634
4531
|
}
|
|
4635
|
-
function
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
mean_tts_ms: cl.mean_tts_ms,
|
|
4641
|
-
bottleneck: cl.bottleneck
|
|
4642
|
-
};
|
|
4643
|
-
if (!verbose) return result;
|
|
4644
|
-
const speechDurations = cl.per_turn.map((t) => t.speech_duration_ms).filter((v) => v != null);
|
|
4645
|
-
const meanSpeech = speechDurations.length > 0 ? Math.round(speechDurations.reduce((a, b) => a + b, 0) / speechDurations.length) : void 0;
|
|
4646
|
-
result.p95_stt_ms = cl.p95_stt_ms;
|
|
4647
|
-
result.p95_llm_ms = cl.p95_llm_ms;
|
|
4648
|
-
result.p95_tts_ms = cl.p95_tts_ms;
|
|
4649
|
-
result.mean_speech_duration_ms = meanSpeech;
|
|
4650
|
-
return result;
|
|
4532
|
+
async function loadAccessToken() {
|
|
4533
|
+
const source = await readCredentials();
|
|
4534
|
+
if (!source) return null;
|
|
4535
|
+
if (source.refreshToken && !usableAccessToken(source.accessToken, 6e4)) return refresh(source);
|
|
4536
|
+
return usableAccessToken(source.accessToken) ? source.accessToken : null;
|
|
4651
4537
|
}
|
|
4652
|
-
function
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
type: transfer.type,
|
|
4657
|
-
destination: transfer.destination,
|
|
4658
|
-
status: transfer.status,
|
|
4659
|
-
sources: transfer.sources
|
|
4660
|
-
};
|
|
4661
|
-
if (transfer.timestamp_ms != null) {
|
|
4662
|
-
formattedTransfer.timestamp_ms = transfer.timestamp_ms;
|
|
4663
|
-
}
|
|
4664
|
-
return formattedTransfer;
|
|
4665
|
-
});
|
|
4666
|
-
const result = {
|
|
4667
|
-
platform: meta.platform,
|
|
4668
|
-
provider_call_id: meta.provider_call_id,
|
|
4669
|
-
provider_session_id: meta.provider_session_id,
|
|
4670
|
-
ended_reason: meta.ended_reason,
|
|
4671
|
-
cost_usd: meta.cost_usd,
|
|
4672
|
-
recording_url: meta.recording_url
|
|
4673
|
-
};
|
|
4674
|
-
if (verbose) {
|
|
4675
|
-
result.cost_breakdown = meta.cost_breakdown;
|
|
4676
|
-
result.usage = meta.usage && meta.usage.length > 0 ? meta.usage : void 0;
|
|
4677
|
-
result.recording_variants = meta.recording_variants;
|
|
4678
|
-
result.provider_debug_urls = meta.provider_debug_urls;
|
|
4679
|
-
result.variables = meta.variables;
|
|
4680
|
-
}
|
|
4681
|
-
if (transfers && transfers.length > 0) {
|
|
4682
|
-
const completedTransferCount = transfers.filter((transfer) => transfer.status === "completed").length;
|
|
4683
|
-
const transferCompleted = completedTransferCount > 0;
|
|
4684
|
-
result.transfer_attempted = true;
|
|
4685
|
-
result.transfer_completed = transferCompleted;
|
|
4686
|
-
result.escalated = transferCompleted;
|
|
4687
|
-
result.transfer_count = transfers.length;
|
|
4688
|
-
result.completed_transfer_count = completedTransferCount;
|
|
4689
|
-
result.transfers = transfers;
|
|
4690
|
-
}
|
|
4691
|
-
return result;
|
|
4538
|
+
async function requireAccessToken() {
|
|
4539
|
+
const token = await loadAccessToken();
|
|
4540
|
+
if (!token) throw new Error("No Vent credentials found. Set VENT_REFRESH_TOKEN or run `npx vent-hq login`.");
|
|
4541
|
+
return token;
|
|
4692
4542
|
}
|
|
4693
|
-
function
|
|
4694
|
-
const
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
provider_metadata: result.call_metadata?.provider_metadata
|
|
4701
|
-
});
|
|
4702
|
-
return debug && Object.keys(debug).length > 0 ? debug : void 0;
|
|
4543
|
+
async function refreshAccessToken(rejectedToken) {
|
|
4544
|
+
const source = await readCredentials();
|
|
4545
|
+
if (!source?.refreshToken) return null;
|
|
4546
|
+
if (source.accessToken !== rejectedToken && usableAccessToken(source.accessToken, 6e4)) {
|
|
4547
|
+
return source.accessToken;
|
|
4548
|
+
}
|
|
4549
|
+
return refresh(source);
|
|
4703
4550
|
}
|
|
4704
|
-
function
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
turn_index: call.turn_index
|
|
4715
|
-
}));
|
|
4551
|
+
async function saveCredentials(credentials) {
|
|
4552
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
4553
|
+
const temporary = `${CREDENTIALS_FILE}.${randomUUID()}.tmp`;
|
|
4554
|
+
try {
|
|
4555
|
+
await fs.writeFile(temporary, `${JSON.stringify(credentials)}
|
|
4556
|
+
`, { mode: 384, flag: "wx" });
|
|
4557
|
+
await fs.rename(temporary, CREDENTIALS_FILE);
|
|
4558
|
+
} finally {
|
|
4559
|
+
await fs.rm(temporary, { force: true });
|
|
4560
|
+
}
|
|
4716
4561
|
}
|
|
4717
|
-
function
|
|
4718
|
-
|
|
4562
|
+
async function deleteCredentials() {
|
|
4563
|
+
try {
|
|
4564
|
+
await fs.rm(CREDENTIALS_FILE);
|
|
4565
|
+
} catch {
|
|
4566
|
+
}
|
|
4719
4567
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4568
|
+
|
|
4569
|
+
// src/lib/api.ts
|
|
4570
|
+
var ApiError = class extends Error {
|
|
4571
|
+
constructor(status, body) {
|
|
4572
|
+
super(`API error ${status}: ${typeof body === "string" ? body : JSON.stringify(body)}`);
|
|
4573
|
+
this.status = status;
|
|
4574
|
+
this.body = body;
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
async function authenticatedFetch(path4, options = {}) {
|
|
4578
|
+
const url = `${API_BASE}${path4}`;
|
|
4579
|
+
const token = await requireAccessToken();
|
|
4580
|
+
const send = (accessToken) => {
|
|
4581
|
+
const headers = new Headers(options.headers);
|
|
4582
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
4583
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
4584
|
+
return fetch(url, { ...options, headers });
|
|
4585
|
+
};
|
|
4586
|
+
const response = await send(token);
|
|
4587
|
+
if (response.status !== 401) return response;
|
|
4588
|
+
const refreshed = await refreshAccessToken(token);
|
|
4589
|
+
if (!refreshed) return response;
|
|
4590
|
+
await response.body?.cancel();
|
|
4591
|
+
return send(refreshed);
|
|
4723
4592
|
}
|
|
4724
|
-
function
|
|
4725
|
-
|
|
4593
|
+
async function apiFetch(path4, options = {}) {
|
|
4594
|
+
const res = await authenticatedFetch(path4, options);
|
|
4595
|
+
if (!res.ok) {
|
|
4596
|
+
const text = await res.text();
|
|
4597
|
+
let body = text;
|
|
4598
|
+
try {
|
|
4599
|
+
body = JSON.parse(text);
|
|
4600
|
+
} catch {
|
|
4601
|
+
}
|
|
4602
|
+
throw new ApiError(res.status, body);
|
|
4603
|
+
}
|
|
4604
|
+
return res;
|
|
4726
4605
|
}
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4606
|
+
|
|
4607
|
+
// src/lib/submit-run.ts
|
|
4608
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4609
|
+
var RETRY_DELAY_MS = 2e3;
|
|
4610
|
+
async function submitRun(payload) {
|
|
4611
|
+
const body = JSON.stringify({ ...payload, idempotency_key: randomUUID2() });
|
|
4612
|
+
const send = () => apiFetch("/runs/submit", { method: "POST", body });
|
|
4613
|
+
try {
|
|
4614
|
+
return await send();
|
|
4615
|
+
} catch (err) {
|
|
4616
|
+
const answerMayBeLost = !(err instanceof ApiError) || [502, 503, 504].includes(err.status);
|
|
4617
|
+
if (!answerMayBeLost) throw err;
|
|
4618
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
4619
|
+
return send();
|
|
4620
|
+
}
|
|
4730
4621
|
}
|
|
4731
4622
|
|
|
4732
|
-
//
|
|
4733
|
-
|
|
4623
|
+
// src/lib/sse.ts
|
|
4624
|
+
var MAX_RETRIES = 5;
|
|
4625
|
+
var RETRY_DELAY_MS2 = 2e3;
|
|
4626
|
+
async function* streamRunEvents(runId, signal) {
|
|
4627
|
+
const path4 = `/runs/${runId}/stream`;
|
|
4628
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
4629
|
+
let retries = 0;
|
|
4630
|
+
while (retries <= MAX_RETRIES) {
|
|
4631
|
+
if (retries > 0) {
|
|
4632
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS2));
|
|
4633
|
+
}
|
|
4634
|
+
let res;
|
|
4635
|
+
try {
|
|
4636
|
+
res = await authenticatedFetch(path4, { signal });
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
if (err.name === "AbortError") throw err;
|
|
4639
|
+
retries++;
|
|
4640
|
+
continue;
|
|
4641
|
+
}
|
|
4642
|
+
if (res.status >= 502 && res.status <= 504) {
|
|
4643
|
+
retries++;
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
if (!res.ok) {
|
|
4647
|
+
const body = await res.text();
|
|
4648
|
+
throw new Error(`SSE stream failed (${res.status}): ${body}`);
|
|
4649
|
+
}
|
|
4650
|
+
if (!res.body) {
|
|
4651
|
+
throw new Error("SSE stream returned no body");
|
|
4652
|
+
}
|
|
4653
|
+
const reader = res.body.getReader();
|
|
4654
|
+
const decoder2 = new TextDecoder();
|
|
4655
|
+
let buffer = "";
|
|
4656
|
+
try {
|
|
4657
|
+
while (true) {
|
|
4658
|
+
let readResult;
|
|
4659
|
+
try {
|
|
4660
|
+
readResult = await reader.read();
|
|
4661
|
+
} catch (err) {
|
|
4662
|
+
if (err.name === "AbortError") throw err;
|
|
4663
|
+
break;
|
|
4664
|
+
}
|
|
4665
|
+
const { done, value } = readResult;
|
|
4666
|
+
if (done) {
|
|
4667
|
+
break;
|
|
4668
|
+
}
|
|
4669
|
+
const chunk = decoder2.decode(value, { stream: true });
|
|
4670
|
+
buffer += chunk;
|
|
4671
|
+
const lines = buffer.split("\n");
|
|
4672
|
+
buffer = lines.pop();
|
|
4673
|
+
for (const line of lines) {
|
|
4674
|
+
if (line.startsWith("data: ")) {
|
|
4675
|
+
const raw = line.slice(6);
|
|
4676
|
+
try {
|
|
4677
|
+
const event = JSON.parse(raw);
|
|
4678
|
+
if (event.id && seenIds.has(event.id)) {
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
if (event.id) seenIds.add(event.id);
|
|
4682
|
+
yield event;
|
|
4683
|
+
if (event.event_type === "run_complete") {
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
} catch {
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
} finally {
|
|
4692
|
+
reader.releaseLock();
|
|
4693
|
+
}
|
|
4694
|
+
retries++;
|
|
4695
|
+
}
|
|
4696
|
+
yield {
|
|
4697
|
+
event_type: "error",
|
|
4698
|
+
message: `Stream lost after ${MAX_RETRIES} reconnect attempts without receiving run_complete`
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4734
4701
|
|
|
4735
4702
|
// src/lib/output.ts
|
|
4703
|
+
import { writeFileSync } from "node:fs";
|
|
4736
4704
|
var isTTY = process.stdout.isTTY;
|
|
4737
4705
|
function stdoutSync(data) {
|
|
4738
4706
|
if (isTTY) {
|
|
@@ -4752,29 +4720,13 @@ var red = (s) => isTTY ? `\x1B[31m${s}\x1B[0m` : s;
|
|
|
4752
4720
|
var yellow = (s) => isTTY ? `\x1B[33m${s}\x1B[0m` : s;
|
|
4753
4721
|
var blue = (s) => isTTY ? `\x1B[34m${s}\x1B[0m` : s;
|
|
4754
4722
|
function printEvent(event) {
|
|
4755
|
-
if (!isTTY) return;
|
|
4756
|
-
|
|
4757
|
-
switch (event.event_type) {
|
|
4758
|
-
case "call_completed":
|
|
4759
|
-
printCallResult(meta);
|
|
4760
|
-
break;
|
|
4761
|
-
case "run_complete":
|
|
4762
|
-
printRunComplete(meta);
|
|
4763
|
-
break;
|
|
4764
|
-
case "call_started": {
|
|
4765
|
-
const name = meta.call_name ?? "call";
|
|
4766
|
-
stdoutSync(dim(` \u25B8 ${name}\u2026`) + "\n");
|
|
4767
|
-
break;
|
|
4768
|
-
}
|
|
4769
|
-
default:
|
|
4770
|
-
stdoutSync(dim(` [${event.event_type}]`) + "\n");
|
|
4771
|
-
}
|
|
4723
|
+
if (!isTTY || event.event_type === "run_complete") return;
|
|
4724
|
+
stdoutSync(dim(` [${event.event_type}]`) + "\n");
|
|
4772
4725
|
}
|
|
4773
|
-
function printCallResult(
|
|
4774
|
-
const
|
|
4775
|
-
const
|
|
4776
|
-
const
|
|
4777
|
-
const durationMs = result?.duration_ms ?? meta.duration_ms;
|
|
4726
|
+
function printCallResult(result) {
|
|
4727
|
+
const callName = result.name ?? "call";
|
|
4728
|
+
const callStatus = result.status;
|
|
4729
|
+
const durationMs = result.duration_ms;
|
|
4778
4730
|
const statusIcon = callStatus === "completed" ? green("\u25CF") : red("\u25CF");
|
|
4779
4731
|
const duration = durationMs != null ? (durationMs / 1e3).toFixed(1) + "s" : "\u2014";
|
|
4780
4732
|
const parts = [statusIcon, bold(callName), dim(duration)];
|
|
@@ -4804,106 +4756,21 @@ function printCallResult(meta) {
|
|
|
4804
4756
|
}
|
|
4805
4757
|
}
|
|
4806
4758
|
}
|
|
4807
|
-
function
|
|
4808
|
-
const agg = meta.aggregate;
|
|
4809
|
-
const counts = agg?.conversation_calls;
|
|
4810
|
-
const total = meta.total_calls ?? counts?.total;
|
|
4811
|
-
stdoutSync("\n");
|
|
4812
|
-
stdoutSync(bold("Run complete") + dim(" \u2014 Vent does not judge mission success; review the calls above.") + "\n");
|
|
4813
|
-
if (total != null) {
|
|
4814
|
-
stdoutSync(`${total} call${total === 1 ? "" : "s"} ran
|
|
4815
|
-
`);
|
|
4816
|
-
}
|
|
4817
|
-
}
|
|
4818
|
-
function printSummary(callResults, runComplete, runId, options = {}) {
|
|
4819
|
-
const allCalls = options.rawCalls ? formatRawCalls(options.rawCalls, options.verbose ?? false) : callResults.map((e) => {
|
|
4820
|
-
const meta = e.metadata_json ?? {};
|
|
4821
|
-
const r = meta.result;
|
|
4822
|
-
if (r) return r;
|
|
4823
|
-
return {
|
|
4824
|
-
name: meta.call_name ?? "call",
|
|
4825
|
-
status: meta.status ?? "unknown",
|
|
4826
|
-
duration_ms: meta.duration_ms,
|
|
4827
|
-
error: null
|
|
4828
|
-
};
|
|
4829
|
-
});
|
|
4830
|
-
const agg = runComplete.aggregate;
|
|
4831
|
-
const counts = agg?.conversation_calls;
|
|
4832
|
-
const summaryData = buildRunSummaryJson({
|
|
4833
|
-
runId,
|
|
4834
|
-
total: runComplete.total_calls ?? counts?.total,
|
|
4835
|
-
formattedCalls: allCalls,
|
|
4836
|
-
verbose: options.verbose,
|
|
4837
|
-
runDetails: options.runDetails ?? { aggregate: runComplete.aggregate }
|
|
4838
|
-
});
|
|
4759
|
+
function printRunOutput(output) {
|
|
4839
4760
|
if (!isTTY) {
|
|
4840
|
-
stdoutSync(JSON.stringify(
|
|
4761
|
+
stdoutSync(JSON.stringify(output, null, 2) + "\n");
|
|
4841
4762
|
return;
|
|
4842
4763
|
}
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
stdoutSync(" " + parts.join(" ") + "\n");
|
|
4852
|
-
}
|
|
4853
|
-
}
|
|
4854
|
-
stdoutSync(dim(`Run ID: ${runId}`) + "\n");
|
|
4855
|
-
}
|
|
4856
|
-
function buildRunSummaryJson(options) {
|
|
4857
|
-
const calls = options.rawCalls ? formatRawCalls(options.rawCalls, options.verbose ?? false) : options.formattedCalls ?? [];
|
|
4858
|
-
const summaryData = {
|
|
4859
|
-
run_id: options.runId,
|
|
4860
|
-
...options.status != null ? { status: options.status } : {},
|
|
4861
|
-
total: options.total,
|
|
4862
|
-
...options.passed != null ? { passed: options.passed } : {},
|
|
4863
|
-
...options.failed != null ? { failed: options.failed } : {},
|
|
4864
|
-
calls
|
|
4865
|
-
};
|
|
4866
|
-
const details = options.runDetails;
|
|
4867
|
-
if (details?.created_at != null) summaryData["created_at"] = details.created_at;
|
|
4868
|
-
if (details?.started_at != null) summaryData["started_at"] = details.started_at;
|
|
4869
|
-
if (details?.finished_at != null) summaryData["finished_at"] = details.finished_at;
|
|
4870
|
-
if (details?.duration_ms != null) summaryData["duration_ms"] = details.duration_ms;
|
|
4871
|
-
if (details?.error_text != null) summaryData["error_text"] = details.error_text;
|
|
4872
|
-
if (details?.aggregate != null) {
|
|
4873
|
-
summaryData["aggregate"] = stripPassFailFromAggregate(details.aggregate);
|
|
4874
|
-
}
|
|
4875
|
-
return summaryData;
|
|
4876
|
-
}
|
|
4877
|
-
function stripPassFailFromAggregate(aggregate) {
|
|
4878
|
-
if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return aggregate;
|
|
4879
|
-
const out = {};
|
|
4880
|
-
for (const [k, v] of Object.entries(aggregate)) {
|
|
4881
|
-
if (k === "conversation_calls" && v && typeof v === "object") {
|
|
4882
|
-
const cc = v;
|
|
4883
|
-
const filtered = {};
|
|
4884
|
-
for (const [ck, cv] of Object.entries(cc)) {
|
|
4885
|
-
if (ck === "passed" || ck === "failed") continue;
|
|
4886
|
-
filtered[ck] = cv;
|
|
4887
|
-
}
|
|
4888
|
-
out[k] = filtered;
|
|
4889
|
-
} else {
|
|
4890
|
-
out[k] = v;
|
|
4891
|
-
}
|
|
4764
|
+
const calls = output.calls ?? [];
|
|
4765
|
+
for (const call of calls) printCallResult(call);
|
|
4766
|
+
stdoutSync("\n");
|
|
4767
|
+
stdoutSync(bold("Run complete") + dim(" \u2014 Vent does not judge mission success; review the calls above.") + "\n");
|
|
4768
|
+
stdoutSync(`${calls.length} call${calls.length === 1 ? "" : "s"} ran
|
|
4769
|
+
`);
|
|
4770
|
+
if (typeof output.error_text === "string") {
|
|
4771
|
+
stdoutSync(red(`Run ${String(output.status ?? "ended")}: ${output.error_text}`) + "\n");
|
|
4892
4772
|
}
|
|
4893
|
-
|
|
4894
|
-
}
|
|
4895
|
-
function formatRawCalls(rawCalls, verbose) {
|
|
4896
|
-
return rawCalls.map((raw) => {
|
|
4897
|
-
const formatted = formatConversationResult(raw, { verbose });
|
|
4898
|
-
if (formatted) return formatted;
|
|
4899
|
-
const fallback = raw;
|
|
4900
|
-
return {
|
|
4901
|
-
name: typeof fallback["name"] === "string" ? fallback["name"] : "call",
|
|
4902
|
-
status: typeof fallback["status"] === "string" ? fallback["status"] : "unknown",
|
|
4903
|
-
duration_ms: typeof fallback["duration_ms"] === "number" ? fallback["duration_ms"] : void 0,
|
|
4904
|
-
error: typeof fallback["error"] === "string" ? fallback["error"] : null
|
|
4905
|
-
};
|
|
4906
|
-
});
|
|
4773
|
+
stdoutSync(dim(`Run ID: ${String(output.run_id)}`) + "\n");
|
|
4907
4774
|
}
|
|
4908
4775
|
function printError(message) {
|
|
4909
4776
|
const line = red(bold("error")) + ` ${message}
|
|
@@ -4940,35 +4807,25 @@ function gitSha() {
|
|
|
4940
4807
|
return null;
|
|
4941
4808
|
}
|
|
4942
4809
|
}
|
|
4943
|
-
async function saveRunHistory(
|
|
4810
|
+
async function saveRunHistory(output) {
|
|
4944
4811
|
try {
|
|
4812
|
+
const runId = output.run_id;
|
|
4945
4813
|
const dir = path2.join(process.cwd(), ".vent", "runs");
|
|
4946
4814
|
await fs2.mkdir(dir, { recursive: true });
|
|
4947
4815
|
const now = /* @__PURE__ */ new Date();
|
|
4948
4816
|
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4949
4817
|
const shortId = runId.slice(0, 8);
|
|
4950
|
-
const
|
|
4951
|
-
const convCalls = aggregate?.conversation_calls;
|
|
4952
|
-
const total = convCalls?.total ?? 0;
|
|
4953
|
-
const passed = convCalls?.passed ?? 0;
|
|
4954
|
-
const failed = convCalls?.failed ?? 0;
|
|
4818
|
+
const calls = output.calls;
|
|
4955
4819
|
const entry = {
|
|
4956
4820
|
run_id: runId,
|
|
4957
4821
|
timestamp: now.toISOString(),
|
|
4958
4822
|
git_sha: gitSha(),
|
|
4959
4823
|
summary: {
|
|
4960
|
-
status:
|
|
4961
|
-
calls_total:
|
|
4962
|
-
|
|
4963
|
-
calls_failed: failed,
|
|
4964
|
-
total_duration_ms: aggregate?.total_duration_ms,
|
|
4965
|
-
total_cost_usd: aggregate?.total_cost_usd
|
|
4824
|
+
status: output.status,
|
|
4825
|
+
calls_total: calls.length,
|
|
4826
|
+
total_duration_ms: calls.reduce((sum, c) => sum + (c.duration_ms ?? 0), 0)
|
|
4966
4827
|
},
|
|
4967
|
-
call_results:
|
|
4968
|
-
const meta = e.metadata_json ?? {};
|
|
4969
|
-
const result = meta.result ?? {};
|
|
4970
|
-
return { ...result, status: meta.status };
|
|
4971
|
-
})
|
|
4828
|
+
call_results: calls
|
|
4972
4829
|
};
|
|
4973
4830
|
const filename = `${timestamp}_${shortId}.json`;
|
|
4974
4831
|
const filepath = path2.join(dir, filename);
|
|
@@ -4979,7 +4836,7 @@ async function saveRunHistory(runId, callResults, runCompleteData) {
|
|
|
4979
4836
|
}
|
|
4980
4837
|
}
|
|
4981
4838
|
|
|
4982
|
-
// src/lib/platform-
|
|
4839
|
+
// src/lib/platform-config.ts
|
|
4983
4840
|
var PLATFORM_ENV_MAP = {
|
|
4984
4841
|
vapi: { vapi_api_key: "VAPI_API_KEY", vapi_assistant_id: "VAPI_ASSISTANT_ID" },
|
|
4985
4842
|
bland: { bland_api_key: "BLAND_API_KEY", bland_pathway_id: "BLAND_PATHWAY_ID", persona_id: "BLAND_PERSONA_ID" },
|
|
@@ -4993,61 +4850,21 @@ function isRemotePlatformAdapter(adapter) {
|
|
|
4993
4850
|
function looksLikeEnvReference(value) {
|
|
4994
4851
|
return typeof value === "string" && /^[A-Z][A-Z0-9_]+$/.test(value);
|
|
4995
4852
|
}
|
|
4996
|
-
function
|
|
4997
|
-
return
|
|
4853
|
+
function describeField(adapter, field) {
|
|
4854
|
+
if (adapter === "vapi" && field === "vapi_assistant_id") return "VAPI_ASSISTANT_ID or VAPI_AGENT_ID";
|
|
4855
|
+
const envFields = PLATFORM_ENV_MAP[adapter];
|
|
4856
|
+
return envFields[field] ?? `platform.${field}`;
|
|
4998
4857
|
}
|
|
4999
|
-
function
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
}
|
|
5005
|
-
if (isMissingResolvedValue(platform.vapi_assistant_id)) {
|
|
5006
|
-
throw new Error("Missing VAPI_ASSISTANT_ID or VAPI_AGENT_ID in .env");
|
|
5007
|
-
}
|
|
5008
|
-
return;
|
|
5009
|
-
case "retell":
|
|
5010
|
-
if (isMissingResolvedValue(platform.retell_api_key)) {
|
|
5011
|
-
throw new Error("Missing RETELL_API_KEY in .env");
|
|
5012
|
-
}
|
|
5013
|
-
if (isMissingResolvedValue(platform.retell_agent_id)) {
|
|
5014
|
-
throw new Error("Missing RETELL_AGENT_ID in .env");
|
|
5015
|
-
}
|
|
5016
|
-
return;
|
|
5017
|
-
case "elevenlabs":
|
|
5018
|
-
if (isMissingResolvedValue(platform.elevenlabs_api_key)) {
|
|
5019
|
-
throw new Error("Missing ELEVENLABS_API_KEY in .env");
|
|
5020
|
-
}
|
|
5021
|
-
if (isMissingResolvedValue(platform.elevenlabs_agent_id)) {
|
|
5022
|
-
throw new Error("Missing ELEVENLABS_AGENT_ID in .env");
|
|
5023
|
-
}
|
|
5024
|
-
return;
|
|
5025
|
-
case "livekit":
|
|
5026
|
-
if (isMissingResolvedValue(platform.livekit_api_key)) {
|
|
5027
|
-
throw new Error("Missing LIVEKIT_API_KEY in .env");
|
|
5028
|
-
}
|
|
5029
|
-
if (isMissingResolvedValue(platform.livekit_api_secret)) {
|
|
5030
|
-
throw new Error("Missing LIVEKIT_API_SECRET in .env");
|
|
5031
|
-
}
|
|
5032
|
-
if (isMissingResolvedValue(platform.livekit_url)) {
|
|
5033
|
-
throw new Error("Missing LIVEKIT_URL in .env");
|
|
5034
|
-
}
|
|
5035
|
-
return;
|
|
5036
|
-
case "bland":
|
|
5037
|
-
if (isMissingResolvedValue(platform.bland_api_key)) {
|
|
5038
|
-
throw new Error("Missing BLAND_API_KEY in .env");
|
|
5039
|
-
}
|
|
5040
|
-
if (isMissingResolvedValue(platform.bland_pathway_id) && isMissingResolvedValue(platform.task) && isMissingResolvedValue(platform.persona_id)) {
|
|
5041
|
-
throw new Error("Missing BLAND_PATHWAY_ID, BLAND_PERSONA_ID, or platform.task in .env");
|
|
5042
|
-
}
|
|
5043
|
-
return;
|
|
5044
|
-
}
|
|
4858
|
+
function requireFields(adapter, platform) {
|
|
4859
|
+
const missing = missingPlatformFields(platform);
|
|
4860
|
+
if (missing.length === 0) return;
|
|
4861
|
+
const list = missing.map((anyOf) => anyOf.map((field) => describeField(adapter, field)).join(" or ")).join(", ");
|
|
4862
|
+
throw new Error(`Missing ${list} (set it in .env, .env.local or the environment)`);
|
|
5045
4863
|
}
|
|
5046
4864
|
function resolveRemotePlatformConfig(config) {
|
|
5047
4865
|
const cfg = config;
|
|
5048
4866
|
const adapter = cfg.connection?.adapter;
|
|
5049
4867
|
if (!isRemotePlatformAdapter(adapter)) return null;
|
|
5050
|
-
if (cfg.connection?.platform_connection_id) return null;
|
|
5051
4868
|
const current = cfg.connection?.platform ?? {};
|
|
5052
4869
|
const provider = current.provider ?? adapter;
|
|
5053
4870
|
if (provider !== adapter) {
|
|
@@ -5060,12 +4877,8 @@ function resolveRemotePlatformConfig(config) {
|
|
|
5060
4877
|
const envFields = PLATFORM_ENV_MAP[adapter];
|
|
5061
4878
|
for (const [field, envVar] of Object.entries(envFields)) {
|
|
5062
4879
|
const currentValue = resolved[field];
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
const envValue = process.env[envVar];
|
|
5066
|
-
if (envValue) {
|
|
5067
|
-
resolved[field] = envValue;
|
|
5068
|
-
}
|
|
4880
|
+
if (!currentValue || looksLikeEnvReference(currentValue)) {
|
|
4881
|
+
resolved[field] = process.env[envVar] || void 0;
|
|
5069
4882
|
}
|
|
5070
4883
|
}
|
|
5071
4884
|
if (adapter === "vapi" && !resolved.vapi_assistant_id) {
|
|
@@ -5073,18 +4886,13 @@ function resolveRemotePlatformConfig(config) {
|
|
|
5073
4886
|
if (alt) resolved.vapi_assistant_id = alt;
|
|
5074
4887
|
}
|
|
5075
4888
|
const platform = PlatformConfigSchema.parse(resolved);
|
|
5076
|
-
|
|
4889
|
+
requireFields(adapter, platform);
|
|
5077
4890
|
return platform;
|
|
5078
4891
|
}
|
|
5079
4892
|
|
|
5080
4893
|
// src/commands/run.ts
|
|
5081
4894
|
var isTTY2 = process.stdout.isTTY;
|
|
5082
4895
|
async function runCommand(args) {
|
|
5083
|
-
const accessToken = await loadAccessToken();
|
|
5084
|
-
if (!accessToken) {
|
|
5085
|
-
printError("No Vent access token found. Set VENT_ACCESS_TOKEN, run `npx vent-hq login`, or pass --access-token.");
|
|
5086
|
-
return 2;
|
|
5087
|
-
}
|
|
5088
4896
|
let config;
|
|
5089
4897
|
try {
|
|
5090
4898
|
const raw = await fs3.readFile(args.file, "utf-8");
|
|
@@ -5121,9 +4929,11 @@ async function runCommand(args) {
|
|
|
5121
4929
|
printError(`Invalid suite JSON: ${err.message}`);
|
|
5122
4930
|
return 2;
|
|
5123
4931
|
}
|
|
5124
|
-
let resolvedRemotePlatform = null;
|
|
5125
4932
|
try {
|
|
5126
|
-
|
|
4933
|
+
const platform = resolveRemotePlatformConfig(config);
|
|
4934
|
+
if (platform) {
|
|
4935
|
+
config.connection.platform = platform;
|
|
4936
|
+
}
|
|
5127
4937
|
} catch (err) {
|
|
5128
4938
|
printError(err.message);
|
|
5129
4939
|
return 2;
|
|
@@ -5138,31 +4948,9 @@ async function runCommand(args) {
|
|
|
5138
4948
|
}
|
|
5139
4949
|
printInfo("Submitting run\u2026");
|
|
5140
4950
|
let submitResult;
|
|
5141
|
-
let activeAccessToken = accessToken;
|
|
5142
|
-
const configConnection = config;
|
|
5143
|
-
async function prepareConfigForSubmit(currentAccessToken) {
|
|
5144
|
-
if (!configConnection.connection) return;
|
|
5145
|
-
if (resolvedRemotePlatform) {
|
|
5146
|
-
const ensured = await ensurePlatformConnection(currentAccessToken, resolvedRemotePlatform);
|
|
5147
|
-
configConnection.connection.platform_connection_id = ensured.platform_connection_id;
|
|
5148
|
-
}
|
|
5149
|
-
if ("platform" in configConnection.connection) {
|
|
5150
|
-
delete configConnection.connection.platform;
|
|
5151
|
-
}
|
|
5152
|
-
}
|
|
5153
|
-
async function submitPrepared(currentAccessToken) {
|
|
5154
|
-
await prepareConfigForSubmit(currentAccessToken);
|
|
5155
|
-
const res = await apiFetch("/runs/submit", currentAccessToken, {
|
|
5156
|
-
method: "POST",
|
|
5157
|
-
body: JSON.stringify({
|
|
5158
|
-
config,
|
|
5159
|
-
agent_session_id: args.session
|
|
5160
|
-
})
|
|
5161
|
-
});
|
|
5162
|
-
return res.json();
|
|
5163
|
-
}
|
|
5164
4951
|
try {
|
|
5165
|
-
|
|
4952
|
+
const res = await submitRun({ config, agent_session_id: args.session });
|
|
4953
|
+
submitResult = await res.json();
|
|
5166
4954
|
} catch (err) {
|
|
5167
4955
|
if (err instanceof ApiError && err.status === 401) {
|
|
5168
4956
|
printError("Access token rejected. Run `npx vent-hq login` to re-authenticate.");
|
|
@@ -5180,14 +4968,13 @@ async function runCommand(args) {
|
|
|
5180
4968
|
printInfo(`Streaming results for run ${run_id}\u2026`);
|
|
5181
4969
|
const abortController = new AbortController();
|
|
5182
4970
|
let exitCode = 0;
|
|
5183
|
-
|
|
5184
|
-
let runCompleteData = null;
|
|
4971
|
+
let runOutput = null;
|
|
5185
4972
|
let stopRequested = false;
|
|
5186
4973
|
let stopPromise = null;
|
|
5187
4974
|
const onSignal = () => {
|
|
5188
4975
|
if (!stopRequested) {
|
|
5189
4976
|
stopRequested = true;
|
|
5190
|
-
stopPromise = apiFetch(`/runs/${run_id}/stop`,
|
|
4977
|
+
stopPromise = apiFetch(`/runs/${run_id}/stop`, {
|
|
5191
4978
|
method: "POST",
|
|
5192
4979
|
signal: AbortSignal.timeout(5e3)
|
|
5193
4980
|
}).catch(() => {
|
|
@@ -5198,20 +4985,10 @@ async function runCommand(args) {
|
|
|
5198
4985
|
process.on("SIGINT", onSignal);
|
|
5199
4986
|
process.on("SIGTERM", onSignal);
|
|
5200
4987
|
try {
|
|
5201
|
-
for await (const event of streamRunEvents(run_id,
|
|
5202
|
-
const meta = event.metadata_json ?? {};
|
|
4988
|
+
for await (const event of streamRunEvents(run_id, abortController.signal)) {
|
|
5203
4989
|
printEvent(event);
|
|
5204
|
-
if (event.event_type === "call_completed") {
|
|
5205
|
-
callResults.push(event);
|
|
5206
|
-
}
|
|
5207
4990
|
if (event.event_type === "run_complete") {
|
|
5208
|
-
|
|
5209
|
-
const anyCallErrored = callResults.some((e) => {
|
|
5210
|
-
const callMeta = e.metadata_json ?? {};
|
|
5211
|
-
const callStatus = callMeta["status"];
|
|
5212
|
-
return callStatus === "error";
|
|
5213
|
-
});
|
|
5214
|
-
exitCode = anyCallErrored ? 1 : 0;
|
|
4991
|
+
runOutput = event.result ?? null;
|
|
5215
4992
|
}
|
|
5216
4993
|
if (event.event_type === "error") {
|
|
5217
4994
|
printError(event.message ?? "Stream connection lost");
|
|
@@ -5227,34 +5004,22 @@ async function runCommand(args) {
|
|
|
5227
5004
|
process.off("SIGINT", onSignal);
|
|
5228
5005
|
process.off("SIGTERM", onSignal);
|
|
5229
5006
|
}
|
|
5230
|
-
if (
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
printWarn("Verbose result fetch failed; falling back to streamed summary.");
|
|
5238
|
-
}
|
|
5007
|
+
if (!runOutput && !stopRequested) {
|
|
5008
|
+
try {
|
|
5009
|
+
const res = await apiFetch(`/runs/${run_id}`);
|
|
5010
|
+
const stored = await res.json();
|
|
5011
|
+
if (["pass", "fail", "cancelled"].includes(String(stored.status))) runOutput = stored;
|
|
5012
|
+
} catch {
|
|
5013
|
+
printWarn("Run result fetch failed.");
|
|
5239
5014
|
}
|
|
5240
|
-
printSummary(callResults, runCompleteData, run_id, {
|
|
5241
|
-
verbose: args.verbose,
|
|
5242
|
-
rawCalls: Array.isArray(rawRunDetails?.["results"]) ? rawRunDetails["results"] : void 0,
|
|
5243
|
-
runDetails: rawRunDetails ? {
|
|
5244
|
-
created_at: rawRunDetails["created_at"],
|
|
5245
|
-
started_at: rawRunDetails["started_at"],
|
|
5246
|
-
finished_at: rawRunDetails["finished_at"],
|
|
5247
|
-
duration_ms: rawRunDetails["duration_ms"],
|
|
5248
|
-
error_text: rawRunDetails["error_text"],
|
|
5249
|
-
aggregate: rawRunDetails["aggregate"]
|
|
5250
|
-
} : void 0
|
|
5251
|
-
});
|
|
5252
5015
|
}
|
|
5253
|
-
if (
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
}
|
|
5016
|
+
if (runOutput) {
|
|
5017
|
+
exitCode = runOutput.status === "pass" ? 0 : 1;
|
|
5018
|
+
printRunOutput(runOutput);
|
|
5019
|
+
const savedPath = await saveRunHistory(runOutput);
|
|
5020
|
+
if (savedPath) printInfo(`Run saved to ${savedPath}`);
|
|
5021
|
+
} else if (exitCode === 0) {
|
|
5022
|
+
exitCode = 2;
|
|
5258
5023
|
}
|
|
5259
5024
|
if (stopPromise) {
|
|
5260
5025
|
await stopPromise;
|
|
@@ -5269,21 +5034,20 @@ import * as net from "node:net";
|
|
|
5269
5034
|
// src/lib/relay.ts
|
|
5270
5035
|
import { spawn } from "node:child_process";
|
|
5271
5036
|
|
|
5272
|
-
//
|
|
5037
|
+
// src/lib/relay-client.ts
|
|
5038
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
5039
|
+
function errorEventMessage(ev) {
|
|
5040
|
+
return ev.message;
|
|
5041
|
+
}
|
|
5273
5042
|
var RelayClient = class {
|
|
5274
5043
|
controlWs = null;
|
|
5275
5044
|
localConnections = /* @__PURE__ */ new Map();
|
|
5276
5045
|
config;
|
|
5277
5046
|
closed = false;
|
|
5278
5047
|
handlers = /* @__PURE__ */ new Map();
|
|
5279
|
-
_agentEnv = {};
|
|
5280
5048
|
constructor(config) {
|
|
5281
5049
|
this.config = config;
|
|
5282
5050
|
}
|
|
5283
|
-
/** Env vars received from Vent server to inject into the agent process. */
|
|
5284
|
-
get agentEnv() {
|
|
5285
|
-
return this._agentEnv;
|
|
5286
|
-
}
|
|
5287
5051
|
on(event, handler) {
|
|
5288
5052
|
const list = this.handlers.get(event) ?? [];
|
|
5289
5053
|
list.push(handler);
|
|
@@ -5294,17 +5058,12 @@ var RelayClient = class {
|
|
|
5294
5058
|
handler(...args);
|
|
5295
5059
|
}
|
|
5296
5060
|
}
|
|
5297
|
-
async connect(
|
|
5298
|
-
if (!this.config.sessionId && !this.config.runId) {
|
|
5299
|
-
throw new Error("RelayClient.connect() requires either sessionId or runId");
|
|
5300
|
-
}
|
|
5061
|
+
async connect() {
|
|
5301
5062
|
const wsBase = this.config.apiUrl.replace(/^http/, "ws");
|
|
5302
|
-
const
|
|
5303
|
-
|
|
5304
|
-
return new Promise((resolve2, reject) => {
|
|
5063
|
+
const controlUrl = `${wsBase}/relay/control?session_id=${this.config.sessionId}&token=${this.config.relayToken}`;
|
|
5064
|
+
return new Promise((resolve, reject) => {
|
|
5305
5065
|
const ws = new WebSocket(controlUrl);
|
|
5306
5066
|
ws.binaryType = "arraybuffer";
|
|
5307
|
-
let configReceived = false;
|
|
5308
5067
|
let settled = false;
|
|
5309
5068
|
const settle = (fn) => {
|
|
5310
5069
|
if (!settled) {
|
|
@@ -5316,48 +5075,28 @@ var RelayClient = class {
|
|
|
5316
5075
|
const hardTimeout = setTimeout(() => {
|
|
5317
5076
|
settle(() => {
|
|
5318
5077
|
ws.close();
|
|
5319
|
-
reject(new Error(`Relay connection timed out after ${
|
|
5078
|
+
reject(new Error(`Relay connection timed out after ${CONNECT_TIMEOUT_MS}ms \u2014 check network connectivity to ${this.config.apiUrl}`));
|
|
5320
5079
|
});
|
|
5321
|
-
},
|
|
5080
|
+
}, CONNECT_TIMEOUT_MS);
|
|
5322
5081
|
ws.addEventListener("open", () => {
|
|
5323
5082
|
this.controlWs = ws;
|
|
5324
5083
|
this.setupControlHandlers(ws);
|
|
5325
|
-
this.emit("connected");
|
|
5326
5084
|
});
|
|
5327
|
-
this.on("
|
|
5328
|
-
|
|
5329
|
-
settle(() =>
|
|
5085
|
+
this.on("ready", () => settle(() => resolve()));
|
|
5086
|
+
ws.addEventListener("close", (ev) => {
|
|
5087
|
+
settle(() => reject(new Error(`Relay rejected the connection (${ev.code}${ev.reason ? `: ${ev.reason}` : ""})`)));
|
|
5330
5088
|
});
|
|
5331
|
-
setTimeout(() => {
|
|
5332
|
-
if (!configReceived && this.controlWs) settle(() => resolve2());
|
|
5333
|
-
}, 3e3);
|
|
5334
5089
|
ws.addEventListener("error", (ev) => {
|
|
5335
5090
|
if (!this.controlWs) {
|
|
5336
|
-
settle(() => reject(new Error(`Failed to connect to relay: ${ev
|
|
5091
|
+
settle(() => reject(new Error(`Failed to connect to relay: ${errorEventMessage(ev) ?? "connection error"}`)));
|
|
5337
5092
|
}
|
|
5338
5093
|
});
|
|
5339
5094
|
});
|
|
5340
5095
|
}
|
|
5341
|
-
async activate(timeoutMs = 15e3) {
|
|
5342
|
-
if (!this.config.runId) {
|
|
5343
|
-
throw new Error("activate() requires runId");
|
|
5344
|
-
}
|
|
5345
|
-
const activateUrl = `${this.config.apiUrl}/internal/runs/${this.config.runId}/activate`;
|
|
5346
|
-
const response = await fetch(activateUrl, {
|
|
5347
|
-
method: "POST",
|
|
5348
|
-
headers: { "Content-Type": "application/json" },
|
|
5349
|
-
body: JSON.stringify({ relay_token: this.config.relayToken }),
|
|
5350
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
5351
|
-
});
|
|
5352
|
-
if (!response.ok) {
|
|
5353
|
-
const text = await response.text();
|
|
5354
|
-
throw new Error(`Activation failed (${response.status}): ${text}`);
|
|
5355
|
-
}
|
|
5356
|
-
}
|
|
5357
5096
|
async disconnect() {
|
|
5358
5097
|
this.closed = true;
|
|
5359
|
-
for (const [connId,
|
|
5360
|
-
if (
|
|
5098
|
+
for (const [connId, local] of this.localConnections) {
|
|
5099
|
+
if (local.readyState !== WebSocket.CLOSED) local.close();
|
|
5361
5100
|
this.localConnections.delete(connId);
|
|
5362
5101
|
}
|
|
5363
5102
|
if (this.controlWs) {
|
|
@@ -5390,12 +5129,12 @@ var RelayClient = class {
|
|
|
5390
5129
|
if (frameType !== 1 && frameType !== 2) return;
|
|
5391
5130
|
const connId = new TextDecoder().decode(data.subarray(1, 37));
|
|
5392
5131
|
const payload = data.subarray(37);
|
|
5393
|
-
const
|
|
5394
|
-
if (
|
|
5132
|
+
const local = this.localConnections.get(connId);
|
|
5133
|
+
if (local?.readyState === WebSocket.OPEN) {
|
|
5395
5134
|
if (frameType === 2) {
|
|
5396
|
-
|
|
5135
|
+
local.send(new TextDecoder().decode(payload));
|
|
5397
5136
|
} else {
|
|
5398
|
-
|
|
5137
|
+
local.send(payload);
|
|
5399
5138
|
}
|
|
5400
5139
|
}
|
|
5401
5140
|
return;
|
|
@@ -5403,17 +5142,14 @@ var RelayClient = class {
|
|
|
5403
5142
|
try {
|
|
5404
5143
|
const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
|
5405
5144
|
const msg = JSON.parse(raw);
|
|
5406
|
-
if (msg.type === "config"
|
|
5407
|
-
this.
|
|
5408
|
-
this.emit("config_received");
|
|
5145
|
+
if (msg.type === "config") {
|
|
5146
|
+
this.emit("ready");
|
|
5409
5147
|
} else if (msg.type === "new_connection" && msg.conn_id) {
|
|
5410
5148
|
this.handleNewConnection(msg.conn_id);
|
|
5411
5149
|
} else if (msg.type === "close" && msg.conn_id) {
|
|
5412
|
-
const
|
|
5413
|
-
if (
|
|
5150
|
+
const local = this.localConnections.get(msg.conn_id);
|
|
5151
|
+
if (local && local.readyState !== WebSocket.CLOSED) local.close();
|
|
5414
5152
|
this.localConnections.delete(msg.conn_id);
|
|
5415
|
-
} else if (msg.type === "run_complete") {
|
|
5416
|
-
this.emit("run_complete");
|
|
5417
5153
|
}
|
|
5418
5154
|
} catch {
|
|
5419
5155
|
}
|
|
@@ -5429,62 +5165,48 @@ var RelayClient = class {
|
|
|
5429
5165
|
}
|
|
5430
5166
|
});
|
|
5431
5167
|
ws.addEventListener("error", (ev) => {
|
|
5432
|
-
this.emit("error", new Error(ev
|
|
5168
|
+
this.emit("error", new Error(errorEventMessage(ev) ?? "WebSocket error"));
|
|
5433
5169
|
});
|
|
5434
5170
|
}
|
|
5435
5171
|
handleNewConnection(connId) {
|
|
5436
|
-
const
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
this.
|
|
5445
|
-
}
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
this.localConnections.delete(connId);
|
|
5460
|
-
this.sendControlMessage({ type: "close", conn_id: connId });
|
|
5461
|
-
};
|
|
5462
|
-
localWs.addEventListener("close", () => cleanup("close"));
|
|
5463
|
-
localWs.addEventListener("error", (ev) => {
|
|
5464
|
-
const msg = ev.message ?? "unknown error";
|
|
5465
|
-
this.emit("log", `[relay] local WS error for ${connId}: ${msg}`);
|
|
5466
|
-
cleanup(`error: ${msg}`);
|
|
5467
|
-
});
|
|
5468
|
-
} catch (err) {
|
|
5469
|
-
this.emit("log", `[relay] Failed to connect local agent for ${connId}: ${err}`);
|
|
5470
|
-
}
|
|
5172
|
+
const localWs = new WebSocket(`ws://localhost:${this.config.agentPort}`);
|
|
5173
|
+
localWs.binaryType = "arraybuffer";
|
|
5174
|
+
localWs.addEventListener("open", () => {
|
|
5175
|
+
this.sendControlMessage({ type: "open_ack", conn_id: connId });
|
|
5176
|
+
this.localConnections.set(connId, localWs);
|
|
5177
|
+
});
|
|
5178
|
+
localWs.addEventListener("message", (event) => {
|
|
5179
|
+
if (event.data instanceof ArrayBuffer) {
|
|
5180
|
+
this.sendDataFrame(connId, new Uint8Array(event.data), 1);
|
|
5181
|
+
} else {
|
|
5182
|
+
this.sendDataFrame(connId, new TextEncoder().encode(event.data), 2);
|
|
5183
|
+
}
|
|
5184
|
+
});
|
|
5185
|
+
let cleaned = false;
|
|
5186
|
+
const cleanup = () => {
|
|
5187
|
+
if (cleaned) return;
|
|
5188
|
+
cleaned = true;
|
|
5189
|
+
if (localWs.readyState !== WebSocket.CLOSED) localWs.close();
|
|
5190
|
+
this.localConnections.delete(connId);
|
|
5191
|
+
this.sendControlMessage({ type: "close", conn_id: connId });
|
|
5192
|
+
};
|
|
5193
|
+
localWs.addEventListener("close", cleanup);
|
|
5194
|
+
localWs.addEventListener("error", cleanup);
|
|
5471
5195
|
}
|
|
5472
5196
|
};
|
|
5473
5197
|
|
|
5474
5198
|
// src/lib/relay.ts
|
|
5475
5199
|
async function startAgentSession(relayConfig) {
|
|
5476
|
-
const
|
|
5200
|
+
const client = new RelayClient({
|
|
5477
5201
|
apiUrl: relayConfig.api_url,
|
|
5478
5202
|
sessionId: relayConfig.session_id,
|
|
5479
5203
|
relayToken: relayConfig.relay_token,
|
|
5480
|
-
agentPort: relayConfig.agent_port
|
|
5481
|
-
|
|
5482
|
-
};
|
|
5483
|
-
const client = new RelayClient(clientConfig);
|
|
5204
|
+
agentPort: relayConfig.agent_port
|
|
5205
|
+
});
|
|
5484
5206
|
await client.connect();
|
|
5485
5207
|
let agentProcess = null;
|
|
5486
5208
|
if (relayConfig.start_command) {
|
|
5487
|
-
const env = { ...process.env,
|
|
5209
|
+
const env = { ...process.env, PORT: String(relayConfig.agent_port) };
|
|
5488
5210
|
agentProcess = spawn(relayConfig.start_command, {
|
|
5489
5211
|
shell: true,
|
|
5490
5212
|
stdio: "pipe",
|
|
@@ -5496,8 +5218,6 @@ async function startAgentSession(relayConfig) {
|
|
|
5496
5218
|
process.stdout.write(`Agent process error: ${err.message}
|
|
5497
5219
|
`);
|
|
5498
5220
|
});
|
|
5499
|
-
}
|
|
5500
|
-
if (relayConfig.start_command) {
|
|
5501
5221
|
await waitForHealth(relayConfig.agent_port, relayConfig.health_endpoint);
|
|
5502
5222
|
}
|
|
5503
5223
|
const cleanup = async () => {
|
|
@@ -5524,11 +5244,6 @@ async function waitForHealth(port, endpoint, timeoutMs = 3e4) {
|
|
|
5524
5244
|
|
|
5525
5245
|
// src/commands/agent.ts
|
|
5526
5246
|
async function agentStartCommand(args) {
|
|
5527
|
-
const accessToken = await loadAccessToken();
|
|
5528
|
-
if (!accessToken) {
|
|
5529
|
-
printError("No Vent access token found. Set VENT_ACCESS_TOKEN, run `npx vent-hq login`, or pass --access-token.");
|
|
5530
|
-
return 2;
|
|
5531
|
-
}
|
|
5532
5247
|
let parsedConfig;
|
|
5533
5248
|
try {
|
|
5534
5249
|
if (args.file) {
|
|
@@ -5557,7 +5272,7 @@ async function agentStartCommand(args) {
|
|
|
5557
5272
|
const closeSession = async () => {
|
|
5558
5273
|
if (!session) return;
|
|
5559
5274
|
try {
|
|
5560
|
-
await apiFetch(`/agent-sessions/${session.session_id}/close`,
|
|
5275
|
+
await apiFetch(`/agent-sessions/${session.session_id}/close`, {
|
|
5561
5276
|
method: "POST"
|
|
5562
5277
|
});
|
|
5563
5278
|
} catch {
|
|
@@ -5565,11 +5280,11 @@ async function agentStartCommand(args) {
|
|
|
5565
5280
|
};
|
|
5566
5281
|
try {
|
|
5567
5282
|
printInfo("Creating agent session\u2026");
|
|
5568
|
-
const createRes = await apiFetch("/agent-sessions",
|
|
5283
|
+
const createRes = await apiFetch("/agent-sessions", {
|
|
5569
5284
|
method: "POST",
|
|
5570
5285
|
body: JSON.stringify({ config: connection })
|
|
5571
5286
|
});
|
|
5572
|
-
session = await createRes.json();
|
|
5287
|
+
session = AgentSessionResponseSchema.parse(await createRes.json());
|
|
5573
5288
|
printInfo("Connecting relay for local agent\u2026");
|
|
5574
5289
|
relay = await startAgentSession(session);
|
|
5575
5290
|
const sessionData = {
|
|
@@ -5595,13 +5310,8 @@ async function agentStartCommand(args) {
|
|
|
5595
5310
|
}
|
|
5596
5311
|
}
|
|
5597
5312
|
async function agentStopCommand(args) {
|
|
5598
|
-
const accessToken = await loadAccessToken();
|
|
5599
|
-
if (!accessToken) {
|
|
5600
|
-
printError("No Vent access token found. Set VENT_ACCESS_TOKEN, run `npx vent-hq login`, or pass --access-token.");
|
|
5601
|
-
return 2;
|
|
5602
|
-
}
|
|
5603
5313
|
try {
|
|
5604
|
-
await apiFetch(`/agent-sessions/${args.sessionId}/close`,
|
|
5314
|
+
await apiFetch(`/agent-sessions/${args.sessionId}/close`, {
|
|
5605
5315
|
method: "POST"
|
|
5606
5316
|
});
|
|
5607
5317
|
printSuccess(`Agent session closed: ${args.sessionId}`, { force: true });
|
|
@@ -5612,7 +5322,7 @@ async function agentStopCommand(args) {
|
|
|
5612
5322
|
}
|
|
5613
5323
|
}
|
|
5614
5324
|
async function waitForSessionExit(relay) {
|
|
5615
|
-
return new Promise((
|
|
5325
|
+
return new Promise((resolve) => {
|
|
5616
5326
|
let settled = false;
|
|
5617
5327
|
const agentProcess = relay.agentProcess;
|
|
5618
5328
|
const finish = (code) => {
|
|
@@ -5623,7 +5333,7 @@ async function waitForSessionExit(relay) {
|
|
|
5623
5333
|
if (agentProcess) {
|
|
5624
5334
|
agentProcess.off("exit", handleAgentExit);
|
|
5625
5335
|
}
|
|
5626
|
-
|
|
5336
|
+
resolve(code);
|
|
5627
5337
|
};
|
|
5628
5338
|
const handleSignal = () => finish(0);
|
|
5629
5339
|
const handleAgentExit = (code, signal) => {
|
|
@@ -5652,12 +5362,12 @@ async function waitForSessionExit(relay) {
|
|
|
5652
5362
|
});
|
|
5653
5363
|
}
|
|
5654
5364
|
function findFreePort() {
|
|
5655
|
-
return new Promise((
|
|
5365
|
+
return new Promise((resolve, reject) => {
|
|
5656
5366
|
const server = net.createServer();
|
|
5657
5367
|
server.listen(0, () => {
|
|
5658
5368
|
const addr = server.address();
|
|
5659
5369
|
const port = addr.port;
|
|
5660
|
-
server.close(() =>
|
|
5370
|
+
server.close(() => resolve(port));
|
|
5661
5371
|
});
|
|
5662
5372
|
server.on("error", reject);
|
|
5663
5373
|
});
|
|
@@ -5706,10 +5416,11 @@ async function deviceAuthFlow() {
|
|
|
5706
5416
|
});
|
|
5707
5417
|
if (!res.ok) continue;
|
|
5708
5418
|
const data = await res.json();
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5419
|
+
if (data.status === "approved") {
|
|
5420
|
+
const parsed = AccessCredentialsSchema.safeParse(data);
|
|
5421
|
+
if (!parsed.success) return { ok: false, error: "Vent returned invalid sign-in credentials." };
|
|
5422
|
+
await saveCredentials(parsed.data);
|
|
5423
|
+
return { ok: true };
|
|
5713
5424
|
}
|
|
5714
5425
|
if (data.status === "expired") {
|
|
5715
5426
|
return { ok: false, error: "Session expired. Run `npx vent-hq login` again." };
|
|
@@ -5728,7 +5439,7 @@ async function loginCommand(args) {
|
|
|
5728
5439
|
if (args.status) {
|
|
5729
5440
|
const token = await loadAccessToken();
|
|
5730
5441
|
if (token) {
|
|
5731
|
-
printSuccess(
|
|
5442
|
+
printSuccess("Logged in.", { force: true });
|
|
5732
5443
|
return 0;
|
|
5733
5444
|
}
|
|
5734
5445
|
printInfo("Not logged in. Run `npx vent-hq login`.", { force: true });
|
|
@@ -5736,9 +5447,10 @@ async function loginCommand(args) {
|
|
|
5736
5447
|
}
|
|
5737
5448
|
const result = await deviceAuthFlow();
|
|
5738
5449
|
if (result.ok) {
|
|
5739
|
-
printSuccess("Logged in! Vent
|
|
5450
|
+
printSuccess("Logged in! Vent credentials saved to ~/.vent/credentials", { force: true });
|
|
5740
5451
|
return 0;
|
|
5741
5452
|
}
|
|
5453
|
+
printError(result.error);
|
|
5742
5454
|
return 1;
|
|
5743
5455
|
}
|
|
5744
5456
|
|
|
@@ -5771,17 +5483,47 @@ function detectGitHubToken() {
|
|
|
5771
5483
|
import * as fs5 from "node:fs/promises";
|
|
5772
5484
|
import * as path3 from "node:path";
|
|
5773
5485
|
|
|
5486
|
+
// src/skills/body.md
|
|
5487
|
+
var body_default = '# Vent - Voice Agent Calls\n\n## When to Test\n\nVent is the voice CI gate. Auto-trigger a call after any non-trivial voice-affecting change \u2014 don\'t wait to be asked. Non-trivial means anything that affects what the caller hears, how the agent responds, what tools are called, how the call routes, or audio timing: prompt rewrites or sections (not one-word tweaks), tool schema or handler changes, state/memory logic, transfer logic, provider/audio config, platform integration, and any voice-behavior bug fix. One-line copy or comment edits don\'t need a call.\n\nBefore launching, scan `.vent/runs/` for unresolved issues from prior runs (failing status, regressions, warnings, latency jumps); fix in-scope ones as part of this change and verify they\'re resolved by the new run, surface out-of-scope ones rather than silently leaving them. The task is not done until you\'ve reviewed the transcript, tool calls, and observed behavior and judged that the change works \u2014 Vent\'s `status` and exit code only reflect pipeline completion, not mission success.\n\n## Autonomous Iteration\n\n**Default: iterate.** Run a fix \u2192 Vent-call \u2192 fix \u2192 Vent-call loop whenever a call surfaces a fixable issue. Don\'t report a failure (or a degraded-quality success) back to the user before attempting to fix it \u2014 they want the agent working, not a diagnosis. After each call, if the result reveals a problem you can fix with a concrete plan, apply the fix and run again. Fixable issues include: tool schema or registry gaps, missing handlers, prompt phrasing, **STT vocabulary mismatches** (e.g. domain terms misheard \u2014 Deepgram `keyterms`, custom vocabulary, prompt-level term lists), **latency tunables** (endpointing thresholds, preemptive generation, model selection), and audio quality config. If you\'d be guessing at the next attempt, stop and ask the user instead of trying.\n\n**"Mission success but with quality problems" still requires iteration.** If the call ran end-to-end but had noticeably bad latency, STT mishearing the agent\'s domain vocabulary, audio artifacts, or any other caller-experience degradation that has a concrete fix, the call is *not done* \u2014 apply the fix and re-run. Don\'t downgrade fixable problems to "observations" or "things worth flagging." The pipeline running cleanly is the floor, not the ceiling.\n\n**Never ask permission to iterate.** "Want me to apply this and re-run?" / "Should I fix it?" / "Let me know if you want me to..." \u2014 these are skill violations. If you have a concrete fix, apply it and run the next call. The user opts out by saying so explicitly; silence is not opt-out.\n\nStop iterating and report when one of:\n\n- The next call confirms the fix worked AND no further fixable issues remain (success \u2014 done).\n- The same fix fails twice (your hypothesis is wrong; ask the user).\n- The failure mode shifts between attempts (you\'re chasing a moving target).\n- You can\'t justify the next call against its provider cost \u2014 each call spends real money and provider quota.\n\nIf the user explicitly opts out ("just report", "don\'t iterate", "stop after the first call", "I\'ll fix it myself"), respect that \u2014 run once, report, stop. Otherwise iterate by default. The first time you start an autonomous loop in a session, mention it once so the user knows it\'s happening (e.g. "the call surfaced X \u2014 fixing and re-running; I\'ll report back when it converges or stops making progress").\n\n{{EXECUTION}}\n\n## Workflow\n\n1. Identify the behavior under test. Read enough of the agent codebase to understand its system prompt, tools, handlers, routes, provider config, platform wiring, and expected handoffs.\n2. Reuse an existing `.vent/suite.<adapter>.json` when possible. If `.vent/` contains multiple suites, inspect `connection.adapter` and report which suite file produced the result.\n3. Create or update a suite only when the existing calls do not cover the changed behavior. Name calls after real flows, for example `reschedule-appointment`, not `call-1`.\n4. If the suite uses `start_command`, start one shared local session first with `npx vent-hq agent start -f .vent/suite.<adapter>.json`, then pass `--session <session-id>` to each run.\n\n **For locally-run LiveKit agents: every run requires killing *all* workers, starting one fresh worker, and waiting a full 60 seconds before submitting.** Unconditional \u2014 LiveKit Cloud round-robins across registered workers, so a single survivor with a dead inference subprocess fails ~N-1 of N calls. Don\'t rely on `pkill -f <path-pattern>`; bare command lines like `node --import tsx agent.ts dev` won\'t match a path filter. Use `ps aux | grep -E "node.*agent\\.ts|@livekit/agents.*ipc"`, `kill -9` by PID, re-run `ps` to confirm zero survivors, then start the fresh worker. Skipping the 60s wait fails with `did not publish audio track`; if that error appears alongside `Error [ERR_IPC_CHANNEL_CLOSED] from InferenceProcExecutor.doInference` in the agent log right after a "running EOU detection" line, that\'s a straggler \u2014 redo the kill sweep. Hosted LiveKit Cloud agents don\'t need any of this; run normally.{{LIVEKIT_NOTE}}\n5. Pick which call(s) to run based on the change. Fixed bug: replay the failing scenario. Changed tool: include a call that triggers that tool. Prompt or routing change: include the relevant happy path and any important edge path.\n6. Compare against the previous JSON in `.vent/runs/` when validating a fix or regression. Check status flips, latency jumps, tool-call success drops, cost jumps, and transcript divergence. Correlate with `git diff` between saved `git_sha` values when available; skip if no previous run exists.\n\n## Commands\n\n```bash\nnpx vent-hq init # First-time setup (auth + skill install + starter suite)\nnpx vent-hq login # Log in to existing account\nnpx vent-hq login --status # Print whether credentials are present\nnpx vent-hq logout # Remove saved credentials from ~/.vent/credentials\nnpx vent-hq run -f .vent/suite.X.json # Run a single-call suite\nnpx vent-hq run -f .vent/suite.X.json --call NAME # Run one named call from a multi-call suite\nnpx vent-hq run ... --session <session-id> # Add to any run; routes through an existing local relay session\nnpx vent-hq stop <run-id> # Cancel a queued or running run\nnpx vent-hq agent start -f .vent/suite.X.json # Start a shared local relay session\nnpx vent-hq agent stop <session-id> # Stop a shared local relay session\n```\n\nIf `~/.vent/credentials` is missing and neither `VENT_REFRESH_TOKEN` nor `VENT_ACCESS_TOKEN` is set, run `npx vent-hq init`. For an existing account, run `npx vent-hq login` or set `VENT_REFRESH_TOKEN` to an API key. Access JWTs renew automatically; `VENT_ACCESS_TOKEN` alone is short-lived.\n\n## Suite Config\n\nSuites live in `.vent/suite.<adapter>.json`. `connection` is declared once per suite. `calls` is a named map, and each key becomes the call name used with `--call`.\n\nLocal websocket suite:\n\n```json\n{\n "connection": {\n "adapter": "websocket",\n "start_command": "npm run start",\n "health_endpoint": "/health",\n "agent_port": 3001\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nPlatform-direct suite:\n\n```json\n{\n "connection": {\n "adapter": "vapi",\n "platform": { "provider": "vapi" }\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nWrite `caller_prompt` as a realistic caller with a name, goal, mood, constraints, and conditional behavior. Set `max_turns` based on flow complexity: FAQ `4-6`, booking or tool use `8-12`, complex flows `12-20`.\n\nCall fields:\n\n- `caller_prompt` is required. `max_turns` defaults to `6`.\n- `persona` supports `pace`, `clarity`, `disfluencies`, `cooperation`, `emotion`, `memory`, `intent_clarity`, and `confirmation_style`.\n- `caller_audio` supports noise, speed, speakerphone, mic distance, clarity, accent, packet loss, and jitter.\n- `caller_audio.accent` picks the caller\'s voice. Supported: `american` (default), `british`, `australian`, `filipino`, `spanish_mexican`, `spanish_peninsular`, `spanish_colombian`, `spanish_argentine`, `german`, `french`, `italian`, `dutch`, `japanese`. Any other value is rejected. The non-English accents are voices in that language: also set `language` (e.g. `"es"`) so the caller speaks it.\n- `language` is an ISO 639-1 code such as `en`, `es`, `fr`, `de`, `it`, `nl`, or `ja`.\n- `voice` is `"male"` or `"female"` (English only; default female). Use to flip the caller\'s perceived gender. Ignored if `caller_audio.accent` is set or `language` is non-English.\n\n## Connections and Credentials\n\n### Adapter choice\n\nUse `websocket` for your own local or hosted runtime. Use `start_command` for local agents or `agent_url` for hosted custom endpoints. For `start_command` and `agent_url`, do not put Deepgram, ElevenLabs, OpenAI, or other agent runtime keys into Vent config unless the Vent adapter itself needs them \u2014 the tested agent owns its own runtime credentials.\n\nUse `vapi`, `retell`, `elevenlabs`, `bland`, or `livekit` for platform-direct testing. In this mode Vent itself talks to the provider on the user\'s behalf.\n\nVent provides `DEEPGRAM_API_KEY` and `ANTHROPIC_API_KEY` for its hosted caller/evaluation stack \u2014 those are Vent\'s, not the tested agent\'s.\n\n### Credential resolution\n\nIn platform-direct mode the CLI auto-resolves credentials from `.env.local`, `.env`, and the current shell environment. If the project uses a secrets manager like Doppler, wrap the command so the secrets are set: `doppler run -- npx vent-hq run -f <suite.json>`. Do not run `source .env && export` before Vent commands. Keep credentials out of chat and suite files; the CLI sends the resolved configuration to Vent, which stores an encrypted copy with each run.\n\nAuto-resolved env vars and JSON fields:\n\n- Vapi: `VAPI_API_KEY` -> `vapi_api_key`; `VAPI_ASSISTANT_ID` or `VAPI_AGENT_ID` -> `vapi_assistant_id`\n- Bland: `BLAND_API_KEY` -> `bland_api_key`; `BLAND_PATHWAY_ID` -> `bland_pathway_id`; `BLAND_PERSONA_ID` -> `persona_id`\n- LiveKit: `LIVEKIT_API_KEY` -> `livekit_api_key`; `LIVEKIT_API_SECRET` -> `livekit_api_secret`; `LIVEKIT_URL` -> `livekit_url`\n- Retell: `RETELL_API_KEY` -> `retell_api_key`; `RETELL_AGENT_ID` -> `retell_agent_id`\n- ElevenLabs: `ELEVENLABS_API_KEY` -> `elevenlabs_api_key`; `ELEVENLABS_AGENT_ID` -> `elevenlabs_agent_id`\n\n### Provider config\n\nUse existing provider config when possible: Vapi assistant, Retell agent, ElevenLabs agent, Bland pathway, or LiveKit agent. Bland uniquely supports inline config \u2014 `platform` needs `bland_pathway_id` or an inline `task`, optionally with `persona_id` and voice, model, and turn-handling overrides (see Bland\'s API docs for the full field list).\n\n### Concurrency\n\nWhen you fan out multiple Vent calls in parallel against the same provider (for example, running several named calls from one suite at once{{PARALLEL_EXAMPLE}}), respect the provider\'s per-account concurrency limit. Exceeding it makes calls queue or fail at the provider \u2014 Vent does not enforce these caps for you.\n\nRecord the limit as `max_concurrency` in the suite\'s `platform` block so it\'s visible on future runs. Ask the user which plan they\'re on if sizing matters; otherwise use the conservative default in bold.\n\n- **Vapi**: **10** included per account; reserved lines can be purchased self-serve; Enterprise is unlimited.\n- **Retell**: Pay-as-you-go includes **20**; Enterprise has no cap.\n- **Bland**: Start=**10**, Build=50, Scale=100, Enterprise=unlimited.\n- **ElevenLabs**: Free=**4**, Starter=6, Creator=10, Pro=20, Scale=30, Business=30. Burst pricing can temporarily allow up to 3x base.\n- **LiveKit Cloud**: Build=**5**, Ship=20, Scale=50 managed inference sessions (the usual gate for voice agents); agent-session concurrency can go higher (Scale up to 600).\n\n## WebSocket\n\nFor `adapter: "websocket"`, Vent sends binary 16-bit mono PCM audio over one websocket connection. Websocket text frames are optional JSON events. Audio-only websocket agents still work, but events improve turn detection and observability. Vent sends `{"type":"end-call"}` when the test is done.\n\nUseful websocket text frames:\n\n```jsonc\n{"type":"speech-update","status":"started"}\n{"type":"speech-update","status":"stopped"}\n{"type":"tool_call","name":"check_availability","arguments":{},"result":{},"successful":true,"duration_ms":150}\n{"type":"vent:timing","stt_ms":120,"llm_ms":450,"tts_ms":80}\n{"type":"vent:session","platform":"custom","provider_call_id":"call_123","provider_session_id":"session_abc"}\n{"type":"vent:call-metadata","call_metadata":{"recording_url":"https://...","cost":{"total_usd":0.12,"breakdown":{"llm":0.07,"tts":0.05}}}}\n{"type":"vent:transcript","role":"caller","text":"I need to reschedule"}\n{"type":"vent:transcript","role":"agent","text":"Sure, which day works for you?"}\n{"type":"vent:transfer","destination":"+15551234567","status":"attempted"}\n{"type":"vent:debug-url","label":"trace","url":"https://..."}\n{"type":"vent:warning","message":"provider warning","code":"provider_warning"}\n```\n\nPlatform adapters capture tool calls automatically. Websocket agents must emit `tool_call` frames for tool observability. Platform adapters get component latency automatically. Websocket agents should emit `vent:timing` after each agent response when STT/LLM/TTS breakdown is available.\n\n## LiveKit\n\nBefore running LiveKit tests, install and add the Vent helper to the LiveKit agent entrypoint. Node: `npm install @vent-hq/livekit`, then call `instrumentLiveKitAgent({ ctx, session })`. Python: `pip install vent-livekit`, then call `instrument_livekit_agent(ctx=ctx, session=session)`.\n\nLiveKit direct mode requires the LiveKit Agents SDK. Custom LiveKit participants should use the websocket adapter with a relay. If the LiveKit agent registered with an explicit dispatch name, set `livekit_agent_name` in `platform`.\n\nLiveKit parallel calls are supported, but capacity depends on where the user runs their agent. LiveKit Cloud only routes audio \u2014 the agent code runs on the user\'s own infrastructure (laptop, Fly.io, Railway, k8s, etc.), and that box\'s CPU/RAM is the real bottleneck. Before firing parallel calls, derive `platform.max_concurrency` from the minimum of: (1) the user\'s LiveKit plan limit (Build=5, Ship=20, Scale=50+ concurrent agent sessions), (2) their agent worker box capacity (rough rule: 1c/1GB\u21921\u20133 jobs, 2c/4GB\u21925\u201310, 4c/8GB\u219210\u201325, 8c/16GB\u219225\u201350; multiply by number of worker boxes), and (3) their STT/TTS provider concurrency (e.g. Deepgram TTS streaming = 2). If the user wants more parallel calls than that minimum allows, push back \u2014 explain which limit is the bottleneck (plan / box / provider) and either reduce the count or suggest scaling that limit. Architecture: register one `agent_name` and run N workers under it (one big worker, or several smaller ones \u2014 same name); LiveKit\'s dispatcher round-robins jobs across them with built-in failover. Don\'t use numbered names (`agent-0`, `agent-1`) \u2014 that reinvents what the dispatcher does and breaks failover.\n\nUse the LiveKit helper for observability; do not publish `vent:*` topics manually. The helper captures SDK metrics, tool events, conversation items, and close events. Native LiveKit `lk.transcription` and `lk.agent.state` provide transcript and agent-state timing.\n\n## Output\n\n### Live result\n\n`npx vent-hq run` returns a single JSON result on stdout in non-TTY mode (not an SSE JSONL stream). Exit codes: `0` = the run passed (every call ran end to end); `1` = it didn\'t (a call errored, or the run failed or was cancelled); `2` = the result stream was lost. A call `error` starting with `Vent failed, agent not tested:` is a failure of Vent itself (its caller LLM, STT/TTS or worker): rerun, it says nothing about the agent. Any other `error` comes from the agent\'s platform or your configuration. `end_note` says how a completed call ended when Vent ended it or the connection closed (e.g. `No agent speech for 30s after the caller\'s turn; Vent hung up.`): read the transcript to judge whether that\'s an agent bug.\n\nMost result fields are always present; `latency`, `component_latency`, `signal_quality`, and `call_metadata` may be `null` when the underlying analysis didn\'t run; `warnings` is absent when there are none. Branch on null before reading nested fields. Each agent turn carries `tool_calls`: the tools it ran for that reply, with arguments, result, `successful` and `latency_ms`, so you can check what the agent said against what its tools returned. A tool the agent ran but never replied after is on the call\'s last turn. A call-level `tool_calls` list appears only for a failed call whose transcript was lost. Tool results are complete, never shortened. `component_latency` is the mean time of each stage of the agent\'s pipeline across the call (`mean_stt_ms`, `mean_llm_ms`, `mean_tts_ms`), as the platform reports it, so a slow stage stands out. For anything deeper, open the links in `call_metadata.provider_debug_urls`.\n\nVent\'s transcript is ground truth. Judge on semantic intent: ignore homophones and minor mis-hears (`"check teach hat"` for `"check that"`, missing question marks on short tails) \u2014 those are streaming-STT noise on Vent\'s caller side, not agent bugs, and **don\'t surface them in the report** (they\'re Vent-side artifacts, not actionable for the user). But clear gibberish or word-soup (e.g. `"Cristoxin"` where the agent should have said `"Of course, talk soon"`) is **not** a Vent artifact \u2014 Vent\'s STT does not invent words like that. It means the platform\'s TTS produced corrupted audio or the agent\'s STT/LLM generated the wrong text, and the fix lives there (TTS voice config, agent prompt, model temperature, codec issue). Missing or cut-off agent text, unlike word-soup, can come from Vent\'s STT stream dropping: check the recording before blaming the agent.\n\nThere are two transcripts. `transcript` is Vent\'s record: caller turns are exactly what Vent\'s caller said, agent turns are what Vent heard the agent say. `platform_transcript` is the agent platform\'s own record (`null` when the platform gives none): agent turns are everything the agent was set to say (its LLM\'s text plus fixed messages like the greeting or a tool\'s "one moment"), caller turns are what the agent\'s speech-to-text heard. On Retell and LiveKit, an agent turn the caller interrupted shows only what was spoken before the cut, since those platforms keep nothing else; on the others it shows the full text. Vapi never reports the text of its tool wait messages ("Just a sec"), so those are missing from Vapi\'s agent turns. Compare them turn by turn:\n- Agent wrote it right but Vent heard it differently on a number, ID, date or name (e.g. LLM wrote `555-0102`, Vent heard "five five five one two"): the agent\'s voice read it wrong. Fix how that text reaches the TTS (normalization, spelling or pronunciation tags), not the prompt\'s logic. If the agent\'s LLM text is already wrong, the fix is in the LLM, prompt or tool data.\n- Vent\'s caller said it right but the agent\'s STT heard something else (e.g. "OSHA" heard as "ocean"): the agent misheard. Fix the agent\'s STT (keyword hints, custom vocabulary).\n\n`latency` covers agent replies: `response_time_ms` (mean), `p50_response_time_ms`, `max_response_time_ms` (the slowest reply), and `first_response_time_ms`. `signal_quality` is the agent\'s raw audio: `mean_snr_db` (>20 good, <10 bad) and `max_clipping_ratio` (>0.01 = distortion). Bad values point at the agent\'s TTS or audio pipeline. `call_metadata.cost` is what the provider charged: `total_usd`, plus `breakdown` in the provider\'s own categories when the provider splits it (Bland and some agents report only the total). `call_metadata.provider_debug_urls` links to the call in the provider\'s dashboard or logs.\n\nFor transfers: `call_metadata.transfer_attempted` (provider claimed) and `transfer_completed` (from the platform\'s call data) can disagree \u2014 report both. `transfers[]` carries destination, type, and per-attempt status.\n\n### Saved history\n\nAfter every run, the CLI saves the call result to `.vent/runs/`. Shape:\n\n```jsonc\n{\n "run_id": "...",\n "timestamp": "2026-04-21T...Z",\n "git_sha": "...",\n "summary": { "status": "pass", "calls_total": 1, "total_duration_ms": 12345 },\n "call_results": [\n { "name": "happy-path", "status": "completed", "duration_ms": 6123, "transcript": [], "latency": { "response_time_ms": 420, "max_response_time_ms": 980 }, "call_metadata": { "cost": { "total_usd": 0.004 } } }\n ]\n}\n```\n\nWhen comparing against a prior run (Workflow step 6), inspect:\n\n- Run-completion status flips: `call_results[i].status` (pipeline-only \u2014 judge mission success from the transcript)\n- Latency: `call_results[i].latency.response_time_ms` (mean) or `latency.max_response_time_ms` increased >20%\n- Tool calls: a tool that succeeded before now fails (`successful: false`), or is no longer called, in the agent turns\' `tool_calls`\n- Cost: `call_results[i].call_metadata.cost.total_usd` increased >30% (`cost.breakdown` shows which part grew)\n- Transcript: `call_results[i].transcript` diverged in semantic content (ignore STT noise)\n\n## Reporting Results\n\nBefore reporting, read the agent\'s code to locate where the observed behavior originates. If the issue is small and you can fix it, fix it and explain what you did \u2014 don\'t ask permission first.\n\nAdapt the report shape to the call \u2014 a clean pass needs little, a regression with a multi-layer cause needs more. Use a transcript excerpt when it helps the user see what happened.\n\nHard rules:\n\n- Pair raw numbers with their plain-English meaning \u2014 don\'t drop the number, but don\'t leave it unexplained. E.g. "the slowest reply took 850ms, which is snappy and well within natural conversational pacing" or "the slowest reply hit 1.6 seconds with the LLM as the bottleneck \u2014 noticeably sluggish to a caller."\n- Name the user\'s voice agent by platform on first mention (e.g. "the Vapi agent responded snappily throughout") so the user knows immediately which agent the observation is about. After that, just say "the agent" \u2014 don\'t repeat the platform name on every line.\n- Always include the recording from `call_metadata.recording_url` as an inline `[Recording](url)` link, placed in **one block at the very end of the report** \u2014 never sprinkled through the prose. Single call: one link as the last line. Multi-call: one labeled link per call (e.g. `reschedule-appointment: [Recording](url)`). Never paste a bare URL.\n- Mission success is your judgment, not Vent\'s. The per-call `status` is only `"completed"` (pipeline ran) or `"error"` (pipeline failed); decide whether the agent actually accomplished the scenario from the transcript and tool calls.\n- Similar-sounding word substitutions (e.g. "ocean" for "OSHA") are STT ambiguity, not comprehension failure. The fix lives in STT keyword hints, custom vocabulary, or a prompt-level term list \u2014 not the agent\'s reasoning.\n- Surface only what the user can act on in their own agent\'s code or config \u2014 never Vent-side artifacts (caller wait modes, harness timing, internal pipeline quirks), or cost unless asked.\n\nFor multi-call runs, lead with your own judgment of what happened across the calls (e.g. "3 of 4 did what they were supposed to; `cancel-appointment` never actually canceled"), not a parroted pass/fail count. Then cover each call with whatever depth it needs.\n';
|
|
5488
|
+
|
|
5774
5489
|
// src/skills/claude-code.md
|
|
5775
|
-
var claude_code_default = '---\nname: vent\ndescription: Place real calls against the user\'s voice agent to verify recent code changes worked end-to-end. Trigger on test requests or after any non-trivial voice-agent change.\nallowed-tools: Bash(npx vent-hq *)\n---\n\n# Vent - Voice Agent Calls\n\n## When to Test\n\nVent is the voice CI gate. Auto-trigger a call after any non-trivial voice-affecting change \u2014 don\'t wait to be asked. Non-trivial means anything that affects what the caller hears, how the agent responds, what tools are called, how the call routes, or audio timing: prompt rewrites or sections (not one-word tweaks), tool schema or handler changes, state/memory logic, transfer logic, provider/audio config, platform integration, and any voice-behavior bug fix. One-line copy or comment edits don\'t need a call.\n\nBefore launching, scan `.vent/runs/` for unresolved issues from prior runs (failing status, regressions, warnings, latency jumps); fix in-scope ones as part of this change and verify they\'re resolved by the new run, surface out-of-scope ones rather than silently leaving them. The task is not done until you\'ve reviewed the transcript, tool calls, and observed behavior and judged that the change works \u2014 Vent\'s `status` and exit code only reflect pipeline completion, not mission success.\n\n## Autonomous Iteration\n\n**Default: iterate.** Run a fix \u2192 Vent-call \u2192 fix \u2192 Vent-call loop whenever a call surfaces a fixable issue. Don\'t report a failure (or a degraded-quality success) back to the user before attempting to fix it \u2014 they want the agent working, not a diagnosis. After each call, if the result reveals a problem you can fix with a concrete plan, apply the fix and run again. Fixable issues include: tool schema or registry gaps, missing handlers, prompt phrasing, **STT vocabulary mismatches** (e.g. domain terms misheard \u2014 Deepgram `keyterms`, custom vocabulary, prompt-level term lists), **latency tunables** (endpointing thresholds, preemptive generation, model selection), and audio quality config. If you\'d be guessing at the next attempt, stop and ask the user instead of trying.\n\n**"Mission success but with quality problems" still requires iteration.** If the call ran end-to-end but had noticeably bad latency, STT mishearing the agent\'s domain vocabulary, audio artifacts, or any other caller-experience degradation that has a concrete fix, the call is *not done* \u2014 apply the fix and re-run. Don\'t downgrade fixable problems to "observations" or "things worth flagging." The pipeline running cleanly is the floor, not the ceiling.\n\n**Never ask permission to iterate.** "Want me to apply this and re-run?" / "Should I fix it?" / "Let me know if you want me to..." \u2014 these are skill violations. If you have a concrete fix, apply it and run the next call. The user opts out by saying so explicitly; silence is not opt-out.\n\nStop iterating and report when one of:\n\n- The next call confirms the fix worked AND no further fixable issues remain (success \u2014 done).\n- The same fix fails twice (your hypothesis is wrong; ask the user).\n- The failure mode shifts between attempts (you\'re chasing a moving target).\n- You can\'t justify the next call against its provider cost \u2014 each call spends real money and provider quota.\n\nIf the user explicitly opts out ("just report", "don\'t iterate", "stop after the first call", "I\'ll fix it myself"), respect that \u2014 run once, report, stop. Otherwise iterate by default. The first time you start an autonomous loop in a session, mention it once so the user knows it\'s happening (e.g. "the call surfaced X \u2014 fixing and re-running; I\'ll report back when it converges or stops making progress").\n\n## Claude Code Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nClaude Code serializes separate Bash tool calls for `npx vent-hq ...`, so run multiple calls from one suite by invoking each named call with `--call <name>` in one Bash command using `&` and a final `wait`:\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path & \\\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path & \\\nwait\n```\n\nFor long-running composite commands \u2014 e.g. LiveKit\'s required `kill workers \u2192 start fresh worker \u2192 wait 60s \u2192 npx vent-hq run` preflight \u2014 keep the entire sequence in **one foreground Bash call**. Do not use `Monitor` or `run_in_background: true` for the wait. Both end the turn until an event fires, which means Vent never sees an inflight call and the user reads it as "you backgrounded the test." The harness also blocks bare `sleep 60` (any long leading sleep), so fold the readiness check and the wait into a single `until`-loop \u2014 short sleeps inside a polling loop are allowed because each iteration is real work:\n\n```bash\nnohup npm run dev > /tmp/lk-agent.log 2>&1 &\nstart=$(date +%s); \\\nuntil grep -q "registered worker" /tmp/lk-agent.log 2>/dev/null \\\n && [ $(($(date +%s) - start)) -ge 60 ]; do sleep 2; done \\\n&& npx vent-hq run -f .vent/suite.livekit.json --call <name>\n```\n\nFor these composites, raise the Bash timeout to **10 minutes** (`600000` ms) \u2014 startup + 60s wait + call can easily exceed the 5-minute default.\n\n## Workflow\n\n1. Identify the behavior under test. Read enough of the agent codebase to understand its system prompt, tools, handlers, routes, provider config, platform wiring, and expected handoffs.\n2. Reuse an existing `.vent/suite.<adapter>.json` when possible. If `.vent/` contains multiple suites, inspect `connection.adapter` and report which suite file produced the result.\n3. Create or update a suite only when the existing calls do not cover the changed behavior. Name calls after real flows, for example `reschedule-appointment`, not `call-1`.\n4. If the suite uses `start_command`, start one shared local session first with `npx vent-hq agent start -f .vent/suite.<adapter>.json`, then pass `--session <session-id>` to each run.\n\n **For locally-run LiveKit agents: every run requires killing *all* workers, starting one fresh worker, and waiting a full 60 seconds before submitting.** Unconditional \u2014 LiveKit Cloud round-robins across registered workers, so a single survivor with a dead inference subprocess fails ~N-1 of N calls. Don\'t rely on `pkill -f <path-pattern>`; bare command lines like `node --import tsx agent.ts dev` won\'t match a path filter. Use `ps aux | grep -E "node.*agent\\.ts|@livekit/agents.*ipc"`, `kill -9` by PID, re-run `ps` to confirm zero survivors, then start the fresh worker. Skipping the 60s wait fails with `did not publish audio track`; if that error appears alongside `Error [ERR_IPC_CHANNEL_CLOSED] from InferenceProcExecutor.doInference` in the agent log right after a "running EOU detection" line, that\'s a straggler \u2014 redo the kill sweep. Hosted LiveKit Cloud agents don\'t need any of this; run normally. (See `Claude Code Execution` for how to compose the kill\u2192start\u2192wait\u2192submit as a single foreground command.)\n5. Pick which call(s) to run based on the change. Fixed bug: replay the failing scenario. Changed tool: include a call that triggers that tool. Prompt or routing change: include the relevant happy path and any important edge path.\n6. Compare against the previous JSON in `.vent/runs/` when validating a fix or regression. Check status flips, latency jumps, tool-call success drops, cost jumps, and transcript divergence. Correlate with `git diff` between saved `git_sha` values when available; skip if no previous run exists.\n\n## Commands\n\n```bash\nnpx vent-hq init # First-time setup (auth + skill install + starter suite)\nnpx vent-hq login # Log in to existing account\nnpx vent-hq login --status # Print whether credentials are present\nnpx vent-hq logout # Remove saved credentials from ~/.vent/credentials\nnpx vent-hq run -f .vent/suite.X.json # Run a single-call suite\nnpx vent-hq run -f .vent/suite.X.json --call NAME # Run one named call from a multi-call suite\nnpx vent-hq run ... --session <session-id> # Add to any run; routes through an existing local relay session\nnpx vent-hq run ... --verbose # Add to any run; include verbose debug fields\nnpx vent-hq stop <run-id> # Cancel a queued or running run\nnpx vent-hq agent start -f .vent/suite.X.json # Start a shared local relay session\nnpx vent-hq agent stop <session-id> # Stop a shared local relay session\n```\n\nIf `~/.vent/credentials` is missing and `VENT_ACCESS_TOKEN` is not set, run `npx vent-hq init`. For an existing account, run `npx vent-hq login` or set `VENT_ACCESS_TOKEN`.\n\n## Suite Config\n\nSuites live in `.vent/suite.<adapter>.json`. `connection` is declared once per suite. `calls` is a named map, and each key becomes the call name used with `--call`.\n\nLocal websocket suite:\n\n```json\n{\n "connection": {\n "adapter": "websocket",\n "start_command": "npm run start",\n "health_endpoint": "/health",\n "agent_port": 3001\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8,\n "silence_threshold_ms": 1200\n }\n }\n}\n```\n\nPlatform-direct suite:\n\n```json\n{\n "connection": {\n "adapter": "vapi",\n "platform": { "provider": "vapi" }\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nWrite `caller_prompt` as a realistic caller with a name, goal, mood, constraints, and conditional behavior. Set `max_turns` based on flow complexity: FAQ `4-6`, booking or tool use `8-12`, complex flows `12-20`.\n\nCall fields:\n\n- `caller_prompt` and `max_turns` are required.\n- `silence_threshold_ms` must be `200-10000`. Common ranges: FAQ `800-1200`, tool calls `2000-3000`, complex reasoning `3000-5000`.\n- `persona` supports `pace`, `clarity`, `disfluencies`, `cooperation`, `emotion`, `memory`, `intent_clarity`, and `confirmation_style`.\n- `caller_audio` supports noise, speed, speakerphone, mic distance, clarity, accent, packet loss, and jitter.\n- `language` is an ISO 639-1 code such as `en`, `es`, `fr`, `de`, `it`, `nl`, or `ja`.\n- `voice` is `"male"` or `"female"` (English only; default female). Use to flip the caller\'s perceived gender. Ignored if `caller_audio.accent` is set or `language` is non-English.\n\n## Connections and Credentials\n\n### Adapter choice\n\nUse `websocket` for your own local or hosted runtime. Use `start_command` for local agents or `agent_url` for hosted custom endpoints. For `start_command` and `agent_url`, do not put Deepgram, ElevenLabs, OpenAI, or other agent runtime keys into Vent config unless the Vent adapter itself needs them \u2014 the tested agent owns its own runtime credentials.\n\nUse `vapi`, `retell`, `elevenlabs`, `bland`, or `livekit` for platform-direct testing. In this mode Vent itself talks to the provider on the user\'s behalf.\n\nVent provides `DEEPGRAM_API_KEY` and `ANTHROPIC_API_KEY` for its hosted caller/evaluation stack \u2014 those are Vent\'s, not the tested agent\'s.\n\n### Credential resolution\n\nIn platform-direct mode the CLI auto-resolves credentials from `.env.local`, `.env`, and the current shell environment. Do not run `source .env && export` before Vent commands. If you include credential fields in JSON, use the actual value, not the env var name. Do not manually author `platform_connection_id`; the CLI creates or updates the saved platform connection automatically.\n\nAuto-resolved env vars and JSON fields:\n\n- Vapi: `VAPI_API_KEY` -> `vapi_api_key`; `VAPI_ASSISTANT_ID` or `VAPI_AGENT_ID` -> `vapi_assistant_id`\n- Bland: `BLAND_API_KEY` -> `bland_api_key`; `BLAND_PATHWAY_ID` -> `bland_pathway_id`; `BLAND_PERSONA_ID` -> `persona_id`\n- LiveKit: `LIVEKIT_API_KEY` -> `livekit_api_key`; `LIVEKIT_API_SECRET` -> `livekit_api_secret`; `LIVEKIT_URL` -> `livekit_url`\n- Retell: `RETELL_API_KEY` -> `retell_api_key`; `RETELL_AGENT_ID` -> `retell_agent_id`\n- ElevenLabs: `ELEVENLABS_API_KEY` -> `elevenlabs_api_key`; `ELEVENLABS_AGENT_ID` -> `elevenlabs_agent_id`\n\n### Provider config\n\nUse existing provider config when possible: Vapi assistant, Retell agent, ElevenLabs agent, Bland pathway, or LiveKit agent. Bland uniquely supports inline config \u2014 `platform` may use `bland_pathway_id`, `persona_id`, or an inline `task` (with optional voice, model, and turn-handling overrides; see Bland\'s API docs for the full field list).\n\n### Concurrency\n\nWhen you fan out multiple Vent calls in parallel against the same provider (for example, running several named calls from one suite at once with `&` and `wait`), respect the provider\'s per-account concurrency limit. Exceeding it makes calls queue or fail at the provider \u2014 Vent does not enforce these caps for you.\n\nRecord the limit as `max_concurrency` in the suite\'s `platform` block so it\'s visible on future runs. Ask the user which plan they\'re on if sizing matters; otherwise use the conservative default in bold.\n\n- **Vapi**: **10** included per account; reserved lines can be purchased self-serve; Enterprise is unlimited.\n- **Retell**: Pay-as-you-go includes **20**; Enterprise has no cap.\n- **Bland**: Start=**10**, Build=50, Scale=100, Enterprise=unlimited.\n- **ElevenLabs**: Free=**4**, Starter=6, Creator=10, Pro=20, Scale=30, Business=30. Burst pricing can temporarily allow up to 3x base.\n- **LiveKit Cloud**: Build=**5**, Ship=20, Scale=50 managed inference sessions (the usual gate for voice agents); agent-session concurrency can go higher (Scale up to 600).\n\n## WebSocket\n\nFor `adapter: "websocket"`, Vent sends binary 16-bit mono PCM audio over one websocket connection. Websocket text frames are optional JSON events. Audio-only websocket agents still work, but events improve turn detection and observability. Vent sends `{"type":"end-call"}` when the test is done.\n\nUseful websocket text frames:\n\n```jsonc\n{"type":"speech-update","status":"started"}\n{"type":"speech-update","status":"stopped"}\n{"type":"tool_call","name":"check_availability","arguments":{},"result":{},"successful":true,"duration_ms":150}\n{"type":"vent:timing","stt_ms":120,"llm_ms":450,"tts_ms":80}\n{"type":"vent:session","platform":"custom","provider_call_id":"call_123","provider_session_id":"session_abc"}\n{"type":"vent:call-metadata","call_metadata":{"recording_url":"https://...","cost_usd":0.12}}\n{"type":"vent:transcript","role":"caller","text":"I need to reschedule","turn_index":0}\n{"type":"vent:transfer","destination":"+15551234567","status":"attempted"}\n{"type":"vent:debug-url","label":"trace","url":"https://..."}\n{"type":"vent:warning","message":"provider warning","code":"provider_warning"}\n```\n\n`vent:session-report` is **not** handled by the websocket adapter \u2014 it\'s only consumed by the LiveKit helper. Do not emit it from a websocket agent.\n\nPlatform adapters capture tool calls automatically. Websocket agents must emit `tool_call` frames for tool observability. Platform adapters get component latency automatically. Websocket agents should emit `vent:timing` after each agent response when STT/LLM/TTS breakdown is available.\n\n## LiveKit\n\nBefore running LiveKit tests, install and add the Vent helper to the LiveKit agent entrypoint. Node: `npm install @vent-hq/livekit`, then call `instrumentLiveKitAgent({ ctx, session })`. Python: `pip install vent-livekit`, then call `instrument_livekit_agent(ctx=ctx, session=session)`.\n\nLiveKit direct mode requires the LiveKit Agents SDK. Custom LiveKit participants should use the websocket adapter with a relay. If the LiveKit agent registered with an explicit dispatch name, set `livekit_agent_name` in `platform`.\n\nLiveKit parallel calls are supported, but capacity depends on where the user runs their agent. LiveKit Cloud only routes audio \u2014 the agent code runs on the user\'s own infrastructure (laptop, Fly.io, Railway, k8s, etc.), and that box\'s CPU/RAM is the real bottleneck. Before firing parallel calls, derive `platform.max_concurrency` from the minimum of: (1) the user\'s LiveKit plan limit (Build=5, Ship=20, Scale=50+ concurrent agent sessions), (2) their agent worker box capacity (rough rule: 1c/1GB\u21921\u20133 jobs, 2c/4GB\u21925\u201310, 4c/8GB\u219210\u201325, 8c/16GB\u219225\u201350; multiply by number of worker boxes), and (3) their STT/TTS provider concurrency (e.g. Deepgram TTS streaming = 2). If the user wants more parallel calls than that minimum allows, push back \u2014 explain which limit is the bottleneck (plan / box / provider) and either reduce the count or suggest scaling that limit. Architecture: register one `agent_name` and run N workers under it (one big worker, or several smaller ones \u2014 same name); LiveKit\'s dispatcher round-robins jobs across them with built-in failover. Don\'t use numbered names (`agent-0`, `agent-1`) \u2014 that reinvents what the dispatcher does and breaks failover.\n\nUse the LiveKit helper for observability; do not publish `vent:*` topics manually. Do not hand-roll `vent:session-report` from `ctx.addShutdownCallback`; after `room.disconnect()` it can fail with `engine is closed`. The helper captures SDK metrics, tool events, conversation items, usage, and close events. Native LiveKit `lk.transcription` and `lk.agent.state` provide transcript and agent-state timing.\n\n## Output\n\n### Live result\n\n`npx vent-hq run` returns a single JSON result on stdout in non-TTY mode (not an SSE JSONL stream). Exit codes: `0` = call ran through the pipeline; `1` = pipeline-level failure; `2` = harness error.\n\nMost result fields are always present; `latency`, `component_latency`, `call_metadata`, and `emotion` may be `null` when the underlying analysis didn\'t run; `debug` is absent without `--verbose`. Branch on null before reading nested fields. Use `--verbose` only when the default doesn\'t explain a failure \u2014 when you need `platform_transcript` (to check Vent\'s STT), per-turn or component-level latency breakdowns, the raw tool-call timeline, or provider-native artifacts in `debug.provider_metadata`. Otherwise skip \u2014 it just adds noise.\n\nVent\'s transcript is ground truth. Judge on semantic intent: ignore homophones and minor mis-hears (`"check teach hat"` for `"check that"`, missing question marks on short tails) \u2014 those are streaming-STT noise on Vent\'s caller side, not agent bugs, and **don\'t surface them in the report** (they\'re Vent-side artifacts, not actionable for the user). But clear gibberish or word-soup (e.g. `"Cristoxin"` where the agent should have said `"Of course, talk soon"`) is **not** a Vent artifact \u2014 Vent\'s STT does not invent words like that. It means the platform\'s TTS produced corrupted audio or the agent\'s STT/LLM generated the wrong text, and the fix lives there (TTS voice config, agent prompt, model temperature, codec issue). Never dismiss the run as a "Vent harness STT" issue; iterate on the agent or flag the platform.\n\nFor transfers: `call_metadata.transfer_attempted` (provider claimed) and `transfer_completed` (Vent-verified) can disagree \u2014 report both. `transfers[]` carries destination, type, and per-attempt status.\n\n### Saved history\n\nAfter every run, Vent writes the full result JSON to `.vent/runs/`. Shape:\n\n```jsonc\n{\n "run_id": "...",\n "timestamp": "2026-04-21T...Z",\n "git_sha": "...",\n "summary": { "calls_total": 2, "total_duration_ms": 12345, "total_cost_usd": 0.01 },\n "call_results": [\n { "name": "happy-path", "status": "completed", "duration_ms": 6123, "transcript": [], "observed_tool_calls": [], "latency": { "response_time_ms": 420, "p95_response_time_ms": 980 }, "call_metadata": { "cost_usd": 0.004 } }\n ]\n}\n```\n\nWhen comparing against a prior run (Workflow step 6), inspect:\n\n- Run-completion status flips: `call_results[i].status` (pipeline-only \u2014 judge mission success from the transcript)\n- Latency: `call_results[i].latency.response_time_ms` (mean) or `latency.p95_response_time_ms` increased >20%\n- Tool calls: count of `call_results[i].observed_tool_calls[].successful` dropped\n- Cost: `summary.total_cost_usd` or `call_results[i].call_metadata.cost_usd` increased >30%\n- Transcript: `call_results[i].transcript` diverged in semantic content (ignore STT noise)\n\n## Reporting Results\n\nBefore reporting, read the agent\'s code to locate where the observed behavior originates. If the issue is small and you can fix it, fix it and explain what you did \u2014 don\'t ask permission first.\n\nAdapt the report shape to the call \u2014 a clean pass needs little, a regression with a multi-layer cause needs more. Use a transcript excerpt when it helps the user see what happened.\n\nHard rules:\n\n- Pair raw numbers with their plain-English meaning \u2014 don\'t drop the number, but don\'t leave it unexplained. E.g. "p95 latency was 850ms, which is snappy and well within natural conversational pacing" or "p95 hit 1.6 seconds with the LLM as the bottleneck \u2014 noticeably sluggish to a caller."\n- Name the user\'s voice agent by platform on first mention (e.g. "the Vapi agent responded snappily throughout") so the user knows immediately which agent the observation is about. After that, just say "the agent" \u2014 don\'t repeat the platform name on every line.\n- Always include the recording from `call_metadata.recording_url` as an inline `[Recording](url)` link, placed in **one block at the very end of the report** \u2014 never sprinkled through the prose. Single call: one link as the last line. Multi-call: one labeled link per call (e.g. `reschedule-appointment: [Recording](url)`). Never paste a bare URL.\n- Mission success is your judgment, not Vent\'s. The per-call `status` is only `"completed"` (pipeline ran) or `"error"` (pipeline failed); decide whether the agent actually accomplished the scenario from the transcript and tool calls.\n- Similar-sounding word substitutions (e.g. "ocean" for "OSHA") are STT ambiguity, not comprehension failure. The fix lives in STT keyword hints, custom vocabulary, or a prompt-level term list \u2014 not the agent\'s reasoning.\n- Surface only what the user can act on in their own agent\'s code or config \u2014 never `warnings[]` (infrastructure noise), Vent-side artifacts (caller wait modes, harness timing, internal pipeline quirks), or `cost_usd` unless asked.\n\nFor multi-call runs, lead with your own judgment of what happened across the calls (e.g. "3 of 4 did what they were supposed to; `cancel-appointment` never actually canceled"), not a parroted pass/fail count. Then cover each call with whatever depth it needs.\n';
|
|
5490
|
+
var claude_code_default = '## Claude Code Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nClaude Code serializes separate Bash tool calls for `npx vent-hq ...`, so run multiple calls from one suite by invoking each named call with `--call <name>` in one Bash command using `&` and a final `wait`:\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path & \\\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path & \\\nwait\n```\n\nFor long-running composite commands \u2014 e.g. LiveKit\'s required `kill workers \u2192 start fresh worker \u2192 wait 60s \u2192 npx vent-hq run` preflight \u2014 keep the entire sequence in **one foreground Bash call**. Do not use `Monitor` or `run_in_background: true` for the wait. Both end the turn until an event fires, which means Vent never sees an inflight call and the user reads it as "you backgrounded the test." The harness also blocks bare `sleep 60` (any long leading sleep), so fold the readiness check and the wait into a single `until`-loop \u2014 short sleeps inside a polling loop are allowed because each iteration is real work:\n\n```bash\nnohup npm run dev > /tmp/lk-agent.log 2>&1 &\nstart=$(date +%s); \\\nuntil grep -q "registered worker" /tmp/lk-agent.log 2>/dev/null \\\n && [ $(($(date +%s) - start)) -ge 60 ]; do sleep 2; done \\\n&& npx vent-hq run -f .vent/suite.livekit.json --call <name>\n```\n\nFor these composites, raise the Bash timeout to **10 minutes** (`600000` ms) \u2014 startup + 60s wait + call can easily exceed the 5-minute default.\n';
|
|
5776
5491
|
|
|
5777
5492
|
// src/skills/cursor.md
|
|
5778
|
-
var cursor_default = '---\ndescription: Place real calls against the user\'s voice agent to verify recent code changes worked end-to-end. Trigger on test requests or after any non-trivial voice-agent change.\nalwaysApply: true\n---\n\n# Vent - Voice Agent Calls\n\n## When to Test\n\nVent is the voice CI gate. Auto-trigger a call after any non-trivial voice-affecting change \u2014 don\'t wait to be asked. Non-trivial means anything that affects what the caller hears, how the agent responds, what tools are called, how the call routes, or audio timing: prompt rewrites or sections (not one-word tweaks), tool schema or handler changes, state/memory logic, transfer logic, provider/audio config, platform integration, and any voice-behavior bug fix. One-line copy or comment edits don\'t need a call.\n\nBefore launching, scan `.vent/runs/` for unresolved issues from prior runs (failing status, regressions, warnings, latency jumps); fix in-scope ones as part of this change and verify they\'re resolved by the new run, surface out-of-scope ones rather than silently leaving them. The task is not done until you\'ve reviewed the transcript, tool calls, and observed behavior and judged that the change works \u2014 Vent\'s `status` and exit code only reflect pipeline completion, not mission success.\n\n## Autonomous Iteration\n\n**Default: iterate.** Run a fix \u2192 Vent-call \u2192 fix \u2192 Vent-call loop whenever a call surfaces a fixable issue. Don\'t report a failure (or a degraded-quality success) back to the user before attempting to fix it \u2014 they want the agent working, not a diagnosis. After each call, if the result reveals a problem you can fix with a concrete plan, apply the fix and run again. Fixable issues include: tool schema or registry gaps, missing handlers, prompt phrasing, **STT vocabulary mismatches** (e.g. domain terms misheard \u2014 Deepgram `keyterms`, custom vocabulary, prompt-level term lists), **latency tunables** (endpointing thresholds, preemptive generation, model selection), and audio quality config. If you\'d be guessing at the next attempt, stop and ask the user instead of trying.\n\n**"Mission success but with quality problems" still requires iteration.** If the call ran end-to-end but had noticeably bad latency, STT mishearing the agent\'s domain vocabulary, audio artifacts, or any other caller-experience degradation that has a concrete fix, the call is *not done* \u2014 apply the fix and re-run. Don\'t downgrade fixable problems to "observations" or "things worth flagging." The pipeline running cleanly is the floor, not the ceiling.\n\n**Never ask permission to iterate.** "Want me to apply this and re-run?" / "Should I fix it?" / "Let me know if you want me to..." \u2014 these are skill violations. If you have a concrete fix, apply it and run the next call. The user opts out by saying so explicitly; silence is not opt-out.\n\nStop iterating and report when one of:\n\n- The next call confirms the fix worked AND no further fixable issues remain (success \u2014 done).\n- The same fix fails twice (your hypothesis is wrong; ask the user).\n- The failure mode shifts between attempts (you\'re chasing a moving target).\n- You can\'t justify the next call against its provider cost \u2014 each call spends real money and provider quota.\n\nIf the user explicitly opts out ("just report", "don\'t iterate", "stop after the first call", "I\'ll fix it myself"), respect that \u2014 run once, report, stop. Otherwise iterate by default. The first time you start an autonomous loop in a session, mention it once so the user knows it\'s happening (e.g. "the call surfaced X \u2014 fixing and re-running; I\'ll report back when it converges or stops making progress").\n\n## Cursor Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nCursor cannot run separate shell tool calls concurrently \u2014 for multiple calls from one suite, invoke each named call with `--call <name>` in one shell command using `&` and a final `wait`:\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path & \\\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path & \\\nwait\n```\n\n## Workflow\n\n1. Identify the behavior under test. Read enough of the agent codebase to understand its system prompt, tools, handlers, routes, provider config, platform wiring, and expected handoffs.\n2. Reuse an existing `.vent/suite.<adapter>.json` when possible. If `.vent/` contains multiple suites, inspect `connection.adapter` and report which suite file produced the result.\n3. Create or update a suite only when the existing calls do not cover the changed behavior. Name calls after real flows, for example `reschedule-appointment`, not `call-1`.\n4. If the suite uses `start_command`, start one shared local session first with `npx vent-hq agent start -f .vent/suite.<adapter>.json`, then pass `--session <session-id>` to each run.\n\n **For locally-run LiveKit agents: every run requires killing *all* workers, starting one fresh worker, and waiting a full 60 seconds before submitting.** Unconditional \u2014 LiveKit Cloud round-robins across registered workers, so a single survivor with a dead inference subprocess fails ~N-1 of N calls. Don\'t rely on `pkill -f <path-pattern>`; bare command lines like `node --import tsx agent.ts dev` won\'t match a path filter. Use `ps aux | grep -E "node.*agent\\.ts|@livekit/agents.*ipc"`, `kill -9` by PID, re-run `ps` to confirm zero survivors, then start the fresh worker. Skipping the 60s wait fails with `did not publish audio track`; if that error appears alongside `Error [ERR_IPC_CHANNEL_CLOSED] from InferenceProcExecutor.doInference` in the agent log right after a "running EOU detection" line, that\'s a straggler \u2014 redo the kill sweep. Hosted LiveKit Cloud agents don\'t need any of this; run normally.\n5. Pick which call(s) to run based on the change. Fixed bug: replay the failing scenario. Changed tool: include a call that triggers that tool. Prompt or routing change: include the relevant happy path and any important edge path.\n6. Compare against the previous JSON in `.vent/runs/` when validating a fix or regression. Check status flips, latency jumps, tool-call success drops, cost jumps, and transcript divergence. Correlate with `git diff` between saved `git_sha` values when available; skip if no previous run exists.\n\n## Commands\n\n```bash\nnpx vent-hq init # First-time setup (auth + skill install + starter suite)\nnpx vent-hq login # Log in to existing account\nnpx vent-hq login --status # Print whether credentials are present\nnpx vent-hq logout # Remove saved credentials from ~/.vent/credentials\nnpx vent-hq run -f .vent/suite.X.json # Run a single-call suite\nnpx vent-hq run -f .vent/suite.X.json --call NAME # Run one named call from a multi-call suite\nnpx vent-hq run ... --session <session-id> # Add to any run; routes through an existing local relay session\nnpx vent-hq run ... --verbose # Add to any run; include verbose debug fields\nnpx vent-hq stop <run-id> # Cancel a queued or running run\nnpx vent-hq agent start -f .vent/suite.X.json # Start a shared local relay session\nnpx vent-hq agent stop <session-id> # Stop a shared local relay session\n```\n\nIf `~/.vent/credentials` is missing and `VENT_ACCESS_TOKEN` is not set, run `npx vent-hq init`. For an existing account, run `npx vent-hq login` or set `VENT_ACCESS_TOKEN`.\n\n## Suite Config\n\nSuites live in `.vent/suite.<adapter>.json`. `connection` is declared once per suite. `calls` is a named map, and each key becomes the call name used with `--call`.\n\nLocal websocket suite:\n\n```json\n{\n "connection": {\n "adapter": "websocket",\n "start_command": "npm run start",\n "health_endpoint": "/health",\n "agent_port": 3001\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8,\n "silence_threshold_ms": 1200\n }\n }\n}\n```\n\nPlatform-direct suite:\n\n```json\n{\n "connection": {\n "adapter": "vapi",\n "platform": { "provider": "vapi" }\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nWrite `caller_prompt` as a realistic caller with a name, goal, mood, constraints, and conditional behavior. Set `max_turns` based on flow complexity: FAQ `4-6`, booking or tool use `8-12`, complex flows `12-20`.\n\nCall fields:\n\n- `caller_prompt` and `max_turns` are required.\n- `silence_threshold_ms` must be `200-10000`. Common ranges: FAQ `800-1200`, tool calls `2000-3000`, complex reasoning `3000-5000`.\n- `persona` supports `pace`, `clarity`, `disfluencies`, `cooperation`, `emotion`, `memory`, `intent_clarity`, and `confirmation_style`.\n- `caller_audio` supports noise, speed, speakerphone, mic distance, clarity, accent, packet loss, and jitter.\n- `language` is an ISO 639-1 code such as `en`, `es`, `fr`, `de`, `it`, `nl`, or `ja`.\n- `voice` is `"male"` or `"female"` (English only; default female). Use to flip the caller\'s perceived gender. Ignored if `caller_audio.accent` is set or `language` is non-English.\n\n## Connections and Credentials\n\n### Adapter choice\n\nUse `websocket` for your own local or hosted runtime. Use `start_command` for local agents or `agent_url` for hosted custom endpoints. For `start_command` and `agent_url`, do not put Deepgram, ElevenLabs, OpenAI, or other agent runtime keys into Vent config unless the Vent adapter itself needs them \u2014 the tested agent owns its own runtime credentials.\n\nUse `vapi`, `retell`, `elevenlabs`, `bland`, or `livekit` for platform-direct testing. In this mode Vent itself talks to the provider on the user\'s behalf.\n\nVent provides `DEEPGRAM_API_KEY` and `ANTHROPIC_API_KEY` for its hosted caller/evaluation stack \u2014 those are Vent\'s, not the tested agent\'s.\n\n### Credential resolution\n\nIn platform-direct mode the CLI auto-resolves credentials from `.env.local`, `.env`, and the current shell environment. Do not run `source .env && export` before Vent commands. If you include credential fields in JSON, use the actual value, not the env var name. Do not manually author `platform_connection_id`; the CLI creates or updates the saved platform connection automatically.\n\nAuto-resolved env vars and JSON fields:\n\n- Vapi: `VAPI_API_KEY` -> `vapi_api_key`; `VAPI_ASSISTANT_ID` or `VAPI_AGENT_ID` -> `vapi_assistant_id`\n- Bland: `BLAND_API_KEY` -> `bland_api_key`; `BLAND_PATHWAY_ID` -> `bland_pathway_id`; `BLAND_PERSONA_ID` -> `persona_id`\n- LiveKit: `LIVEKIT_API_KEY` -> `livekit_api_key`; `LIVEKIT_API_SECRET` -> `livekit_api_secret`; `LIVEKIT_URL` -> `livekit_url`\n- Retell: `RETELL_API_KEY` -> `retell_api_key`; `RETELL_AGENT_ID` -> `retell_agent_id`\n- ElevenLabs: `ELEVENLABS_API_KEY` -> `elevenlabs_api_key`; `ELEVENLABS_AGENT_ID` -> `elevenlabs_agent_id`\n\n### Provider config\n\nUse existing provider config when possible: Vapi assistant, Retell agent, ElevenLabs agent, Bland pathway, or LiveKit agent. Bland uniquely supports inline config \u2014 `platform` may use `bland_pathway_id`, `persona_id`, or an inline `task` (with optional voice, model, and turn-handling overrides; see Bland\'s API docs for the full field list).\n\n### Concurrency\n\nWhen you fan out multiple Vent calls in parallel against the same provider (for example, running several named calls from one suite at once with `&` and `wait`), respect the provider\'s per-account concurrency limit. Exceeding it makes calls queue or fail at the provider \u2014 Vent does not enforce these caps for you.\n\nRecord the limit as `max_concurrency` in the suite\'s `platform` block so it\'s visible on future runs. Ask the user which plan they\'re on if sizing matters; otherwise use the conservative default in bold.\n\n- **Vapi**: **10** included per account; reserved lines can be purchased self-serve; Enterprise is unlimited.\n- **Retell**: Pay-as-you-go includes **20**; Enterprise has no cap.\n- **Bland**: Start=**10**, Build=50, Scale=100, Enterprise=unlimited.\n- **ElevenLabs**: Free=**4**, Starter=6, Creator=10, Pro=20, Scale=30, Business=30. Burst pricing can temporarily allow up to 3x base.\n- **LiveKit Cloud**: Build=**5**, Ship=20, Scale=50 managed inference sessions (the usual gate for voice agents); agent-session concurrency can go higher (Scale up to 600).\n\n## WebSocket\n\nFor `adapter: "websocket"`, Vent sends binary 16-bit mono PCM audio over one websocket connection. Websocket text frames are optional JSON events. Audio-only websocket agents still work, but events improve turn detection and observability. Vent sends `{"type":"end-call"}` when the test is done.\n\nUseful websocket text frames:\n\n```jsonc\n{"type":"speech-update","status":"started"}\n{"type":"speech-update","status":"stopped"}\n{"type":"tool_call","name":"check_availability","arguments":{},"result":{},"successful":true,"duration_ms":150}\n{"type":"vent:timing","stt_ms":120,"llm_ms":450,"tts_ms":80}\n{"type":"vent:session","platform":"custom","provider_call_id":"call_123","provider_session_id":"session_abc"}\n{"type":"vent:call-metadata","call_metadata":{"recording_url":"https://...","cost_usd":0.12}}\n{"type":"vent:transcript","role":"caller","text":"I need to reschedule","turn_index":0}\n{"type":"vent:transfer","destination":"+15551234567","status":"attempted"}\n{"type":"vent:debug-url","label":"trace","url":"https://..."}\n{"type":"vent:warning","message":"provider warning","code":"provider_warning"}\n```\n\n`vent:session-report` is **not** handled by the websocket adapter \u2014 it\'s only consumed by the LiveKit helper. Do not emit it from a websocket agent.\n\nPlatform adapters capture tool calls automatically. Websocket agents must emit `tool_call` frames for tool observability. Platform adapters get component latency automatically. Websocket agents should emit `vent:timing` after each agent response when STT/LLM/TTS breakdown is available.\n\n## LiveKit\n\nBefore running LiveKit tests, install and add the Vent helper to the LiveKit agent entrypoint. Node: `npm install @vent-hq/livekit`, then call `instrumentLiveKitAgent({ ctx, session })`. Python: `pip install vent-livekit`, then call `instrument_livekit_agent(ctx=ctx, session=session)`.\n\nLiveKit direct mode requires the LiveKit Agents SDK. Custom LiveKit participants should use the websocket adapter with a relay. If the LiveKit agent registered with an explicit dispatch name, set `livekit_agent_name` in `platform`.\n\nLiveKit parallel calls are supported, but capacity depends on where the user runs their agent. LiveKit Cloud only routes audio \u2014 the agent code runs on the user\'s own infrastructure (laptop, Fly.io, Railway, k8s, etc.), and that box\'s CPU/RAM is the real bottleneck. Before firing parallel calls, derive `platform.max_concurrency` from the minimum of: (1) the user\'s LiveKit plan limit (Build=5, Ship=20, Scale=50+ concurrent agent sessions), (2) their agent worker box capacity (rough rule: 1c/1GB\u21921\u20133 jobs, 2c/4GB\u21925\u201310, 4c/8GB\u219210\u201325, 8c/16GB\u219225\u201350; multiply by number of worker boxes), and (3) their STT/TTS provider concurrency (e.g. Deepgram TTS streaming = 2). If the user wants more parallel calls than that minimum allows, push back \u2014 explain which limit is the bottleneck (plan / box / provider) and either reduce the count or suggest scaling that limit. Architecture: register one `agent_name` and run N workers under it (one big worker, or several smaller ones \u2014 same name); LiveKit\'s dispatcher round-robins jobs across them with built-in failover. Don\'t use numbered names (`agent-0`, `agent-1`) \u2014 that reinvents what the dispatcher does and breaks failover.\n\nUse the LiveKit helper for observability; do not publish `vent:*` topics manually. Do not hand-roll `vent:session-report` from `ctx.addShutdownCallback`; after `room.disconnect()` it can fail with `engine is closed`. The helper captures SDK metrics, tool events, conversation items, usage, and close events. Native LiveKit `lk.transcription` and `lk.agent.state` provide transcript and agent-state timing.\n\n## Output\n\n### Live result\n\n`npx vent-hq run` returns a single JSON result on stdout in non-TTY mode (not an SSE JSONL stream). Exit codes: `0` = call ran through the pipeline; `1` = pipeline-level failure; `2` = harness error.\n\nMost result fields are always present; `latency`, `component_latency`, `call_metadata`, and `emotion` may be `null` when the underlying analysis didn\'t run; `debug` is absent without `--verbose`. Branch on null before reading nested fields. Use `--verbose` only when the default doesn\'t explain a failure \u2014 when you need `platform_transcript` (to check Vent\'s STT), per-turn or component-level latency breakdowns, the raw tool-call timeline, or provider-native artifacts in `debug.provider_metadata`. Otherwise skip \u2014 it just adds noise.\n\nVent\'s transcript is ground truth. Judge on semantic intent: ignore homophones and minor mis-hears (`"check teach hat"` for `"check that"`, missing question marks on short tails) \u2014 those are streaming-STT noise on Vent\'s caller side, not agent bugs, and **don\'t surface them in the report** (they\'re Vent-side artifacts, not actionable for the user). But clear gibberish or word-soup (e.g. `"Cristoxin"` where the agent should have said `"Of course, talk soon"`) is **not** a Vent artifact \u2014 Vent\'s STT does not invent words like that. It means the platform\'s TTS produced corrupted audio or the agent\'s STT/LLM generated the wrong text, and the fix lives there (TTS voice config, agent prompt, model temperature, codec issue). Never dismiss the run as a "Vent harness STT" issue; iterate on the agent or flag the platform.\n\nFor transfers: `call_metadata.transfer_attempted` (provider claimed) and `transfer_completed` (Vent-verified) can disagree \u2014 report both. `transfers[]` carries destination, type, and per-attempt status.\n\n### Saved history\n\nAfter every run, Vent writes the full result JSON to `.vent/runs/`. Shape:\n\n```jsonc\n{\n "run_id": "...",\n "timestamp": "2026-04-21T...Z",\n "git_sha": "...",\n "summary": { "calls_total": 2, "total_duration_ms": 12345, "total_cost_usd": 0.01 },\n "call_results": [\n { "name": "happy-path", "status": "completed", "duration_ms": 6123, "transcript": [], "observed_tool_calls": [], "latency": { "response_time_ms": 420, "p95_response_time_ms": 980 }, "call_metadata": { "cost_usd": 0.004 } }\n ]\n}\n```\n\nWhen comparing against a prior run (Workflow step 6), inspect:\n\n- Run-completion status flips: `call_results[i].status` (pipeline-only \u2014 judge mission success from the transcript)\n- Latency: `call_results[i].latency.response_time_ms` (mean) or `latency.p95_response_time_ms` increased >20%\n- Tool calls: count of `call_results[i].observed_tool_calls[].successful` dropped\n- Cost: `summary.total_cost_usd` or `call_results[i].call_metadata.cost_usd` increased >30%\n- Transcript: `call_results[i].transcript` diverged in semantic content (ignore STT noise)\n\n## Reporting Results\n\nBefore reporting, read the agent\'s code to locate where the observed behavior originates. If the issue is small and you can fix it, fix it and explain what you did \u2014 don\'t ask permission first.\n\nAdapt the report shape to the call \u2014 a clean pass needs little, a regression with a multi-layer cause needs more. Use a transcript excerpt when it helps the user see what happened.\n\nHard rules:\n\n- Pair raw numbers with their plain-English meaning \u2014 don\'t drop the number, but don\'t leave it unexplained. E.g. "p95 latency was 850ms, which is snappy and well within natural conversational pacing" or "p95 hit 1.6 seconds with the LLM as the bottleneck \u2014 noticeably sluggish to a caller."\n- Name the user\'s voice agent by platform on first mention (e.g. "the Vapi agent responded snappily throughout") so the user knows immediately which agent the observation is about. After that, just say "the agent" \u2014 don\'t repeat the platform name on every line.\n- Always include the recording from `call_metadata.recording_url` as an inline `[Recording](url)` link, placed in **one block at the very end of the report** \u2014 never sprinkled through the prose. Single call: one link as the last line. Multi-call: one labeled link per call (e.g. `reschedule-appointment: [Recording](url)`). Never paste a bare URL.\n- Mission success is your judgment, not Vent\'s. The per-call `status` is only `"completed"` (pipeline ran) or `"error"` (pipeline failed); decide whether the agent actually accomplished the scenario from the transcript and tool calls.\n- Similar-sounding word substitutions (e.g. "ocean" for "OSHA") are STT ambiguity, not comprehension failure. The fix lives in STT keyword hints, custom vocabulary, or a prompt-level term list \u2014 not the agent\'s reasoning.\n- Surface only what the user can act on in their own agent\'s code or config \u2014 never `warnings[]` (infrastructure noise), Vent-side artifacts (caller wait modes, harness timing, internal pipeline quirks), or `cost_usd` unless asked.\n\nFor multi-call runs, lead with your own judgment of what happened across the calls (e.g. "3 of 4 did what they were supposed to; `cancel-appointment` never actually canceled"), not a parroted pass/fail count. Then cover each call with whatever depth it needs.\n';
|
|
5493
|
+
var cursor_default = "## Cursor Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nCursor cannot run separate shell tool calls concurrently \u2014 for multiple calls from one suite, invoke each named call with `--call <name>` in one shell command using `&` and a final `wait`:\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path & \\\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path & \\\nwait\n```\n";
|
|
5779
5494
|
|
|
5780
5495
|
// src/skills/codex.md
|
|
5781
|
-
var codex_default = '# Vent - Voice Agent Calls\n\n## When to Test\n\nVent is the voice CI gate. Auto-trigger a call after any non-trivial voice-affecting change \u2014 don\'t wait to be asked. Non-trivial means anything that affects what the caller hears, how the agent responds, what tools are called, how the call routes, or audio timing: prompt rewrites or sections (not one-word tweaks), tool schema or handler changes, state/memory logic, transfer logic, provider/audio config, platform integration, and any voice-behavior bug fix. One-line copy or comment edits don\'t need a call.\n\nBefore launching, scan `.vent/runs/` for unresolved issues from prior runs (failing status, regressions, warnings, latency jumps); fix in-scope ones as part of this change and verify they\'re resolved by the new run, surface out-of-scope ones rather than silently leaving them. The task is not done until you\'ve reviewed the transcript, tool calls, and observed behavior and judged that the change works \u2014 Vent\'s `status` and exit code only reflect pipeline completion, not mission success.\n\n## Autonomous Iteration\n\n**Default: iterate.** Run a fix \u2192 Vent-call \u2192 fix \u2192 Vent-call loop whenever a call surfaces a fixable issue. Don\'t report a failure (or a degraded-quality success) back to the user before attempting to fix it \u2014 they want the agent working, not a diagnosis. After each call, if the result reveals a problem you can fix with a concrete plan, apply the fix and run again. Fixable issues include: tool schema or registry gaps, missing handlers, prompt phrasing, **STT vocabulary mismatches** (e.g. domain terms misheard \u2014 Deepgram `keyterms`, custom vocabulary, prompt-level term lists), **latency tunables** (endpointing thresholds, preemptive generation, model selection), and audio quality config. If you\'d be guessing at the next attempt, stop and ask the user instead of trying.\n\n**"Mission success but with quality problems" still requires iteration.** If the call ran end-to-end but had noticeably bad latency, STT mishearing the agent\'s domain vocabulary, audio artifacts, or any other caller-experience degradation that has a concrete fix, the call is *not done* \u2014 apply the fix and re-run. Don\'t downgrade fixable problems to "observations" or "things worth flagging." The pipeline running cleanly is the floor, not the ceiling.\n\n**Never ask permission to iterate.** "Want me to apply this and re-run?" / "Should I fix it?" / "Let me know if you want me to..." \u2014 these are skill violations. If you have a concrete fix, apply it and run the next call. The user opts out by saying so explicitly; silence is not opt-out.\n\nStop iterating and report when one of:\n\n- The next call confirms the fix worked AND no further fixable issues remain (success \u2014 done).\n- The same fix fails twice (your hypothesis is wrong; ask the user).\n- The failure mode shifts between attempts (you\'re chasing a moving target).\n- You can\'t justify the next call against its provider cost \u2014 each call spends real money and provider quota.\n\nIf the user explicitly opts out ("just report", "don\'t iterate", "stop after the first call", "I\'ll fix it myself"), respect that \u2014 run once, report, stop. Otherwise iterate by default. The first time you start an autonomous loop in a session, mention it once so the user knows it\'s happening (e.g. "the call surfaced X \u2014 fixing and re-running; I\'ll report back when it converges or stops making progress").\n\n## Codex Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nCodex can run shell tool calls concurrently \u2014 for multiple calls from one suite, run each named call as its own parallel shell tool call (do not combine them with `&`):\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path\n```\n\n## Workflow\n\n1. Identify the behavior under test. Read enough of the agent codebase to understand its system prompt, tools, handlers, routes, provider config, platform wiring, and expected handoffs.\n2. Reuse an existing `.vent/suite.<adapter>.json` when possible. If `.vent/` contains multiple suites, inspect `connection.adapter` and report which suite file produced the result.\n3. Create or update a suite only when the existing calls do not cover the changed behavior. Name calls after real flows, for example `reschedule-appointment`, not `call-1`.\n4. If the suite uses `start_command`, start one shared local session first with `npx vent-hq agent start -f .vent/suite.<adapter>.json`, then pass `--session <session-id>` to each run.\n\n **For locally-run LiveKit agents: every run requires killing *all* workers, starting one fresh worker, and waiting a full 60 seconds before submitting.** Unconditional \u2014 LiveKit Cloud round-robins across registered workers, so a single survivor with a dead inference subprocess fails ~N-1 of N calls. Don\'t rely on `pkill -f <path-pattern>`; bare command lines like `node --import tsx agent.ts dev` won\'t match a path filter. Use `ps aux | grep -E "node.*agent\\.ts|@livekit/agents.*ipc"`, `kill -9` by PID, re-run `ps` to confirm zero survivors, then start the fresh worker. Skipping the 60s wait fails with `did not publish audio track`; if that error appears alongside `Error [ERR_IPC_CHANNEL_CLOSED] from InferenceProcExecutor.doInference` in the agent log right after a "running EOU detection" line, that\'s a straggler \u2014 redo the kill sweep. Hosted LiveKit Cloud agents don\'t need any of this; run normally.\n5. Pick which call(s) to run based on the change. Fixed bug: replay the failing scenario. Changed tool: include a call that triggers that tool. Prompt or routing change: include the relevant happy path and any important edge path.\n6. Compare against the previous JSON in `.vent/runs/` when validating a fix or regression. Check status flips, latency jumps, tool-call success drops, cost jumps, and transcript divergence. Correlate with `git diff` between saved `git_sha` values when available; skip if no previous run exists.\n\n## Commands\n\n```bash\nnpx vent-hq init # First-time setup (auth + skill install + starter suite)\nnpx vent-hq login # Log in to existing account\nnpx vent-hq login --status # Print whether credentials are present\nnpx vent-hq logout # Remove saved credentials from ~/.vent/credentials\nnpx vent-hq run -f .vent/suite.X.json # Run a single-call suite\nnpx vent-hq run -f .vent/suite.X.json --call NAME # Run one named call from a multi-call suite\nnpx vent-hq run ... --session <session-id> # Add to any run; routes through an existing local relay session\nnpx vent-hq run ... --verbose # Add to any run; include verbose debug fields\nnpx vent-hq stop <run-id> # Cancel a queued or running run\nnpx vent-hq agent start -f .vent/suite.X.json # Start a shared local relay session\nnpx vent-hq agent stop <session-id> # Stop a shared local relay session\n```\n\nIf `~/.vent/credentials` is missing and `VENT_ACCESS_TOKEN` is not set, run `npx vent-hq init`. For an existing account, run `npx vent-hq login` or set `VENT_ACCESS_TOKEN`.\n\n## Suite Config\n\nSuites live in `.vent/suite.<adapter>.json`. `connection` is declared once per suite. `calls` is a named map, and each key becomes the call name used with `--call`.\n\nLocal websocket suite:\n\n```json\n{\n "connection": {\n "adapter": "websocket",\n "start_command": "npm run start",\n "health_endpoint": "/health",\n "agent_port": 3001\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8,\n "silence_threshold_ms": 1200\n }\n }\n}\n```\n\nPlatform-direct suite:\n\n```json\n{\n "connection": {\n "adapter": "vapi",\n "platform": { "provider": "vapi" }\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nWrite `caller_prompt` as a realistic caller with a name, goal, mood, constraints, and conditional behavior. Set `max_turns` based on flow complexity: FAQ `4-6`, booking or tool use `8-12`, complex flows `12-20`.\n\nCall fields:\n\n- `caller_prompt` and `max_turns` are required.\n- `silence_threshold_ms` must be `200-10000`. Common ranges: FAQ `800-1200`, tool calls `2000-3000`, complex reasoning `3000-5000`.\n- `persona` supports `pace`, `clarity`, `disfluencies`, `cooperation`, `emotion`, `memory`, `intent_clarity`, and `confirmation_style`.\n- `caller_audio` supports noise, speed, speakerphone, mic distance, clarity, accent, packet loss, and jitter.\n- `language` is an ISO 639-1 code such as `en`, `es`, `fr`, `de`, `it`, `nl`, or `ja`.\n- `voice` is `"male"` or `"female"` (English only; default female). Use to flip the caller\'s perceived gender. Ignored if `caller_audio.accent` is set or `language` is non-English.\n\n## Connections and Credentials\n\n### Adapter choice\n\nUse `websocket` for your own local or hosted runtime. Use `start_command` for local agents or `agent_url` for hosted custom endpoints. For `start_command` and `agent_url`, do not put Deepgram, ElevenLabs, OpenAI, or other agent runtime keys into Vent config unless the Vent adapter itself needs them \u2014 the tested agent owns its own runtime credentials.\n\nUse `vapi`, `retell`, `elevenlabs`, `bland`, or `livekit` for platform-direct testing. In this mode Vent itself talks to the provider on the user\'s behalf.\n\nVent provides `DEEPGRAM_API_KEY` and `ANTHROPIC_API_KEY` for its hosted caller/evaluation stack \u2014 those are Vent\'s, not the tested agent\'s.\n\n### Credential resolution\n\nIn platform-direct mode the CLI auto-resolves credentials from `.env.local`, `.env`, and the current shell environment. Do not run `source .env && export` before Vent commands. If you include credential fields in JSON, use the actual value, not the env var name. Do not manually author `platform_connection_id`; the CLI creates or updates the saved platform connection automatically.\n\nAuto-resolved env vars and JSON fields:\n\n- Vapi: `VAPI_API_KEY` -> `vapi_api_key`; `VAPI_ASSISTANT_ID` or `VAPI_AGENT_ID` -> `vapi_assistant_id`\n- Bland: `BLAND_API_KEY` -> `bland_api_key`; `BLAND_PATHWAY_ID` -> `bland_pathway_id`; `BLAND_PERSONA_ID` -> `persona_id`\n- LiveKit: `LIVEKIT_API_KEY` -> `livekit_api_key`; `LIVEKIT_API_SECRET` -> `livekit_api_secret`; `LIVEKIT_URL` -> `livekit_url`\n- Retell: `RETELL_API_KEY` -> `retell_api_key`; `RETELL_AGENT_ID` -> `retell_agent_id`\n- ElevenLabs: `ELEVENLABS_API_KEY` -> `elevenlabs_api_key`; `ELEVENLABS_AGENT_ID` -> `elevenlabs_agent_id`\n\n### Provider config\n\nUse existing provider config when possible: Vapi assistant, Retell agent, ElevenLabs agent, Bland pathway, or LiveKit agent. Bland uniquely supports inline config \u2014 `platform` may use `bland_pathway_id`, `persona_id`, or an inline `task` (with optional voice, model, and turn-handling overrides; see Bland\'s API docs for the full field list).\n\n### Concurrency\n\nWhen you fan out multiple Vent calls in parallel against the same provider (for example, running several named calls from one suite at once), respect the provider\'s per-account concurrency limit. Exceeding it makes calls queue or fail at the provider \u2014 Vent does not enforce these caps for you.\n\nRecord the limit as `max_concurrency` in the suite\'s `platform` block so it\'s visible on future runs. Ask the user which plan they\'re on if sizing matters; otherwise use the conservative default in bold.\n\n- **Vapi**: **10** included per account; reserved lines can be purchased self-serve; Enterprise is unlimited.\n- **Retell**: Pay-as-you-go includes **20**; Enterprise has no cap.\n- **Bland**: Start=**10**, Build=50, Scale=100, Enterprise=unlimited.\n- **ElevenLabs**: Free=**4**, Starter=6, Creator=10, Pro=20, Scale=30, Business=30. Burst pricing can temporarily allow up to 3x base.\n- **LiveKit Cloud**: Build=**5**, Ship=20, Scale=50 managed inference sessions (the usual gate for voice agents); agent-session concurrency can go higher (Scale up to 600).\n\n## WebSocket\n\nFor `adapter: "websocket"`, Vent sends binary 16-bit mono PCM audio over one websocket connection. Websocket text frames are optional JSON events. Audio-only websocket agents still work, but events improve turn detection and observability. Vent sends `{"type":"end-call"}` when the test is done.\n\nUseful websocket text frames:\n\n```jsonc\n{"type":"speech-update","status":"started"}\n{"type":"speech-update","status":"stopped"}\n{"type":"tool_call","name":"check_availability","arguments":{},"result":{},"successful":true,"duration_ms":150}\n{"type":"vent:timing","stt_ms":120,"llm_ms":450,"tts_ms":80}\n{"type":"vent:session","platform":"custom","provider_call_id":"call_123","provider_session_id":"session_abc"}\n{"type":"vent:call-metadata","call_metadata":{"recording_url":"https://...","cost_usd":0.12}}\n{"type":"vent:transcript","role":"caller","text":"I need to reschedule","turn_index":0}\n{"type":"vent:transfer","destination":"+15551234567","status":"attempted"}\n{"type":"vent:debug-url","label":"trace","url":"https://..."}\n{"type":"vent:warning","message":"provider warning","code":"provider_warning"}\n```\n\n`vent:session-report` is **not** handled by the websocket adapter \u2014 it\'s only consumed by the LiveKit helper. Do not emit it from a websocket agent.\n\nPlatform adapters capture tool calls automatically. Websocket agents must emit `tool_call` frames for tool observability. Platform adapters get component latency automatically. Websocket agents should emit `vent:timing` after each agent response when STT/LLM/TTS breakdown is available.\n\n## LiveKit\n\nBefore running LiveKit tests, install and add the Vent helper to the LiveKit agent entrypoint. Node: `npm install @vent-hq/livekit`, then call `instrumentLiveKitAgent({ ctx, session })`. Python: `pip install vent-livekit`, then call `instrument_livekit_agent(ctx=ctx, session=session)`.\n\nLiveKit direct mode requires the LiveKit Agents SDK. Custom LiveKit participants should use the websocket adapter with a relay. If the LiveKit agent registered with an explicit dispatch name, set `livekit_agent_name` in `platform`.\n\nLiveKit parallel calls are supported, but capacity depends on where the user runs their agent. LiveKit Cloud only routes audio \u2014 the agent code runs on the user\'s own infrastructure (laptop, Fly.io, Railway, k8s, etc.), and that box\'s CPU/RAM is the real bottleneck. Before firing parallel calls, derive `platform.max_concurrency` from the minimum of: (1) the user\'s LiveKit plan limit (Build=5, Ship=20, Scale=50+ concurrent agent sessions), (2) their agent worker box capacity (rough rule: 1c/1GB\u21921\u20133 jobs, 2c/4GB\u21925\u201310, 4c/8GB\u219210\u201325, 8c/16GB\u219225\u201350; multiply by number of worker boxes), and (3) their STT/TTS provider concurrency (e.g. Deepgram TTS streaming = 2). If the user wants more parallel calls than that minimum allows, push back \u2014 explain which limit is the bottleneck (plan / box / provider) and either reduce the count or suggest scaling that limit. Architecture: register one `agent_name` and run N workers under it (one big worker, or several smaller ones \u2014 same name); LiveKit\'s dispatcher round-robins jobs across them with built-in failover. Don\'t use numbered names (`agent-0`, `agent-1`) \u2014 that reinvents what the dispatcher does and breaks failover.\n\nUse the LiveKit helper for observability; do not publish `vent:*` topics manually. Do not hand-roll `vent:session-report` from `ctx.addShutdownCallback`; after `room.disconnect()` it can fail with `engine is closed`. The helper captures SDK metrics, tool events, conversation items, usage, and close events. Native LiveKit `lk.transcription` and `lk.agent.state` provide transcript and agent-state timing.\n\n## Output\n\n### Live result\n\n`npx vent-hq run` returns a single JSON result on stdout in non-TTY mode (not an SSE JSONL stream). Exit codes: `0` = call ran through the pipeline; `1` = pipeline-level failure; `2` = harness error.\n\nMost result fields are always present; `latency`, `component_latency`, `call_metadata`, and `emotion` may be `null` when the underlying analysis didn\'t run; `debug` is absent without `--verbose`. Branch on null before reading nested fields. Use `--verbose` only when the default doesn\'t explain a failure \u2014 when you need `platform_transcript` (to check Vent\'s STT), per-turn or component-level latency breakdowns, the raw tool-call timeline, or provider-native artifacts in `debug.provider_metadata`. Otherwise skip \u2014 it just adds noise.\n\nVent\'s transcript is ground truth. Judge on semantic intent: ignore homophones and minor mis-hears (`"check teach hat"` for `"check that"`, missing question marks on short tails) \u2014 those are streaming-STT noise on Vent\'s caller side, not agent bugs, and **don\'t surface them in the report** (they\'re Vent-side artifacts, not actionable for the user). But clear gibberish or word-soup (e.g. `"Cristoxin"` where the agent should have said `"Of course, talk soon"`) is **not** a Vent artifact \u2014 Vent\'s STT does not invent words like that. It means the platform\'s TTS produced corrupted audio or the agent\'s STT/LLM generated the wrong text, and the fix lives there (TTS voice config, agent prompt, model temperature, codec issue). Never dismiss the run as a "Vent harness STT" issue; iterate on the agent or flag the platform.\n\nFor transfers: `call_metadata.transfer_attempted` (provider claimed) and `transfer_completed` (Vent-verified) can disagree \u2014 report both. `transfers[]` carries destination, type, and per-attempt status.\n\n### Saved history\n\nAfter every run, Vent writes the full result JSON to `.vent/runs/`. Shape:\n\n```jsonc\n{\n "run_id": "...",\n "timestamp": "2026-04-21T...Z",\n "git_sha": "...",\n "summary": { "calls_total": 2, "total_duration_ms": 12345, "total_cost_usd": 0.01 },\n "call_results": [\n { "name": "happy-path", "status": "completed", "duration_ms": 6123, "transcript": [], "observed_tool_calls": [], "latency": { "response_time_ms": 420, "p95_response_time_ms": 980 }, "call_metadata": { "cost_usd": 0.004 } }\n ]\n}\n```\n\nWhen comparing against a prior run (Workflow step 6), inspect:\n\n- Run-completion status flips: `call_results[i].status` (pipeline-only \u2014 judge mission success from the transcript)\n- Latency: `call_results[i].latency.response_time_ms` (mean) or `latency.p95_response_time_ms` increased >20%\n- Tool calls: count of `call_results[i].observed_tool_calls[].successful` dropped\n- Cost: `summary.total_cost_usd` or `call_results[i].call_metadata.cost_usd` increased >30%\n- Transcript: `call_results[i].transcript` diverged in semantic content (ignore STT noise)\n\n## Reporting Results\n\nBefore reporting, read the agent\'s code to locate where the observed behavior originates. If the issue is small and you can fix it, fix it and explain what you did \u2014 don\'t ask permission first.\n\nAdapt the report shape to the call \u2014 a clean pass needs little, a regression with a multi-layer cause needs more. Use a transcript excerpt when it helps the user see what happened.\n\nHard rules:\n\n- Pair raw numbers with their plain-English meaning \u2014 don\'t drop the number, but don\'t leave it unexplained. E.g. "p95 latency was 850ms, which is snappy and well within natural conversational pacing" or "p95 hit 1.6 seconds with the LLM as the bottleneck \u2014 noticeably sluggish to a caller."\n- Name the user\'s voice agent by platform on first mention (e.g. "the Vapi agent responded snappily throughout") so the user knows immediately which agent the observation is about. After that, just say "the agent" \u2014 don\'t repeat the platform name on every line.\n- Always include the recording from `call_metadata.recording_url` as an inline `[Recording](url)` link, placed in **one block at the very end of the report** \u2014 never sprinkled through the prose. Single call: one link as the last line. Multi-call: one labeled link per call (e.g. `reschedule-appointment: [Recording](url)`). Never paste a bare URL.\n- Mission success is your judgment, not Vent\'s. The per-call `status` is only `"completed"` (pipeline ran) or `"error"` (pipeline failed); decide whether the agent actually accomplished the scenario from the transcript and tool calls.\n- Similar-sounding word substitutions (e.g. "ocean" for "OSHA") are STT ambiguity, not comprehension failure. The fix lives in STT keyword hints, custom vocabulary, or a prompt-level term list \u2014 not the agent\'s reasoning.\n- Surface only what the user can act on in their own agent\'s code or config \u2014 never `warnings[]` (infrastructure noise), Vent-side artifacts (caller wait modes, harness timing, internal pipeline quirks), or `cost_usd` unless asked.\n\nFor multi-call runs, lead with your own judgment of what happened across the calls (e.g. "3 of 4 did what they were supposed to; `cancel-appointment` never actually canceled"), not a parroted pass/fail count. Then cover each call with whatever depth it needs.\n';
|
|
5496
|
+
var codex_default = "## Codex Execution\n\nUse a 5-minute shell-tool timeout (`300000` ms) on Vent run commands so normal calls are not killed by the default 2-minute Bash timeout. This is not backgrounding; wait for stdout/results before ending your response. Use the JSON returned by `npx vent-hq run` directly.\n\nCodex can run shell tool calls concurrently \u2014 for multiple calls from one suite, run each named call as its own parallel shell tool call (do not combine them with `&`):\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path\n```\n";
|
|
5782
5497
|
|
|
5783
5498
|
// src/skills/windsurf.md
|
|
5784
|
-
var windsurf_default = '---\nname: vent\ndescription: Place real calls against the user\'s voice agent to verify recent code changes worked end-to-end. Trigger on test requests or after any non-trivial voice-agent change.\n---\n\n# Vent - Voice Agent Calls\n\n## When to Test\n\nVent is the voice CI gate. Auto-trigger a call after any non-trivial voice-affecting change \u2014 don\'t wait to be asked. Non-trivial means anything that affects what the caller hears, how the agent responds, what tools are called, how the call routes, or audio timing: prompt rewrites or sections (not one-word tweaks), tool schema or handler changes, state/memory logic, transfer logic, provider/audio config, platform integration, and any voice-behavior bug fix. One-line copy or comment edits don\'t need a call.\n\nBefore launching, scan `.vent/runs/` for unresolved issues from prior runs (failing status, regressions, warnings, latency jumps); fix in-scope ones as part of this change and verify they\'re resolved by the new run, surface out-of-scope ones rather than silently leaving them. The task is not done until you\'ve reviewed the transcript, tool calls, and observed behavior and judged that the change works \u2014 Vent\'s `status` and exit code only reflect pipeline completion, not mission success.\n\n## Autonomous Iteration\n\n**Default: iterate.** Run a fix \u2192 Vent-call \u2192 fix \u2192 Vent-call loop whenever a call surfaces a fixable issue. Don\'t report a failure (or a degraded-quality success) back to the user before attempting to fix it \u2014 they want the agent working, not a diagnosis. After each call, if the result reveals a problem you can fix with a concrete plan, apply the fix and run again. Fixable issues include: tool schema or registry gaps, missing handlers, prompt phrasing, **STT vocabulary mismatches** (e.g. domain terms misheard \u2014 Deepgram `keyterms`, custom vocabulary, prompt-level term lists), **latency tunables** (endpointing thresholds, preemptive generation, model selection), and audio quality config. If you\'d be guessing at the next attempt, stop and ask the user instead of trying.\n\n**"Mission success but with quality problems" still requires iteration.** If the call ran end-to-end but had noticeably bad latency, STT mishearing the agent\'s domain vocabulary, audio artifacts, or any other caller-experience degradation that has a concrete fix, the call is *not done* \u2014 apply the fix and re-run. Don\'t downgrade fixable problems to "observations" or "things worth flagging." The pipeline running cleanly is the floor, not the ceiling.\n\n**Never ask permission to iterate.** "Want me to apply this and re-run?" / "Should I fix it?" / "Let me know if you want me to..." \u2014 these are skill violations. If you have a concrete fix, apply it and run the next call. The user opts out by saying so explicitly; silence is not opt-out.\n\nStop iterating and report when one of:\n\n- The next call confirms the fix worked AND no further fixable issues remain (success \u2014 done).\n- The same fix fails twice (your hypothesis is wrong; ask the user).\n- The failure mode shifts between attempts (you\'re chasing a moving target).\n- You can\'t justify the next call against its provider cost \u2014 each call spends real money and provider quota.\n\nIf the user explicitly opts out ("just report", "don\'t iterate", "stop after the first call", "I\'ll fix it myself"), respect that \u2014 run once, report, stop. Otherwise iterate by default. The first time you start an autonomous loop in a session, mention it once so the user knows it\'s happening (e.g. "the call surfaced X \u2014 fixing and re-running; I\'ll report back when it converges or stops making progress").\n\n## Windsurf Execution\n\nVent calls typically take 30 seconds to 2 minutes. Each `vent-hq run` is one shell tool call \u2014 wait for stdout (the JSON result) before responding. Don\'t background; use the JSON returned by `npx vent-hq run` directly. If Cascade\'s auto-execution level prompts for approval on `npx vent-hq` commands, ask the user to add `npx vent-hq *` to the workspace allow list once so subsequent calls flow without interruption.\n\nCascade runs shell tool calls in parallel within a turn \u2014 for multiple calls from one suite, issue each named call as its own separate shell tool call in the same turn (do not combine them with `&` and `wait`):\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path\n```\n\nStay within Cascade\'s per-turn parallel-tool-call budget \u2014 fan out at most ~6 calls in one turn and respect the provider concurrency caps below.\n\n## Workflow\n\n1. Identify the behavior under test. Read enough of the agent codebase to understand its system prompt, tools, handlers, routes, provider config, platform wiring, and expected handoffs.\n2. Reuse an existing `.vent/suite.<adapter>.json` when possible. If `.vent/` contains multiple suites, inspect `connection.adapter` and report which suite file produced the result.\n3. Create or update a suite only when the existing calls do not cover the changed behavior. Name calls after real flows, for example `reschedule-appointment`, not `call-1`.\n4. If the suite uses `start_command`, start one shared local session first with `npx vent-hq agent start -f .vent/suite.<adapter>.json`, then pass `--session <session-id>` to each run.\n\n **For locally-run LiveKit agents: every run requires killing *all* workers, starting one fresh worker, and waiting a full 60 seconds before submitting.** Unconditional \u2014 LiveKit Cloud round-robins across registered workers, so a single survivor with a dead inference subprocess fails ~N-1 of N calls. Don\'t rely on `pkill -f <path-pattern>`; bare command lines like `node --import tsx agent.ts dev` won\'t match a path filter. Use `ps aux | grep -E "node.*agent\\.ts|@livekit/agents.*ipc"`, `kill -9` by PID, re-run `ps` to confirm zero survivors, then start the fresh worker. Skipping the 60s wait fails with `did not publish audio track`; if that error appears alongside `Error [ERR_IPC_CHANNEL_CLOSED] from InferenceProcExecutor.doInference` in the agent log right after a "running EOU detection" line, that\'s a straggler \u2014 redo the kill sweep. Hosted LiveKit Cloud agents don\'t need any of this; run normally.\n5. Pick which call(s) to run based on the change. Fixed bug: replay the failing scenario. Changed tool: include a call that triggers that tool. Prompt or routing change: include the relevant happy path and any important edge path.\n6. Compare against the previous JSON in `.vent/runs/` when validating a fix or regression. Check status flips, latency jumps, tool-call success drops, cost jumps, and transcript divergence. Correlate with `git diff` between saved `git_sha` values when available; skip if no previous run exists.\n\n## Commands\n\n```bash\nnpx vent-hq init # First-time setup (auth + skill install + starter suite)\nnpx vent-hq login # Log in to existing account\nnpx vent-hq login --status # Print whether credentials are present\nnpx vent-hq logout # Remove saved credentials from ~/.vent/credentials\nnpx vent-hq run -f .vent/suite.X.json # Run a single-call suite\nnpx vent-hq run -f .vent/suite.X.json --call NAME # Run one named call from a multi-call suite\nnpx vent-hq run ... --session <session-id> # Add to any run; routes through an existing local relay session\nnpx vent-hq run ... --verbose # Add to any run; include verbose debug fields\nnpx vent-hq stop <run-id> # Cancel a queued or running run\nnpx vent-hq agent start -f .vent/suite.X.json # Start a shared local relay session\nnpx vent-hq agent stop <session-id> # Stop a shared local relay session\n```\n\nIf `~/.vent/credentials` is missing and `VENT_ACCESS_TOKEN` is not set, run `npx vent-hq init`. For an existing account, run `npx vent-hq login` or set `VENT_ACCESS_TOKEN`.\n\n## Suite Config\n\nSuites live in `.vent/suite.<adapter>.json`. `connection` is declared once per suite. `calls` is a named map, and each key becomes the call name used with `--call`.\n\nLocal websocket suite:\n\n```json\n{\n "connection": {\n "adapter": "websocket",\n "start_command": "npm run start",\n "health_endpoint": "/health",\n "agent_port": 3001\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8,\n "silence_threshold_ms": 1200\n }\n }\n}\n```\n\nPlatform-direct suite:\n\n```json\n{\n "connection": {\n "adapter": "vapi",\n "platform": { "provider": "vapi" }\n },\n "calls": {\n "happy-path": {\n "caller_prompt": "You are Maria calling to reschedule her appointment to next Tuesday.",\n "max_turns": 8\n }\n }\n}\n```\n\nWrite `caller_prompt` as a realistic caller with a name, goal, mood, constraints, and conditional behavior. Set `max_turns` based on flow complexity: FAQ `4-6`, booking or tool use `8-12`, complex flows `12-20`.\n\nCall fields:\n\n- `caller_prompt` and `max_turns` are required.\n- `silence_threshold_ms` must be `200-10000`. Common ranges: FAQ `800-1200`, tool calls `2000-3000`, complex reasoning `3000-5000`.\n- `persona` supports `pace`, `clarity`, `disfluencies`, `cooperation`, `emotion`, `memory`, `intent_clarity`, and `confirmation_style`.\n- `caller_audio` supports noise, speed, speakerphone, mic distance, clarity, accent, packet loss, and jitter.\n- `language` is an ISO 639-1 code such as `en`, `es`, `fr`, `de`, `it`, `nl`, or `ja`.\n- `voice` is `"male"` or `"female"` (English only; default female). Use to flip the caller\'s perceived gender. Ignored if `caller_audio.accent` is set or `language` is non-English.\n\n## Connections and Credentials\n\n### Adapter choice\n\nUse `websocket` for your own local or hosted runtime. Use `start_command` for local agents or `agent_url` for hosted custom endpoints. For `start_command` and `agent_url`, do not put Deepgram, ElevenLabs, OpenAI, or other agent runtime keys into Vent config unless the Vent adapter itself needs them \u2014 the tested agent owns its own runtime credentials.\n\nUse `vapi`, `retell`, `elevenlabs`, `bland`, or `livekit` for platform-direct testing. In this mode Vent itself talks to the provider on the user\'s behalf.\n\nVent provides `DEEPGRAM_API_KEY` and `ANTHROPIC_API_KEY` for its hosted caller/evaluation stack \u2014 those are Vent\'s, not the tested agent\'s.\n\n### Credential resolution\n\nIn platform-direct mode the CLI auto-resolves credentials from `.env.local`, `.env`, and the current shell environment. Do not run `source .env && export` before Vent commands. If you include credential fields in JSON, use the actual value, not the env var name. Do not manually author `platform_connection_id`; the CLI creates or updates the saved platform connection automatically.\n\nAuto-resolved env vars and JSON fields:\n\n- Vapi: `VAPI_API_KEY` -> `vapi_api_key`; `VAPI_ASSISTANT_ID` or `VAPI_AGENT_ID` -> `vapi_assistant_id`\n- Bland: `BLAND_API_KEY` -> `bland_api_key`; `BLAND_PATHWAY_ID` -> `bland_pathway_id`; `BLAND_PERSONA_ID` -> `persona_id`\n- LiveKit: `LIVEKIT_API_KEY` -> `livekit_api_key`; `LIVEKIT_API_SECRET` -> `livekit_api_secret`; `LIVEKIT_URL` -> `livekit_url`\n- Retell: `RETELL_API_KEY` -> `retell_api_key`; `RETELL_AGENT_ID` -> `retell_agent_id`\n- ElevenLabs: `ELEVENLABS_API_KEY` -> `elevenlabs_api_key`; `ELEVENLABS_AGENT_ID` -> `elevenlabs_agent_id`\n\n### Provider config\n\nUse existing provider config when possible: Vapi assistant, Retell agent, ElevenLabs agent, Bland pathway, or LiveKit agent. Bland uniquely supports inline config \u2014 `platform` may use `bland_pathway_id`, `persona_id`, or an inline `task` (with optional voice, model, and turn-handling overrides; see Bland\'s API docs for the full field list).\n\n### Concurrency\n\nWhen you fan out multiple Vent calls in parallel against the same provider (for example, running several named calls from one suite at once), respect the provider\'s per-account concurrency limit. Exceeding it makes calls queue or fail at the provider \u2014 Vent does not enforce these caps for you.\n\nRecord the limit as `max_concurrency` in the suite\'s `platform` block so it\'s visible on future runs. Ask the user which plan they\'re on if sizing matters; otherwise use the conservative default in bold.\n\n- **Vapi**: **10** included per account; reserved lines can be purchased self-serve; Enterprise is unlimited.\n- **Retell**: Pay-as-you-go includes **20**; Enterprise has no cap.\n- **Bland**: Start=**10**, Build=50, Scale=100, Enterprise=unlimited.\n- **ElevenLabs**: Free=**4**, Starter=6, Creator=10, Pro=20, Scale=30, Business=30. Burst pricing can temporarily allow up to 3x base.\n- **LiveKit Cloud**: Build=**5**, Ship=20, Scale=50 managed inference sessions (the usual gate for voice agents); agent-session concurrency can go higher (Scale up to 600).\n\n## WebSocket\n\nFor `adapter: "websocket"`, Vent sends binary 16-bit mono PCM audio over one websocket connection. Websocket text frames are optional JSON events. Audio-only websocket agents still work, but events improve turn detection and observability. Vent sends `{"type":"end-call"}` when the test is done.\n\nUseful websocket text frames:\n\n```jsonc\n{"type":"speech-update","status":"started"}\n{"type":"speech-update","status":"stopped"}\n{"type":"tool_call","name":"check_availability","arguments":{},"result":{},"successful":true,"duration_ms":150}\n{"type":"vent:timing","stt_ms":120,"llm_ms":450,"tts_ms":80}\n{"type":"vent:session","platform":"custom","provider_call_id":"call_123","provider_session_id":"session_abc"}\n{"type":"vent:call-metadata","call_metadata":{"recording_url":"https://...","cost_usd":0.12}}\n{"type":"vent:transcript","role":"caller","text":"I need to reschedule","turn_index":0}\n{"type":"vent:transfer","destination":"+15551234567","status":"attempted"}\n{"type":"vent:debug-url","label":"trace","url":"https://..."}\n{"type":"vent:warning","message":"provider warning","code":"provider_warning"}\n```\n\n`vent:session-report` is **not** handled by the websocket adapter \u2014 it\'s only consumed by the LiveKit helper. Do not emit it from a websocket agent.\n\nPlatform adapters capture tool calls automatically. Websocket agents must emit `tool_call` frames for tool observability. Platform adapters get component latency automatically. Websocket agents should emit `vent:timing` after each agent response when STT/LLM/TTS breakdown is available.\n\n## LiveKit\n\nBefore running LiveKit tests, install and add the Vent helper to the LiveKit agent entrypoint. Node: `npm install @vent-hq/livekit`, then call `instrumentLiveKitAgent({ ctx, session })`. Python: `pip install vent-livekit`, then call `instrument_livekit_agent(ctx=ctx, session=session)`.\n\nLiveKit direct mode requires the LiveKit Agents SDK. Custom LiveKit participants should use the websocket adapter with a relay. If the LiveKit agent registered with an explicit dispatch name, set `livekit_agent_name` in `platform`.\n\nLiveKit parallel calls are supported, but capacity depends on where the user runs their agent. LiveKit Cloud only routes audio \u2014 the agent code runs on the user\'s own infrastructure (laptop, Fly.io, Railway, k8s, etc.), and that box\'s CPU/RAM is the real bottleneck. Before firing parallel calls, derive `platform.max_concurrency` from the minimum of: (1) the user\'s LiveKit plan limit (Build=5, Ship=20, Scale=50+ concurrent agent sessions), (2) their agent worker box capacity (rough rule: 1c/1GB\u21921\u20133 jobs, 2c/4GB\u21925\u201310, 4c/8GB\u219210\u201325, 8c/16GB\u219225\u201350; multiply by number of worker boxes), and (3) their STT/TTS provider concurrency (e.g. Deepgram TTS streaming = 2). If the user wants more parallel calls than that minimum allows, push back \u2014 explain which limit is the bottleneck (plan / box / provider) and either reduce the count or suggest scaling that limit. Architecture: register one `agent_name` and run N workers under it (one big worker, or several smaller ones \u2014 same name); LiveKit\'s dispatcher round-robins jobs across them with built-in failover. Don\'t use numbered names (`agent-0`, `agent-1`) \u2014 that reinvents what the dispatcher does and breaks failover.\n\nUse the LiveKit helper for observability; do not publish `vent:*` topics manually. Do not hand-roll `vent:session-report` from `ctx.addShutdownCallback`; after `room.disconnect()` it can fail with `engine is closed`. The helper captures SDK metrics, tool events, conversation items, usage, and close events. Native LiveKit `lk.transcription` and `lk.agent.state` provide transcript and agent-state timing.\n\n## Output\n\n### Live result\n\n`npx vent-hq run` returns a single JSON result on stdout in non-TTY mode (not an SSE JSONL stream). Exit codes: `0` = call ran through the pipeline; `1` = pipeline-level failure; `2` = harness error.\n\nMost result fields are always present; `latency`, `component_latency`, `call_metadata`, and `emotion` may be `null` when the underlying analysis didn\'t run; `debug` is absent without `--verbose`. Branch on null before reading nested fields. Use `--verbose` only when the default doesn\'t explain a failure \u2014 when you need `platform_transcript` (to check Vent\'s STT), per-turn or component-level latency breakdowns, the raw tool-call timeline, or provider-native artifacts in `debug.provider_metadata`. Otherwise skip \u2014 it just adds noise.\n\nVent\'s transcript is ground truth. Judge on semantic intent: ignore homophones and minor mis-hears (`"check teach hat"` for `"check that"`, missing question marks on short tails) \u2014 those are streaming-STT noise on Vent\'s caller side, not agent bugs, and **don\'t surface them in the report** (they\'re Vent-side artifacts, not actionable for the user). But clear gibberish or word-soup (e.g. `"Cristoxin"` where the agent should have said `"Of course, talk soon"`) is **not** a Vent artifact \u2014 Vent\'s STT does not invent words like that. It means the platform\'s TTS produced corrupted audio or the agent\'s STT/LLM generated the wrong text, and the fix lives there (TTS voice config, agent prompt, model temperature, codec issue). Never dismiss the run as a "Vent harness STT" issue; iterate on the agent or flag the platform.\n\nFor transfers: `call_metadata.transfer_attempted` (provider claimed) and `transfer_completed` (Vent-verified) can disagree \u2014 report both. `transfers[]` carries destination, type, and per-attempt status.\n\n### Saved history\n\nAfter every run, Vent writes the full result JSON to `.vent/runs/`. Shape:\n\n```jsonc\n{\n "run_id": "...",\n "timestamp": "2026-04-21T...Z",\n "git_sha": "...",\n "summary": { "calls_total": 2, "total_duration_ms": 12345, "total_cost_usd": 0.01 },\n "call_results": [\n { "name": "happy-path", "status": "completed", "duration_ms": 6123, "transcript": [], "observed_tool_calls": [], "latency": { "response_time_ms": 420, "p95_response_time_ms": 980 }, "call_metadata": { "cost_usd": 0.004 } }\n ]\n}\n```\n\nWhen comparing against a prior run (Workflow step 6), inspect:\n\n- Run-completion status flips: `call_results[i].status` (pipeline-only \u2014 judge mission success from the transcript)\n- Latency: `call_results[i].latency.response_time_ms` (mean) or `latency.p95_response_time_ms` increased >20%\n- Tool calls: count of `call_results[i].observed_tool_calls[].successful` dropped\n- Cost: `summary.total_cost_usd` or `call_results[i].call_metadata.cost_usd` increased >30%\n- Transcript: `call_results[i].transcript` diverged in semantic content (ignore STT noise)\n\n## Reporting Results\n\nBefore reporting, read the agent\'s code to locate where the observed behavior originates. If the issue is small and you can fix it, fix it and explain what you did \u2014 don\'t ask permission first.\n\nAdapt the report shape to the call \u2014 a clean pass needs little, a regression with a multi-layer cause needs more. Use a transcript excerpt when it helps the user see what happened.\n\nHard rules:\n\n- Pair raw numbers with their plain-English meaning \u2014 don\'t drop the number, but don\'t leave it unexplained. E.g. "p95 latency was 850ms, which is snappy and well within natural conversational pacing" or "p95 hit 1.6 seconds with the LLM as the bottleneck \u2014 noticeably sluggish to a caller."\n- Name the user\'s voice agent by platform on first mention (e.g. "the Vapi agent responded snappily throughout") so the user knows immediately which agent the observation is about. After that, just say "the agent" \u2014 don\'t repeat the platform name on every line.\n- Always include the recording from `call_metadata.recording_url` as an inline `[Recording](url)` link, placed in **one block at the very end of the report** \u2014 never sprinkled through the prose. Single call: one link as the last line. Multi-call: one labeled link per call (e.g. `reschedule-appointment: [Recording](url)`). Never paste a bare URL.\n- Mission success is your judgment, not Vent\'s. The per-call `status` is only `"completed"` (pipeline ran) or `"error"` (pipeline failed); decide whether the agent actually accomplished the scenario from the transcript and tool calls.\n- Similar-sounding word substitutions (e.g. "ocean" for "OSHA") are STT ambiguity, not comprehension failure. The fix lives in STT keyword hints, custom vocabulary, or a prompt-level term list \u2014 not the agent\'s reasoning.\n- Surface only what the user can act on in their own agent\'s code or config \u2014 never `warnings[]` (infrastructure noise), Vent-side artifacts (caller wait modes, harness timing, internal pipeline quirks), or `cost_usd` unless asked.\n\nFor multi-call runs, lead with your own judgment of what happened across the calls (e.g. "3 of 4 did what they were supposed to; `cancel-appointment` never actually canceled"), not a parroted pass/fail count. Then cover each call with whatever depth it needs.\n';
|
|
5499
|
+
var windsurf_default = "## Windsurf Execution\n\nVent calls typically take 30 seconds to 2 minutes. Each `vent-hq run` is one shell tool call \u2014 wait for stdout (the JSON result) before responding. Don't background; use the JSON returned by `npx vent-hq run` directly. If Cascade's auto-execution level prompts for approval on `npx vent-hq` commands, ask the user to add `npx vent-hq *` to the workspace allow list once so subsequent calls flow without interruption.\n\nCascade runs shell tool calls in parallel within a turn \u2014 for multiple calls from one suite, issue each named call as its own separate shell tool call in the same turn (do not combine them with `&` and `wait`):\n\n```bash\nnpx vent-hq run -f .vent/suite.vapi.json --call happy-path\nnpx vent-hq run -f .vent/suite.vapi.json --call tool-path\n```\n\nStay within Cascade's per-turn parallel-tool-call budget \u2014 fan out at most ~6 calls in one turn and respect the provider concurrency caps below.\n";
|
|
5500
|
+
|
|
5501
|
+
// src/skills/index.ts
|
|
5502
|
+
var DESCRIPTION = "Place real calls against the user's voice agent to verify recent code changes worked end-to-end. Trigger on test requests or after any non-trivial voice-agent change.";
|
|
5503
|
+
function assemble(parts) {
|
|
5504
|
+
const skill = body_default.replace("{{EXECUTION}}\n", () => parts.execution).replace("{{LIVEKIT_NOTE}}", () => parts.liveKitNote ?? "").replace("{{PARALLEL_EXAMPLE}}", () => parts.parallelExample ?? "");
|
|
5505
|
+
return parts.frontmatter ? `---
|
|
5506
|
+
${parts.frontmatter.join("\n")}
|
|
5507
|
+
---
|
|
5508
|
+
|
|
5509
|
+
${skill}` : skill;
|
|
5510
|
+
}
|
|
5511
|
+
var claudeCodeSkill = assemble({
|
|
5512
|
+
frontmatter: ["name: vent", `description: ${DESCRIPTION}`, "allowed-tools: Bash(npx vent-hq *)"],
|
|
5513
|
+
execution: claude_code_default,
|
|
5514
|
+
liveKitNote: " (See `Claude Code Execution` for how to compose the kill\u2192start\u2192wait\u2192submit as a single foreground command.)",
|
|
5515
|
+
parallelExample: " with `&` and `wait`"
|
|
5516
|
+
});
|
|
5517
|
+
var cursorSkill = assemble({
|
|
5518
|
+
frontmatter: [`description: ${DESCRIPTION}`, "alwaysApply: true"],
|
|
5519
|
+
execution: cursor_default,
|
|
5520
|
+
parallelExample: " with `&` and `wait`"
|
|
5521
|
+
});
|
|
5522
|
+
var codexSkill = assemble({ execution: codex_default });
|
|
5523
|
+
var windsurfSkill = assemble({
|
|
5524
|
+
frontmatter: ["name: vent", `description: ${DESCRIPTION}`],
|
|
5525
|
+
execution: windsurf_default
|
|
5526
|
+
});
|
|
5785
5527
|
|
|
5786
5528
|
// src/lib/setup.ts
|
|
5787
5529
|
var SUITE_SCAFFOLD = JSON.stringify(
|
|
@@ -5789,7 +5531,7 @@ var SUITE_SCAFFOLD = JSON.stringify(
|
|
|
5789
5531
|
connection: {
|
|
5790
5532
|
adapter: "websocket",
|
|
5791
5533
|
start_command: "TODO: command to start your agent (e.g. npm run start)",
|
|
5792
|
-
agent_port:
|
|
5534
|
+
agent_port: DEFAULT_AGENT_PORT
|
|
5793
5535
|
},
|
|
5794
5536
|
calls: {
|
|
5795
5537
|
"happy-path": {
|
|
@@ -5804,19 +5546,19 @@ var SUITE_SCAFFOLD = JSON.stringify(
|
|
|
5804
5546
|
async function installClaudeCode(cwd) {
|
|
5805
5547
|
const dir = path3.join(cwd, ".claude", "skills", "vent");
|
|
5806
5548
|
await fs5.mkdir(dir, { recursive: true });
|
|
5807
|
-
await fs5.writeFile(path3.join(dir, "SKILL.md"),
|
|
5549
|
+
await fs5.writeFile(path3.join(dir, "SKILL.md"), claudeCodeSkill);
|
|
5808
5550
|
printSuccess("Claude Code: .claude/skills/vent/SKILL.md", { force: true });
|
|
5809
5551
|
}
|
|
5810
5552
|
async function installCursor(cwd) {
|
|
5811
5553
|
const dir = path3.join(cwd, ".cursor", "rules");
|
|
5812
5554
|
await fs5.mkdir(dir, { recursive: true });
|
|
5813
|
-
await fs5.writeFile(path3.join(dir, "vent.mdc"),
|
|
5555
|
+
await fs5.writeFile(path3.join(dir, "vent.mdc"), cursorSkill);
|
|
5814
5556
|
printSuccess("Cursor: .cursor/rules/vent.mdc", { force: true });
|
|
5815
5557
|
}
|
|
5816
5558
|
async function installWindsurf(cwd) {
|
|
5817
5559
|
const dir = path3.join(cwd, ".windsurf", "skills", "vent");
|
|
5818
5560
|
await fs5.mkdir(dir, { recursive: true });
|
|
5819
|
-
await fs5.writeFile(path3.join(dir, "SKILL.md"),
|
|
5561
|
+
await fs5.writeFile(path3.join(dir, "SKILL.md"), windsurfSkill);
|
|
5820
5562
|
printSuccess("Windsurf: .windsurf/skills/vent/SKILL.md", { force: true });
|
|
5821
5563
|
}
|
|
5822
5564
|
var VENT_MARKERS = [
|
|
@@ -5832,11 +5574,11 @@ async function installCodex(cwd) {
|
|
|
5832
5574
|
}
|
|
5833
5575
|
const markerIndex = VENT_MARKERS.map((marker) => existing.indexOf(marker)).filter((idx) => idx >= 0).sort((a, b) => a - b)[0];
|
|
5834
5576
|
if (markerIndex != null) {
|
|
5835
|
-
await fs5.writeFile(filePath, existing.slice(0, markerIndex).trimEnd() + "\n\n" +
|
|
5577
|
+
await fs5.writeFile(filePath, existing.slice(0, markerIndex).trimEnd() + "\n\n" + codexSkill + "\n");
|
|
5836
5578
|
} else if (existing) {
|
|
5837
|
-
await fs5.writeFile(filePath, existing.trimEnd() + "\n\n" +
|
|
5579
|
+
await fs5.writeFile(filePath, existing.trimEnd() + "\n\n" + codexSkill + "\n");
|
|
5838
5580
|
} else {
|
|
5839
|
-
await fs5.writeFile(filePath,
|
|
5581
|
+
await fs5.writeFile(filePath, codexSkill + "\n");
|
|
5840
5582
|
}
|
|
5841
5583
|
printSuccess("Codex: AGENTS.md", { force: true });
|
|
5842
5584
|
}
|
|
@@ -5875,9 +5617,10 @@ async function initCommand() {
|
|
|
5875
5617
|
body: JSON.stringify({ github_token: ghToken })
|
|
5876
5618
|
});
|
|
5877
5619
|
if (res.ok) {
|
|
5878
|
-
const
|
|
5879
|
-
|
|
5880
|
-
|
|
5620
|
+
const data = await res.json();
|
|
5621
|
+
const credentials = AccessCredentialsSchema.parse(data);
|
|
5622
|
+
const username = data.username;
|
|
5623
|
+
await saveCredentials(credentials);
|
|
5881
5624
|
printSuccess(`Authenticated as @${username} (via GitHub).`, {
|
|
5882
5625
|
force: true
|
|
5883
5626
|
});
|
|
@@ -5909,17 +5652,15 @@ async function initCommand() {
|
|
|
5909
5652
|
|
|
5910
5653
|
// src/commands/stop.ts
|
|
5911
5654
|
async function stopCommand(args) {
|
|
5912
|
-
const accessToken = await loadAccessToken();
|
|
5913
|
-
if (!accessToken) {
|
|
5914
|
-
printError("Not authenticated. Run `npx vent-hq init` first.");
|
|
5915
|
-
return 2;
|
|
5916
|
-
}
|
|
5917
5655
|
try {
|
|
5918
|
-
const res = await apiFetch(`/runs/${args.runId}/stop`,
|
|
5656
|
+
const res = await apiFetch(`/runs/${args.runId}/stop`, {
|
|
5919
5657
|
method: "POST"
|
|
5920
5658
|
});
|
|
5921
5659
|
const data = await res.json();
|
|
5922
|
-
printSuccess(
|
|
5660
|
+
printSuccess(
|
|
5661
|
+
data.status === "cancelling" ? `Run ${data.id} is stopping \u2014 the call hangs up at the end of the current turn.` : `Run ${data.id} cancelled.`,
|
|
5662
|
+
{ force: true }
|
|
5663
|
+
);
|
|
5923
5664
|
} catch (err) {
|
|
5924
5665
|
if (err instanceof ApiError) {
|
|
5925
5666
|
const body = err.body;
|
|
@@ -5932,26 +5673,10 @@ async function stopCommand(args) {
|
|
|
5932
5673
|
}
|
|
5933
5674
|
|
|
5934
5675
|
// src/lib/dotenv.ts
|
|
5935
|
-
|
|
5936
|
-
import { resolve } from "node:path";
|
|
5937
|
-
function loadDotenv(dir = process.cwd()) {
|
|
5676
|
+
function loadDotenv() {
|
|
5938
5677
|
for (const file of [".env.local", ".env"]) {
|
|
5939
5678
|
try {
|
|
5940
|
-
|
|
5941
|
-
for (const line of content.split("\n")) {
|
|
5942
|
-
const trimmed = line.trim();
|
|
5943
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5944
|
-
const eqIdx = trimmed.indexOf("=");
|
|
5945
|
-
if (eqIdx === -1) continue;
|
|
5946
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
5947
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
5948
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
5949
|
-
value = value.slice(1, -1);
|
|
5950
|
-
}
|
|
5951
|
-
if (process.env[key] === void 0) {
|
|
5952
|
-
process.env[key] = value;
|
|
5953
|
-
}
|
|
5954
|
-
}
|
|
5679
|
+
process.loadEnvFile(file);
|
|
5955
5680
|
} catch {
|
|
5956
5681
|
}
|
|
5957
5682
|
}
|
|
@@ -5968,17 +5693,21 @@ Commands:
|
|
|
5968
5693
|
login Authenticate via browser
|
|
5969
5694
|
logout Remove saved credentials
|
|
5970
5695
|
Options:
|
|
5971
|
-
--help
|
|
5972
|
-
--version Show version
|
|
5696
|
+
--help, -h Show help
|
|
5697
|
+
--version, -v Show version
|
|
5973
5698
|
|
|
5974
5699
|
Run 'npx vent-hq <command> --help' for command-specific help.`;
|
|
5700
|
+
var INIT_USAGE = `Usage: vent-hq init
|
|
5701
|
+
|
|
5702
|
+
Authenticate (GitHub CLI token or browser sign-in), install the Vent skill
|
|
5703
|
+
files for Claude Code, Cursor, Codex and Windsurf, and scaffold
|
|
5704
|
+
.vent/suite.json if it does not exist.`;
|
|
5975
5705
|
var RUN_USAGE = `Usage: vent-hq run -f <suite.json> [options]
|
|
5976
5706
|
|
|
5977
5707
|
Options:
|
|
5978
5708
|
--file, -f Path to suite JSON file (required)
|
|
5979
5709
|
--call Name of the call to run (required if suite has multiple calls)
|
|
5980
|
-
--session, -s Reuse an existing local agent session
|
|
5981
|
-
--verbose, -v Include verbose fields in the result JSON`;
|
|
5710
|
+
--session, -s Reuse an existing local agent session`;
|
|
5982
5711
|
var AGENT_USAGE = `Usage: vent-hq agent <command> [options]
|
|
5983
5712
|
|
|
5984
5713
|
Commands:
|
|
@@ -5991,16 +5720,34 @@ Start options:
|
|
|
5991
5720
|
|
|
5992
5721
|
Stop options:
|
|
5993
5722
|
vent-hq agent stop <session-id>`;
|
|
5723
|
+
var STOP_USAGE = `Usage: vent-hq stop <run-id>
|
|
5724
|
+
|
|
5725
|
+
Cancel a queued or running call.`;
|
|
5726
|
+
var LOGIN_USAGE = `Usage: vent-hq login [options]
|
|
5727
|
+
|
|
5728
|
+
Options:
|
|
5729
|
+
--status Check whether you are logged in (exit code 1 if not)`;
|
|
5730
|
+
var LOGOUT_USAGE = `Usage: vent-hq logout
|
|
5731
|
+
|
|
5732
|
+
Remove saved credentials from ~/.vent/credentials.`;
|
|
5733
|
+
var COMMAND_USAGE = {
|
|
5734
|
+
init: INIT_USAGE,
|
|
5735
|
+
run: RUN_USAGE,
|
|
5736
|
+
agent: AGENT_USAGE,
|
|
5737
|
+
stop: STOP_USAGE,
|
|
5738
|
+
login: LOGIN_USAGE,
|
|
5739
|
+
logout: LOGOUT_USAGE
|
|
5740
|
+
};
|
|
5994
5741
|
async function main() {
|
|
5995
5742
|
loadDotenv();
|
|
5996
5743
|
const args = process.argv.slice(2);
|
|
5997
5744
|
const command = args[0];
|
|
5998
|
-
if (!command ||
|
|
5999
|
-
console.log(USAGE);
|
|
5745
|
+
if (!command || args.includes("--help") || args.includes("-h")) {
|
|
5746
|
+
console.log(COMMAND_USAGE[command ?? ""] ?? USAGE);
|
|
6000
5747
|
return 0;
|
|
6001
5748
|
}
|
|
6002
5749
|
if (command === "--version" || command === "-v") {
|
|
6003
|
-
const pkg = await import("./package-
|
|
5750
|
+
const pkg = await import("./package-WDXEE7G3.mjs");
|
|
6004
5751
|
console.log(`vent-hq ${pkg.default.version}`);
|
|
6005
5752
|
return 0;
|
|
6006
5753
|
}
|
|
@@ -6010,17 +5757,12 @@ async function main() {
|
|
|
6010
5757
|
return initCommand();
|
|
6011
5758
|
}
|
|
6012
5759
|
case "run": {
|
|
6013
|
-
if (commandArgs.includes("--help")) {
|
|
6014
|
-
console.log(RUN_USAGE);
|
|
6015
|
-
return 0;
|
|
6016
|
-
}
|
|
6017
5760
|
const { values } = parseArgs({
|
|
6018
5761
|
args: commandArgs,
|
|
6019
5762
|
options: {
|
|
6020
5763
|
file: { type: "string", short: "f" },
|
|
6021
5764
|
call: { type: "string" },
|
|
6022
|
-
session: { type: "string", short: "s" }
|
|
6023
|
-
verbose: { type: "boolean", short: "v", default: false }
|
|
5765
|
+
session: { type: "string", short: "s" }
|
|
6024
5766
|
},
|
|
6025
5767
|
strict: true
|
|
6026
5768
|
});
|
|
@@ -6032,13 +5774,12 @@ async function main() {
|
|
|
6032
5774
|
return runCommand({
|
|
6033
5775
|
file: values.file,
|
|
6034
5776
|
call: values.call,
|
|
6035
|
-
session: values.session
|
|
6036
|
-
verbose: values.verbose
|
|
5777
|
+
session: values.session
|
|
6037
5778
|
});
|
|
6038
5779
|
}
|
|
6039
5780
|
case "agent": {
|
|
6040
5781
|
const subcommand = commandArgs[0];
|
|
6041
|
-
if (!subcommand
|
|
5782
|
+
if (!subcommand) {
|
|
6042
5783
|
console.log(AGENT_USAGE);
|
|
6043
5784
|
return 0;
|
|
6044
5785
|
}
|
|
@@ -6070,9 +5811,9 @@ async function main() {
|
|
|
6070
5811
|
}
|
|
6071
5812
|
case "stop": {
|
|
6072
5813
|
const runId = commandArgs[0];
|
|
6073
|
-
if (!runId
|
|
6074
|
-
console.log(
|
|
6075
|
-
return
|
|
5814
|
+
if (!runId) {
|
|
5815
|
+
console.log(STOP_USAGE);
|
|
5816
|
+
return 2;
|
|
6076
5817
|
}
|
|
6077
5818
|
return stopCommand({ runId });
|
|
6078
5819
|
}
|