vent-hq 0.14.0 → 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 +373 -768
- package/dist/{package-ZEOXQ7DY.mjs → package-WDXEE7G3.mjs} +8 -4
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -94,7 +94,7 @@ function decodeJwt(jwt) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// ../shared/src/constants.ts
|
|
97
|
-
var
|
|
97
|
+
var DEFAULT_AGENT_PORT = 3001;
|
|
98
98
|
var CALLER_ACCENTS = [
|
|
99
99
|
"american",
|
|
100
100
|
"british",
|
|
@@ -4152,122 +4152,92 @@ var coerce = {
|
|
|
4152
4152
|
};
|
|
4153
4153
|
var NEVER = INVALID;
|
|
4154
4154
|
|
|
4155
|
-
// ../shared/src/schemas.ts
|
|
4156
|
-
var
|
|
4157
|
-
refresh_token: external_exports.string().regex(/^vent_[0-9a-f]{48}$/)
|
|
4158
|
-
});
|
|
4159
|
-
var AccessCredentialsSchema = external_exports.object({
|
|
4160
|
-
access_token: external_exports.string().min(1),
|
|
4161
|
-
refresh_token: RefreshTokenRequestSchema.shape.refresh_token
|
|
4162
|
-
});
|
|
4163
|
-
var CallerPersonaSchema = external_exports.object({
|
|
4164
|
-
pace: external_exports.enum(["slow", "normal", "fast"]).optional(),
|
|
4165
|
-
clarity: external_exports.enum(["clear", "vague", "rambling"]).optional(),
|
|
4166
|
-
disfluencies: external_exports.boolean().optional(),
|
|
4167
|
-
cooperation: external_exports.enum(["cooperative", "reluctant", "hostile"]).optional(),
|
|
4168
|
-
emotion: external_exports.enum(["neutral", "cheerful", "confused", "frustrated", "skeptical", "rushed"]).optional(),
|
|
4169
|
-
memory: external_exports.enum(["reliable", "unreliable"]).optional(),
|
|
4170
|
-
intent_clarity: external_exports.enum(["clear", "indirect", "vague"]).optional(),
|
|
4171
|
-
confirmation_style: external_exports.enum(["explicit", "vague"]).optional()
|
|
4172
|
-
}).optional();
|
|
4173
|
-
var CallerAudioEffectsSchema = external_exports.object({
|
|
4174
|
-
noise: external_exports.object({
|
|
4175
|
-
type: external_exports.enum(["babble", "white", "pink"]),
|
|
4176
|
-
snr_db: external_exports.number().min(0).max(40).default(10)
|
|
4177
|
-
}).optional(),
|
|
4178
|
-
speed: external_exports.number().min(0.5).max(2).optional(),
|
|
4179
|
-
speakerphone: external_exports.boolean().optional(),
|
|
4180
|
-
mic_distance: external_exports.enum(["close", "normal", "far"]).optional(),
|
|
4181
|
-
clarity: external_exports.number().min(0).max(1).optional(),
|
|
4182
|
-
accent: external_exports.enum(CALLER_ACCENTS).optional(),
|
|
4183
|
-
packet_loss: external_exports.number().min(0).max(0.3).optional(),
|
|
4184
|
-
jitter_ms: external_exports.number().min(0).max(100).optional()
|
|
4185
|
-
});
|
|
4186
|
-
var ConversationCallSpecSchema = external_exports.object({
|
|
4187
|
-
name: external_exports.string().optional(),
|
|
4188
|
-
caller_prompt: external_exports.string().min(1),
|
|
4189
|
-
max_turns: external_exports.number().int().min(1).max(50).default(6),
|
|
4190
|
-
persona: CallerPersonaSchema,
|
|
4191
|
-
caller_audio: CallerAudioEffectsSchema.optional(),
|
|
4192
|
-
/** ISO 639-1 language code for multilingual calls (e.g., "es", "fr", "de"). Caller speaks this language, STT transcribes it, judge evaluates in it. */
|
|
4193
|
-
language: external_exports.string().min(2).max(5).optional(),
|
|
4194
|
-
/** Caller voice gender (English only; default female). Ignored if caller_audio.accent is set or language is non-English. */
|
|
4195
|
-
voice: external_exports.enum(["male", "female"]).optional()
|
|
4196
|
-
});
|
|
4197
|
-
var CallSpecSchema = external_exports.object({
|
|
4198
|
-
call: ConversationCallSpecSchema
|
|
4199
|
-
});
|
|
4200
|
-
var AdapterTypeSchema = external_exports.enum(["websocket", "livekit", "vapi", "retell", "elevenlabs", "bland"]);
|
|
4201
|
-
var PlatformProviderSchema = external_exports.enum(["bland", "livekit", "vapi", "retell", "elevenlabs"]);
|
|
4202
|
-
var ObservedToolCallSchema = external_exports.object({
|
|
4203
|
-
name: external_exports.string(),
|
|
4204
|
-
arguments: external_exports.record(external_exports.unknown()),
|
|
4205
|
-
result: external_exports.unknown().optional(),
|
|
4206
|
-
successful: external_exports.boolean().optional(),
|
|
4207
|
-
provider_tool_type: external_exports.string().optional(),
|
|
4208
|
-
timestamp_ms: external_exports.number().optional(),
|
|
4209
|
-
latency_ms: external_exports.number().optional(),
|
|
4210
|
-
turn_index: external_exports.number().int().min(0).optional()
|
|
4211
|
-
});
|
|
4212
|
-
var ToolCallMetricsSchema = external_exports.object({
|
|
4213
|
-
total: external_exports.number().int().min(0),
|
|
4214
|
-
successful: external_exports.number().int().min(0),
|
|
4215
|
-
failed: external_exports.number().int().min(0),
|
|
4216
|
-
mean_latency_ms: external_exports.number().optional(),
|
|
4217
|
-
names: external_exports.array(external_exports.string())
|
|
4218
|
-
});
|
|
4155
|
+
// ../shared/src/schemas/platforms.ts
|
|
4156
|
+
var PLATFORM_PROVIDERS = ["vapi", "retell", "elevenlabs", "livekit", "bland"];
|
|
4219
4157
|
var BasePlatformSchema = external_exports.object({
|
|
4158
|
+
/** Max concurrent calls (platform-dependent) */
|
|
4220
4159
|
max_concurrency: external_exports.number().int().min(1).optional()
|
|
4221
4160
|
});
|
|
4222
4161
|
var BlandPlatformSchema = BasePlatformSchema.extend({
|
|
4223
4162
|
provider: external_exports.literal("bland"),
|
|
4163
|
+
/** Bland API key (the CLI reads BLAND_API_KEY from the user's env) */
|
|
4224
4164
|
bland_api_key: external_exports.string().optional(),
|
|
4165
|
+
/** Bland pathway ID (the CLI reads BLAND_PATHWAY_ID from the user's env) */
|
|
4225
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) */
|
|
4226
4168
|
persona_id: external_exports.string().optional(),
|
|
4169
|
+
/** Task prompt — used instead of pathway_id for simple agents */
|
|
4227
4170
|
task: external_exports.string().optional(),
|
|
4171
|
+
/** Tool definitions (inline objects) or tool IDs (TL-xxx strings) */
|
|
4228
4172
|
tools: external_exports.array(external_exports.unknown()).optional(),
|
|
4173
|
+
/** Voice name ("maya", "josh") or custom voice clone UUID */
|
|
4229
4174
|
voice: external_exports.string().optional(),
|
|
4175
|
+
/** Model: "base" (full features), "turbo" (fastest, limited features) */
|
|
4230
4176
|
model: external_exports.string().optional(),
|
|
4177
|
+
/** Opening sentence — overrides any greeting in the task/pathway */
|
|
4231
4178
|
first_sentence: external_exports.string().optional(),
|
|
4179
|
+
/** If true, agent waits for callee to speak first (default: false) */
|
|
4232
4180
|
wait_for_greeting: external_exports.boolean().optional(),
|
|
4181
|
+
/** Max call duration in minutes (default: 30) */
|
|
4233
4182
|
max_duration: external_exports.number().optional(),
|
|
4183
|
+
/** Temperature 0-1 (default: 0.7) */
|
|
4234
4184
|
temperature: external_exports.number().min(0).max(1).optional(),
|
|
4185
|
+
/** Language code e.g. "babel-en", "babel-es" */
|
|
4235
4186
|
language: external_exports.string().optional(),
|
|
4187
|
+
/** How quickly agent stops speaking when interrupted, in ms (default: 500) */
|
|
4236
4188
|
interruption_threshold: external_exports.number().optional(),
|
|
4189
|
+
/** When true, agent ignores user interruptions entirely */
|
|
4237
4190
|
block_interruptions: external_exports.boolean().optional(),
|
|
4191
|
+
/** When true, enable Bland's noise filtering on caller audio */
|
|
4238
4192
|
noise_cancellation: external_exports.boolean().optional(),
|
|
4193
|
+
/** Background audio: "office", "cafe", "restaurant", "none", or null (default phone static) */
|
|
4239
4194
|
background_track: external_exports.string().nullable().optional(),
|
|
4195
|
+
/** Boost transcription accuracy for specific words. Supports "word:boost_factor" */
|
|
4240
4196
|
keywords: external_exports.array(external_exports.string()).optional(),
|
|
4197
|
+
/** Key-value pairs accessible as {{variable}} in agent prompts/pathways */
|
|
4241
4198
|
request_data: external_exports.record(external_exports.unknown()).optional(),
|
|
4199
|
+
/** Pronunciation overrides */
|
|
4242
4200
|
pronunciation_guide: external_exports.array(external_exports.object({
|
|
4243
4201
|
word: external_exports.string(),
|
|
4244
4202
|
pronunciation: external_exports.string(),
|
|
4245
4203
|
case_sensitive: external_exports.boolean().optional(),
|
|
4246
4204
|
spaced: external_exports.boolean().optional()
|
|
4247
4205
|
})).optional(),
|
|
4206
|
+
/** Start pathway from a specific node instead of the default */
|
|
4248
4207
|
start_node_id: external_exports.string().optional(),
|
|
4208
|
+
/** Specific pathway version to test (default: production) */
|
|
4249
4209
|
pathway_version: external_exports.number().optional()
|
|
4250
4210
|
});
|
|
4251
4211
|
var LiveKitPlatformSchema = BasePlatformSchema.extend({
|
|
4252
4212
|
provider: external_exports.literal("livekit"),
|
|
4213
|
+
/** LiveKit API key (the CLI reads LIVEKIT_API_KEY from the user's env) */
|
|
4253
4214
|
livekit_api_key: external_exports.string().optional(),
|
|
4215
|
+
/** LiveKit API secret (the CLI reads LIVEKIT_API_SECRET from the user's env) */
|
|
4254
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) */
|
|
4255
4218
|
livekit_url: external_exports.string().optional(),
|
|
4219
|
+
/** Explicit agent dispatch — agent_name from WorkerOptions. Omit for automatic dispatch. */
|
|
4256
4220
|
livekit_agent_name: external_exports.string().optional()
|
|
4257
4221
|
}).strict();
|
|
4258
4222
|
var VapiPlatformSchema = BasePlatformSchema.extend({
|
|
4259
4223
|
provider: external_exports.literal("vapi"),
|
|
4224
|
+
/** Vapi API key (the CLI reads VAPI_API_KEY from the user's env) */
|
|
4260
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) */
|
|
4261
4227
|
vapi_assistant_id: external_exports.string().optional()
|
|
4262
4228
|
});
|
|
4263
4229
|
var RetellPlatformSchema = BasePlatformSchema.extend({
|
|
4264
4230
|
provider: external_exports.literal("retell"),
|
|
4231
|
+
/** Retell API key (the CLI reads RETELL_API_KEY from the user's env) */
|
|
4265
4232
|
retell_api_key: external_exports.string().optional(),
|
|
4233
|
+
/** Retell agent ID (the CLI reads RETELL_AGENT_ID from the user's env) */
|
|
4266
4234
|
retell_agent_id: external_exports.string().optional()
|
|
4267
4235
|
});
|
|
4268
4236
|
var ElevenLabsPlatformSchema = BasePlatformSchema.extend({
|
|
4269
4237
|
provider: external_exports.literal("elevenlabs"),
|
|
4238
|
+
/** ElevenLabs API key (the CLI reads ELEVENLABS_API_KEY from the user's env) */
|
|
4270
4239
|
elevenlabs_api_key: external_exports.string().optional(),
|
|
4240
|
+
/** ElevenLabs agent ID (the CLI reads ELEVENLABS_AGENT_ID from the user's env) */
|
|
4271
4241
|
elevenlabs_agent_id: external_exports.string().optional()
|
|
4272
4242
|
});
|
|
4273
4243
|
var PlatformConfigSchema = external_exports.discriminatedUnion("provider", [
|
|
@@ -4277,89 +4247,118 @@ var PlatformConfigSchema = external_exports.discriminatedUnion("provider", [
|
|
|
4277
4247
|
RetellPlatformSchema,
|
|
4278
4248
|
ElevenLabsPlatformSchema
|
|
4279
4249
|
]);
|
|
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()
|
|
4277
|
+
});
|
|
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()
|
|
4310
|
+
});
|
|
4280
4311
|
var ConversationTurnSchema = external_exports.object({
|
|
4281
4312
|
role: external_exports.enum(["caller", "agent"]),
|
|
4282
4313
|
text: external_exports.string(),
|
|
4283
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. */
|
|
4284
4316
|
caller_decision_mode: external_exports.enum(["continue", "wait", "closing", "end_now"]).optional(),
|
|
4285
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). */
|
|
4286
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). */
|
|
4287
4321
|
ttfw_ms: external_exports.number().optional(),
|
|
4322
|
+
/** Dead audio before speech starts: ttfw_ms - ttfb_ms */
|
|
4288
4323
|
silence_pad_ms: external_exports.number().optional(),
|
|
4289
4324
|
stt_confidence: external_exports.number().optional(),
|
|
4325
|
+
/** Harness TTS synthesis time for this turn's caller audio (ms) */
|
|
4290
4326
|
tts_ms: external_exports.number().optional(),
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
stt_ms: external_exports.number().optional(),
|
|
4294
|
-
llm_ms: external_exports.number().optional(),
|
|
4295
|
-
tts_ms: external_exports.number().optional(),
|
|
4296
|
-
speech_duration_ms: external_exports.number().optional()
|
|
4297
|
-
}).optional(),
|
|
4298
|
-
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()
|
|
4299
4329
|
});
|
|
4300
|
-
var
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
p95_ttfb_ms: external_exports.number().optional(),
|
|
4305
|
-
p99_ttfb_ms: external_exports.number().optional(),
|
|
4306
|
-
first_turn_ttfb_ms: external_exports.number().optional(),
|
|
4307
|
-
ttfw_per_turn_ms: external_exports.array(external_exports.number()).optional(),
|
|
4308
|
-
p50_ttfw_ms: external_exports.number().optional(),
|
|
4309
|
-
p90_ttfw_ms: external_exports.number().optional(),
|
|
4310
|
-
p95_ttfw_ms: external_exports.number().optional(),
|
|
4311
|
-
p99_ttfw_ms: external_exports.number().optional(),
|
|
4312
|
-
first_turn_ttfw_ms: external_exports.number().optional(),
|
|
4313
|
-
mean_silence_pad_ms: external_exports.number().optional(),
|
|
4314
|
-
mouth_to_ear_est_ms: external_exports.number().optional(),
|
|
4315
|
-
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()
|
|
4316
4334
|
});
|
|
4317
|
-
var
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
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()
|
|
4322
4344
|
});
|
|
4323
4345
|
var SignalQualityMetricsSchema = external_exports.object({
|
|
4346
|
+
/** Mean signal-to-noise ratio across turns (dB). >20 good, <10 bad */
|
|
4324
4347
|
mean_snr_db: external_exports.number(),
|
|
4348
|
+
/** Max clipping ratio across turns (0-1). >0.01 = distortion */
|
|
4325
4349
|
max_clipping_ratio: external_exports.number(),
|
|
4350
|
+
/** Mean energy consistency across turns (0-1). <0.5 = unstable volume */
|
|
4326
4351
|
energy_consistency: external_exports.number(),
|
|
4352
|
+
/** Total sudden volume drops across all turns */
|
|
4327
4353
|
sudden_drops: external_exports.number().int().min(0),
|
|
4354
|
+
/** Total sudden volume spikes across all turns */
|
|
4328
4355
|
sudden_spikes: external_exports.number().int().min(0),
|
|
4356
|
+
/** All turns have clean start/end (no clicks) */
|
|
4329
4357
|
clean_edges: external_exports.boolean()
|
|
4330
4358
|
});
|
|
4331
|
-
var
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
tts_ms: external_exports.number().optional(),
|
|
4335
|
-
speech_duration_ms: external_exports.number().optional()
|
|
4336
|
-
});
|
|
4337
|
-
var ComponentLatencyMetricsSchema = external_exports.object({
|
|
4338
|
-
per_turn: external_exports.array(ComponentLatencySchema),
|
|
4339
|
-
mean_stt_ms: external_exports.number().optional(),
|
|
4340
|
-
mean_llm_ms: external_exports.number().optional(),
|
|
4341
|
-
mean_tts_ms: external_exports.number().optional(),
|
|
4342
|
-
p95_stt_ms: external_exports.number().optional(),
|
|
4343
|
-
p95_llm_ms: external_exports.number().optional(),
|
|
4344
|
-
p95_tts_ms: external_exports.number().optional(),
|
|
4345
|
-
bottleneck: external_exports.enum(["stt", "llm", "tts"]).optional()
|
|
4346
|
-
});
|
|
4347
|
-
var UsageEntrySchema = external_exports.object({
|
|
4348
|
-
type: external_exports.string(),
|
|
4349
|
-
provider: external_exports.string(),
|
|
4350
|
-
model: external_exports.string(),
|
|
4351
|
-
input_tokens: external_exports.number().int().optional(),
|
|
4352
|
-
output_tokens: external_exports.number().int().optional()
|
|
4353
|
-
});
|
|
4354
|
-
var CostBreakdownSchema = external_exports.object({
|
|
4355
|
-
stt_usd: external_exports.number().optional(),
|
|
4356
|
-
llm_usd: external_exports.number().optional(),
|
|
4357
|
-
tts_usd: external_exports.number().optional(),
|
|
4358
|
-
transport_usd: external_exports.number().optional(),
|
|
4359
|
-
platform_usd: external_exports.number().optional(),
|
|
4360
|
-
total_usd: external_exports.number().optional(),
|
|
4361
|
-
llm_prompt_tokens: external_exports.number().int().optional(),
|
|
4362
|
-
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()
|
|
4363
4362
|
});
|
|
4364
4363
|
var ProviderWarningSchema = external_exports.object({
|
|
4365
4364
|
message: external_exports.string().optional(),
|
|
@@ -4375,56 +4374,63 @@ var CallTransferSchema = external_exports.object({
|
|
|
4375
4374
|
});
|
|
4376
4375
|
var CallMetadataSchema = external_exports.object({
|
|
4377
4376
|
platform: external_exports.string(),
|
|
4377
|
+
/** Provider-visible call/conversation identifier for dashboard/API lookup. */
|
|
4378
4378
|
provider_call_id: external_exports.string().optional(),
|
|
4379
|
+
/** Secondary runtime/session identifier when distinct from the call ID. */
|
|
4379
4380
|
provider_session_id: external_exports.string().optional(),
|
|
4380
4381
|
ended_reason: external_exports.string().optional(),
|
|
4381
|
-
|
|
4382
|
-
cost_breakdown: CostBreakdownSchema.optional(),
|
|
4382
|
+
cost: CallCostSchema.optional(),
|
|
4383
4383
|
recording_url: external_exports.string().optional(),
|
|
4384
|
+
/** Provider-specific alternate recording artifacts keyed by variant name. */
|
|
4384
4385
|
recording_variants: external_exports.record(external_exports.string()).optional(),
|
|
4386
|
+
/** Provider-specific debug and deep-link URLs keyed by purpose. */
|
|
4385
4387
|
provider_debug_urls: external_exports.record(external_exports.string()).optional(),
|
|
4386
|
-
|
|
4387
|
-
usage: external_exports.array(UsageEntrySchema).optional(),
|
|
4388
|
+
/** Provider/runtime warnings retained in a normalized structure. */
|
|
4388
4389
|
provider_warnings: external_exports.array(ProviderWarningSchema).optional(),
|
|
4389
|
-
|
|
4390
|
+
/** Call transfers that occurred during the call */
|
|
4390
4391
|
transfers: external_exports.array(CallTransferSchema).optional()
|
|
4391
4392
|
});
|
|
4392
4393
|
var ConversationMetricsSchema = external_exports.object({
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
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. */
|
|
4397
4397
|
signal_quality: SignalQualityMetricsSchema.optional(),
|
|
4398
|
-
|
|
4399
|
-
|
|
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()
|
|
4400
4402
|
});
|
|
4401
4403
|
var ConversationCallResultSchema = external_exports.object({
|
|
4402
4404
|
name: external_exports.string().optional(),
|
|
4403
4405
|
caller_prompt: external_exports.string(),
|
|
4404
4406
|
status: external_exports.enum(["completed", "error"]),
|
|
4407
|
+
/** How a completed call ended, when Vent ended it or the connection closed. */
|
|
4405
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. */
|
|
4406
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(),
|
|
4407
4413
|
observed_tool_calls: external_exports.array(ObservedToolCallSchema).optional(),
|
|
4408
4414
|
duration_ms: external_exports.number(),
|
|
4409
4415
|
metrics: ConversationMetricsSchema,
|
|
4410
4416
|
error: external_exports.string().optional(),
|
|
4417
|
+
/** Platform call metadata (cost, ended reason, recording, analysis) */
|
|
4411
4418
|
call_metadata: CallMetadataSchema.optional()
|
|
4412
4419
|
});
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
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
|
|
4421
4428
|
});
|
|
4422
|
-
var
|
|
4423
|
-
var
|
|
4429
|
+
var RUN_TERMINAL_STATUSES = ["pass", "fail", "cancelled"];
|
|
4430
|
+
var WorkerResultCallbackSchema = external_exports.object({
|
|
4424
4431
|
run_id: external_exports.string().uuid(),
|
|
4425
|
-
status: external_exports.enum(
|
|
4432
|
+
status: external_exports.enum(RUN_TERMINAL_STATUSES),
|
|
4426
4433
|
conversation_result: ConversationCallResultSchema,
|
|
4427
|
-
aggregate: RunAggregateV2Schema,
|
|
4428
4434
|
error_text: external_exports.string().optional()
|
|
4429
4435
|
});
|
|
4430
4436
|
var RunFailedCallbackSchema = external_exports.object({
|
|
@@ -4437,253 +4443,31 @@ var RunEventCallbackSchema = external_exports.object({
|
|
|
4437
4443
|
message: external_exports.string().max(2e3),
|
|
4438
4444
|
metadata_json: external_exports.record(external_exports.unknown()).optional()
|
|
4439
4445
|
});
|
|
4440
|
-
var
|
|
4446
|
+
var RunJobSchema = external_exports.object({
|
|
4441
4447
|
run_id: external_exports.string().uuid(),
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
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()
|
|
4449
4461
|
});
|
|
4450
|
-
|
|
4451
|
-
// ../shared/src/format-result.ts
|
|
4452
|
-
function formatConversationResult(raw, options = {}) {
|
|
4453
|
-
if (!raw || typeof raw !== "object") return null;
|
|
4454
|
-
const r = raw;
|
|
4455
|
-
if (typeof r.caller_prompt !== "string") return null;
|
|
4456
|
-
const verbose = options.verbose ?? false;
|
|
4457
|
-
const debug = verbose ? formatDebug(r) : void 0;
|
|
4458
|
-
const warnings = dedupeStrings([
|
|
4459
|
-
...formatProviderWarningMessages(r.call_metadata?.provider_warnings)
|
|
4460
|
-
]);
|
|
4461
|
-
const result = {
|
|
4462
|
-
name: r.name ?? null,
|
|
4463
|
-
status: r.status,
|
|
4464
|
-
duration_ms: r.duration_ms,
|
|
4465
|
-
error: r.error ?? null,
|
|
4466
|
-
end_note: r.end_note ?? null,
|
|
4467
|
-
transcript: formatTranscript(r.transcript, options),
|
|
4468
|
-
latency: r.metrics?.latency ? formatLatency(r.metrics.latency, r.metrics, verbose) : null,
|
|
4469
|
-
tool_calls: formatToolCalls(r.metrics?.tool_calls, r.observed_tool_calls, verbose),
|
|
4470
|
-
component_latency: formatComponentLatency(r.metrics?.component_latency, verbose),
|
|
4471
|
-
call_metadata: formatCallMetadata(r.call_metadata, verbose)
|
|
4472
|
-
};
|
|
4473
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
4474
|
-
if (verbose) result.caller_prompt = r.caller_prompt;
|
|
4475
|
-
if (debug) result.debug = debug;
|
|
4476
|
-
return result;
|
|
4477
|
-
}
|
|
4478
|
-
function formatTranscript(turns, options) {
|
|
4479
|
-
if (!turns) return [];
|
|
4480
|
-
return turns.map((t) => {
|
|
4481
|
-
const turn = {
|
|
4482
|
-
role: t.role,
|
|
4483
|
-
text: t.text
|
|
4484
|
-
};
|
|
4485
|
-
if (t.ttfb_ms != null) turn.ttfb_ms = t.ttfb_ms;
|
|
4486
|
-
if (t.ttfw_ms != null) turn.ttfw_ms = t.ttfw_ms;
|
|
4487
|
-
if (t.audio_duration_ms != null) turn.audio_duration_ms = t.audio_duration_ms;
|
|
4488
|
-
if (options.verbose) {
|
|
4489
|
-
const debug = compactUnknownRecord({
|
|
4490
|
-
timestamp_ms: t.timestamp_ms,
|
|
4491
|
-
caller_decision_mode: t.caller_decision_mode,
|
|
4492
|
-
silence_pad_ms: t.silence_pad_ms,
|
|
4493
|
-
stt_confidence: t.stt_confidence,
|
|
4494
|
-
harness_tts_ms: t.tts_ms,
|
|
4495
|
-
harness_stt_ms: t.stt_ms,
|
|
4496
|
-
component_latency: t.component_latency,
|
|
4497
|
-
platform_transcript: t.platform_transcript
|
|
4498
|
-
});
|
|
4499
|
-
if (debug && Object.keys(debug).length > 0) {
|
|
4500
|
-
turn.debug = debug;
|
|
4501
|
-
}
|
|
4502
|
-
}
|
|
4503
|
-
return turn;
|
|
4504
|
-
});
|
|
4505
|
-
}
|
|
4506
|
-
function formatLatency(latency, metrics, verbose) {
|
|
4507
|
-
const hasTtfw = metrics.mean_ttfw_ms != null && latency.p50_ttfw_ms != null && latency.p95_ttfw_ms != null;
|
|
4508
|
-
const responseTimeSource = hasTtfw ? "ttfw" : "ttfb";
|
|
4509
|
-
const firstResponse = hasTtfw ? latency.first_turn_ttfw_ms ?? latency.first_turn_ttfb_ms : latency.first_turn_ttfb_ms;
|
|
4510
|
-
if (firstResponse == null || !hasTtfw && (metrics.mean_ttfb_ms == null || latency.p95_ttfb_ms == null)) return null;
|
|
4511
|
-
const result = {
|
|
4512
|
-
response_time_ms: hasTtfw ? metrics.mean_ttfw_ms : metrics.mean_ttfb_ms,
|
|
4513
|
-
response_time_source: responseTimeSource,
|
|
4514
|
-
p95_response_time_ms: hasTtfw ? latency.p95_ttfw_ms : latency.p95_ttfb_ms,
|
|
4515
|
-
first_response_time_ms: firstResponse
|
|
4516
|
-
};
|
|
4517
|
-
if (!verbose) return result;
|
|
4518
|
-
result.p50_response_time_ms = hasTtfw ? latency.p50_ttfw_ms : latency.p50_ttfb_ms;
|
|
4519
|
-
result.p90_response_time_ms = hasTtfw ? latency.p90_ttfw_ms ?? latency.p90_ttfb_ms : latency.p90_ttfb_ms;
|
|
4520
|
-
result.p99_response_time_ms = hasTtfw ? latency.p99_ttfw_ms ?? latency.p99_ttfb_ms : latency.p99_ttfb_ms;
|
|
4521
|
-
if (hasTtfw) {
|
|
4522
|
-
result.mean_ttfw_ms = metrics.mean_ttfw_ms;
|
|
4523
|
-
result.p50_ttfw_ms = latency.p50_ttfw_ms;
|
|
4524
|
-
result.p90_ttfw_ms = latency.p90_ttfw_ms ?? latency.p90_ttfb_ms;
|
|
4525
|
-
result.p95_ttfw_ms = latency.p95_ttfw_ms;
|
|
4526
|
-
result.p99_ttfw_ms = latency.p99_ttfw_ms ?? latency.p99_ttfb_ms;
|
|
4527
|
-
result.first_turn_ttfw_ms = latency.first_turn_ttfw_ms ?? latency.first_turn_ttfb_ms;
|
|
4528
|
-
}
|
|
4529
|
-
if (latency.drift_slope_ms_per_turn != null) result.drift_slope_ms_per_turn = latency.drift_slope_ms_per_turn;
|
|
4530
|
-
if (latency.mean_silence_pad_ms != null) result.mean_silence_pad_ms = latency.mean_silence_pad_ms;
|
|
4531
|
-
if (latency.mouth_to_ear_est_ms != null) result.mouth_to_ear_est_ms = latency.mouth_to_ear_est_ms;
|
|
4532
|
-
return result;
|
|
4533
|
-
}
|
|
4534
|
-
function formatToolCalls(summary, observed, verbose) {
|
|
4535
|
-
return {
|
|
4536
|
-
total: summary?.total ?? observed?.length ?? 0,
|
|
4537
|
-
successful: summary?.successful ?? observed?.filter((c) => c.successful).length ?? 0,
|
|
4538
|
-
failed: summary?.failed ?? observed?.filter((c) => c.successful === false).length ?? 0,
|
|
4539
|
-
mean_latency_ms: summary?.mean_latency_ms,
|
|
4540
|
-
names: summary?.names ?? [...new Set((observed ?? []).map((c) => c.name))],
|
|
4541
|
-
observed: (observed ?? []).map((c) => {
|
|
4542
|
-
const entry = {
|
|
4543
|
-
name: c.name,
|
|
4544
|
-
arguments: stripExecutionMessage(c.arguments),
|
|
4545
|
-
result: verbose ? c.result : truncateLargeArrays(c.result),
|
|
4546
|
-
successful: c.successful,
|
|
4547
|
-
latency_ms: c.latency_ms,
|
|
4548
|
-
turn_index: c.turn_index
|
|
4549
|
-
};
|
|
4550
|
-
if (c.provider_tool_type && c.provider_tool_type !== "custom") {
|
|
4551
|
-
entry.provider_tool_type = c.provider_tool_type;
|
|
4552
|
-
}
|
|
4553
|
-
return entry;
|
|
4554
|
-
})
|
|
4555
|
-
};
|
|
4556
|
-
}
|
|
4557
|
-
var TOOL_RESULT_ARRAY_LIMIT = 5;
|
|
4558
|
-
function truncateLargeArrays(value) {
|
|
4559
|
-
if (Array.isArray(value)) {
|
|
4560
|
-
if (value.length <= TOOL_RESULT_ARRAY_LIMIT) {
|
|
4561
|
-
return value.map(truncateLargeArrays);
|
|
4562
|
-
}
|
|
4563
|
-
return [
|
|
4564
|
-
...value.slice(0, TOOL_RESULT_ARRAY_LIMIT).map(truncateLargeArrays),
|
|
4565
|
-
{ _truncated: `${value.length - TOOL_RESULT_ARRAY_LIMIT} more entries omitted; use --verbose for full result` }
|
|
4566
|
-
];
|
|
4567
|
-
}
|
|
4568
|
-
if (value && typeof value === "object") {
|
|
4569
|
-
const out = {};
|
|
4570
|
-
for (const [k, v] of Object.entries(value)) {
|
|
4571
|
-
out[k] = truncateLargeArrays(v);
|
|
4572
|
-
}
|
|
4573
|
-
return out;
|
|
4574
|
-
}
|
|
4575
|
-
return value;
|
|
4576
|
-
}
|
|
4577
|
-
function stripExecutionMessage(args) {
|
|
4578
|
-
if (!args || typeof args !== "object") return args;
|
|
4579
|
-
const { execution_message: _drop, ...rest } = args;
|
|
4580
|
-
return rest;
|
|
4581
|
-
}
|
|
4582
|
-
function formatComponentLatency(cl, verbose) {
|
|
4583
|
-
if (!cl) return null;
|
|
4584
|
-
const result = {
|
|
4585
|
-
mean_stt_ms: cl.mean_stt_ms,
|
|
4586
|
-
mean_llm_ms: cl.mean_llm_ms,
|
|
4587
|
-
mean_tts_ms: cl.mean_tts_ms,
|
|
4588
|
-
bottleneck: cl.bottleneck
|
|
4589
|
-
};
|
|
4590
|
-
if (!verbose) return result;
|
|
4591
|
-
const speechDurations = cl.per_turn.map((t) => t.speech_duration_ms).filter((v) => v != null);
|
|
4592
|
-
const meanSpeech = speechDurations.length > 0 ? Math.round(speechDurations.reduce((a, b) => a + b, 0) / speechDurations.length) : void 0;
|
|
4593
|
-
result.p95_stt_ms = cl.p95_stt_ms;
|
|
4594
|
-
result.p95_llm_ms = cl.p95_llm_ms;
|
|
4595
|
-
result.p95_tts_ms = cl.p95_tts_ms;
|
|
4596
|
-
result.mean_speech_duration_ms = meanSpeech;
|
|
4597
|
-
return result;
|
|
4598
|
-
}
|
|
4599
|
-
function formatCallMetadata(meta, verbose) {
|
|
4600
|
-
if (!meta) return null;
|
|
4601
|
-
const transfers = meta.transfers?.map((transfer) => {
|
|
4602
|
-
const formattedTransfer = {
|
|
4603
|
-
type: transfer.type,
|
|
4604
|
-
destination: transfer.destination,
|
|
4605
|
-
status: transfer.status,
|
|
4606
|
-
sources: transfer.sources
|
|
4607
|
-
};
|
|
4608
|
-
if (transfer.timestamp_ms != null) {
|
|
4609
|
-
formattedTransfer.timestamp_ms = transfer.timestamp_ms;
|
|
4610
|
-
}
|
|
4611
|
-
return formattedTransfer;
|
|
4612
|
-
});
|
|
4613
|
-
const result = {
|
|
4614
|
-
platform: meta.platform,
|
|
4615
|
-
provider_call_id: meta.provider_call_id,
|
|
4616
|
-
provider_session_id: meta.provider_session_id,
|
|
4617
|
-
ended_reason: meta.ended_reason,
|
|
4618
|
-
cost_usd: meta.cost_usd,
|
|
4619
|
-
recording_url: meta.recording_url
|
|
4620
|
-
};
|
|
4621
|
-
if (verbose) {
|
|
4622
|
-
result.cost_breakdown = meta.cost_breakdown;
|
|
4623
|
-
result.usage = meta.usage && meta.usage.length > 0 ? meta.usage : void 0;
|
|
4624
|
-
result.recording_variants = meta.recording_variants;
|
|
4625
|
-
result.provider_debug_urls = meta.provider_debug_urls;
|
|
4626
|
-
result.variables = meta.variables;
|
|
4627
|
-
}
|
|
4628
|
-
if (transfers && transfers.length > 0) {
|
|
4629
|
-
const completedTransferCount = transfers.filter((transfer) => transfer.status === "completed").length;
|
|
4630
|
-
const transferCompleted = completedTransferCount > 0;
|
|
4631
|
-
result.transfer_attempted = true;
|
|
4632
|
-
result.transfer_completed = transferCompleted;
|
|
4633
|
-
result.escalated = transferCompleted;
|
|
4634
|
-
result.transfer_count = transfers.length;
|
|
4635
|
-
result.completed_transfer_count = completedTransferCount;
|
|
4636
|
-
result.transfers = transfers;
|
|
4637
|
-
}
|
|
4638
|
-
return result;
|
|
4639
|
-
}
|
|
4640
|
-
function formatDebug(result) {
|
|
4641
|
-
const debug = compactUnknownRecord({
|
|
4642
|
-
signal_quality: result.metrics?.signal_quality,
|
|
4643
|
-
harness_overhead: result.metrics?.harness_overhead,
|
|
4644
|
-
provider_warnings: nonEmptyArray(result.call_metadata?.provider_warnings),
|
|
4645
|
-
component_latency_per_turn: nonEmptyArray(result.metrics?.component_latency?.per_turn),
|
|
4646
|
-
observed_tool_calls: formatDebugToolCalls(result.observed_tool_calls),
|
|
4647
|
-
provider_metadata: result.call_metadata?.provider_metadata
|
|
4648
|
-
});
|
|
4649
|
-
return debug && Object.keys(debug).length > 0 ? debug : void 0;
|
|
4650
|
-
}
|
|
4651
|
-
function formatDebugToolCalls(observed) {
|
|
4652
|
-
if (!observed || observed.length === 0) return void 0;
|
|
4653
|
-
return observed.map((call) => ({
|
|
4654
|
-
name: call.name,
|
|
4655
|
-
arguments: call.arguments,
|
|
4656
|
-
result: call.result,
|
|
4657
|
-
successful: call.successful,
|
|
4658
|
-
provider_tool_type: call.provider_tool_type,
|
|
4659
|
-
timestamp_ms: call.timestamp_ms,
|
|
4660
|
-
latency_ms: call.latency_ms,
|
|
4661
|
-
turn_index: call.turn_index
|
|
4662
|
-
}));
|
|
4663
|
-
}
|
|
4664
|
-
function nonEmptyArray(value) {
|
|
4665
|
-
return value && value.length > 0 ? value : void 0;
|
|
4666
|
-
}
|
|
4667
|
-
function formatProviderWarningMessages(warnings) {
|
|
4668
|
-
if (!warnings || warnings.length === 0) return [];
|
|
4669
|
-
return warnings.map((warning) => warning.message ?? warning.code).filter((message) => typeof message === "string" && message.length > 0);
|
|
4670
|
-
}
|
|
4671
|
-
function dedupeStrings(values) {
|
|
4672
|
-
return [...new Set(values)];
|
|
4673
|
-
}
|
|
4674
|
-
function compactUnknownRecord(record) {
|
|
4675
|
-
const entries = Object.entries(record).filter(([, value]) => value != null);
|
|
4676
|
-
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
4677
|
-
}
|
|
4678
4462
|
|
|
4679
4463
|
// ../shared/src/callback-auth.ts
|
|
4680
4464
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4465
|
+
var RUNNER_CALLBACK_MAX_SKEW_MS = 5 * 6e4;
|
|
4681
4466
|
|
|
4682
4467
|
// src/lib/config.ts
|
|
4683
4468
|
var CONFIG_DIR = path.join(homedir(), ".vent");
|
|
4684
4469
|
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials");
|
|
4685
4470
|
var API_BASE = process.env.VENT_API_URL ?? "https://api.venthq.dev";
|
|
4686
|
-
var DASHBOARD_URL = process.env.VENT_DASHBOARD_URL ?? "https://venthq.dev";
|
|
4687
4471
|
var refreshes = /* @__PURE__ */ new Map();
|
|
4688
4472
|
async function readCredentials() {
|
|
4689
4473
|
const accessToken = process.env.VENT_ACCESS_TOKEN;
|
|
@@ -4751,6 +4535,11 @@ async function loadAccessToken() {
|
|
|
4751
4535
|
if (source.refreshToken && !usableAccessToken(source.accessToken, 6e4)) return refresh(source);
|
|
4752
4536
|
return usableAccessToken(source.accessToken) ? source.accessToken : null;
|
|
4753
4537
|
}
|
|
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;
|
|
4542
|
+
}
|
|
4754
4543
|
async function refreshAccessToken(rejectedToken) {
|
|
4755
4544
|
const source = await readCredentials();
|
|
4756
4545
|
if (!source?.refreshToken) return null;
|
|
@@ -4785,9 +4574,9 @@ var ApiError = class extends Error {
|
|
|
4785
4574
|
this.body = body;
|
|
4786
4575
|
}
|
|
4787
4576
|
};
|
|
4788
|
-
async function authenticatedFetch(path4,
|
|
4577
|
+
async function authenticatedFetch(path4, options = {}) {
|
|
4789
4578
|
const url = `${API_BASE}${path4}`;
|
|
4790
|
-
const token = await
|
|
4579
|
+
const token = await requireAccessToken();
|
|
4791
4580
|
const send = (accessToken) => {
|
|
4792
4581
|
const headers = new Headers(options.headers);
|
|
4793
4582
|
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
@@ -4801,8 +4590,8 @@ async function authenticatedFetch(path4, apiKey, options = {}) {
|
|
|
4801
4590
|
await response.body?.cancel();
|
|
4802
4591
|
return send(refreshed);
|
|
4803
4592
|
}
|
|
4804
|
-
async function apiFetch(path4,
|
|
4805
|
-
const res = await authenticatedFetch(path4,
|
|
4593
|
+
async function apiFetch(path4, options = {}) {
|
|
4594
|
+
const res = await authenticatedFetch(path4, options);
|
|
4806
4595
|
if (!res.ok) {
|
|
4807
4596
|
const text = await res.text();
|
|
4808
4597
|
let body = text;
|
|
@@ -4818,15 +4607,15 @@ async function apiFetch(path4, apiKey, options = {}) {
|
|
|
4818
4607
|
// src/lib/submit-run.ts
|
|
4819
4608
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4820
4609
|
var RETRY_DELAY_MS = 2e3;
|
|
4821
|
-
async function submitRun(
|
|
4610
|
+
async function submitRun(payload) {
|
|
4822
4611
|
const body = JSON.stringify({ ...payload, idempotency_key: randomUUID2() });
|
|
4823
|
-
const send = () => apiFetch("/runs/submit",
|
|
4612
|
+
const send = () => apiFetch("/runs/submit", { method: "POST", body });
|
|
4824
4613
|
try {
|
|
4825
4614
|
return await send();
|
|
4826
4615
|
} catch (err) {
|
|
4827
4616
|
const answerMayBeLost = !(err instanceof ApiError) || [502, 503, 504].includes(err.status);
|
|
4828
4617
|
if (!answerMayBeLost) throw err;
|
|
4829
|
-
await new Promise((
|
|
4618
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
4830
4619
|
return send();
|
|
4831
4620
|
}
|
|
4832
4621
|
}
|
|
@@ -4834,7 +4623,7 @@ async function submitRun(accessToken, payload) {
|
|
|
4834
4623
|
// src/lib/sse.ts
|
|
4835
4624
|
var MAX_RETRIES = 5;
|
|
4836
4625
|
var RETRY_DELAY_MS2 = 2e3;
|
|
4837
|
-
async function* streamRunEvents(runId,
|
|
4626
|
+
async function* streamRunEvents(runId, signal) {
|
|
4838
4627
|
const path4 = `/runs/${runId}/stream`;
|
|
4839
4628
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4840
4629
|
let retries = 0;
|
|
@@ -4844,9 +4633,7 @@ async function* streamRunEvents(runId, apiKey, signal) {
|
|
|
4844
4633
|
}
|
|
4845
4634
|
let res;
|
|
4846
4635
|
try {
|
|
4847
|
-
res = await authenticatedFetch(path4,
|
|
4848
|
-
signal
|
|
4849
|
-
});
|
|
4636
|
+
res = await authenticatedFetch(path4, { signal });
|
|
4850
4637
|
} catch (err) {
|
|
4851
4638
|
if (err.name === "AbortError") throw err;
|
|
4852
4639
|
retries++;
|
|
@@ -4933,29 +4720,13 @@ var red = (s) => isTTY ? `\x1B[31m${s}\x1B[0m` : s;
|
|
|
4933
4720
|
var yellow = (s) => isTTY ? `\x1B[33m${s}\x1B[0m` : s;
|
|
4934
4721
|
var blue = (s) => isTTY ? `\x1B[34m${s}\x1B[0m` : s;
|
|
4935
4722
|
function printEvent(event) {
|
|
4936
|
-
if (!isTTY) return;
|
|
4937
|
-
|
|
4938
|
-
switch (event.event_type) {
|
|
4939
|
-
case "call_completed":
|
|
4940
|
-
printCallResult(meta);
|
|
4941
|
-
break;
|
|
4942
|
-
case "run_complete":
|
|
4943
|
-
printRunComplete(meta);
|
|
4944
|
-
break;
|
|
4945
|
-
case "call_started": {
|
|
4946
|
-
const name = meta.call_name ?? "call";
|
|
4947
|
-
stdoutSync(dim(` \u25B8 ${name}\u2026`) + "\n");
|
|
4948
|
-
break;
|
|
4949
|
-
}
|
|
4950
|
-
default:
|
|
4951
|
-
stdoutSync(dim(` [${event.event_type}]`) + "\n");
|
|
4952
|
-
}
|
|
4723
|
+
if (!isTTY || event.event_type === "run_complete") return;
|
|
4724
|
+
stdoutSync(dim(` [${event.event_type}]`) + "\n");
|
|
4953
4725
|
}
|
|
4954
|
-
function printCallResult(
|
|
4955
|
-
const
|
|
4956
|
-
const
|
|
4957
|
-
const
|
|
4958
|
-
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;
|
|
4959
4730
|
const statusIcon = callStatus === "completed" ? green("\u25CF") : red("\u25CF");
|
|
4960
4731
|
const duration = durationMs != null ? (durationMs / 1e3).toFixed(1) + "s" : "\u2014";
|
|
4961
4732
|
const parts = [statusIcon, bold(callName), dim(duration)];
|
|
@@ -4985,113 +4756,21 @@ function printCallResult(meta) {
|
|
|
4985
4756
|
}
|
|
4986
4757
|
}
|
|
4987
4758
|
}
|
|
4988
|
-
function
|
|
4989
|
-
const agg = meta.aggregate;
|
|
4990
|
-
const counts = agg?.conversation_calls;
|
|
4991
|
-
const total = meta.total_calls ?? counts?.total;
|
|
4992
|
-
stdoutSync("\n");
|
|
4993
|
-
stdoutSync(bold("Run complete") + dim(" \u2014 Vent does not judge mission success; review the calls above.") + "\n");
|
|
4994
|
-
if (total != null) {
|
|
4995
|
-
stdoutSync(`${total} call${total === 1 ? "" : "s"} ran
|
|
4996
|
-
`);
|
|
4997
|
-
}
|
|
4998
|
-
if (typeof meta.error_text === "string") {
|
|
4999
|
-
stdoutSync(red(`Run ${String(meta.status ?? "ended")}: ${meta.error_text}`) + "\n");
|
|
5000
|
-
}
|
|
5001
|
-
}
|
|
5002
|
-
function printSummary(callResults, runComplete, runId, options = {}) {
|
|
5003
|
-
const allCalls = options.rawCalls ? formatRawCalls(options.rawCalls, options.verbose ?? false) : callResults.map((e) => {
|
|
5004
|
-
const meta = e.metadata_json ?? {};
|
|
5005
|
-
const r = meta.result;
|
|
5006
|
-
if (r) return r;
|
|
5007
|
-
return {
|
|
5008
|
-
name: meta.call_name ?? "call",
|
|
5009
|
-
status: meta.status ?? "unknown",
|
|
5010
|
-
duration_ms: meta.duration_ms,
|
|
5011
|
-
error: null
|
|
5012
|
-
};
|
|
5013
|
-
});
|
|
5014
|
-
const agg = runComplete.aggregate;
|
|
5015
|
-
const counts = agg?.conversation_calls;
|
|
5016
|
-
const summaryData = buildRunSummaryJson({
|
|
5017
|
-
runId,
|
|
5018
|
-
status: runComplete.status,
|
|
5019
|
-
total: runComplete.total_calls ?? counts?.total,
|
|
5020
|
-
formattedCalls: allCalls,
|
|
5021
|
-
verbose: options.verbose,
|
|
5022
|
-
runDetails: options.runDetails ?? {
|
|
5023
|
-
aggregate: runComplete.aggregate,
|
|
5024
|
-
error_text: runComplete.error_text
|
|
5025
|
-
}
|
|
5026
|
-
});
|
|
4759
|
+
function printRunOutput(output) {
|
|
5027
4760
|
if (!isTTY) {
|
|
5028
|
-
stdoutSync(JSON.stringify(
|
|
4761
|
+
stdoutSync(JSON.stringify(output, null, 2) + "\n");
|
|
5029
4762
|
return;
|
|
5030
4763
|
}
|
|
5031
|
-
const
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
stdoutSync(" " + parts.join(" ") + "\n");
|
|
5040
|
-
}
|
|
5041
|
-
}
|
|
5042
|
-
stdoutSync(dim(`Run ID: ${runId}`) + "\n");
|
|
5043
|
-
}
|
|
5044
|
-
function buildRunSummaryJson(options) {
|
|
5045
|
-
const calls = options.rawCalls ? formatRawCalls(options.rawCalls, options.verbose ?? false) : options.formattedCalls ?? [];
|
|
5046
|
-
const summaryData = {
|
|
5047
|
-
run_id: options.runId,
|
|
5048
|
-
...options.status != null ? { status: options.status } : {},
|
|
5049
|
-
total: options.total,
|
|
5050
|
-
...options.passed != null ? { passed: options.passed } : {},
|
|
5051
|
-
...options.failed != null ? { failed: options.failed } : {},
|
|
5052
|
-
calls
|
|
5053
|
-
};
|
|
5054
|
-
const details = options.runDetails;
|
|
5055
|
-
if (details?.created_at != null) summaryData["created_at"] = details.created_at;
|
|
5056
|
-
if (details?.started_at != null) summaryData["started_at"] = details.started_at;
|
|
5057
|
-
if (details?.finished_at != null) summaryData["finished_at"] = details.finished_at;
|
|
5058
|
-
if (details?.duration_ms != null) summaryData["duration_ms"] = details.duration_ms;
|
|
5059
|
-
if (details?.error_text != null) summaryData["error_text"] = details.error_text;
|
|
5060
|
-
if (details?.aggregate != null) {
|
|
5061
|
-
summaryData["aggregate"] = stripPassFailFromAggregate(details.aggregate);
|
|
5062
|
-
}
|
|
5063
|
-
return summaryData;
|
|
5064
|
-
}
|
|
5065
|
-
function stripPassFailFromAggregate(aggregate) {
|
|
5066
|
-
if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return aggregate;
|
|
5067
|
-
const out = {};
|
|
5068
|
-
for (const [k, v] of Object.entries(aggregate)) {
|
|
5069
|
-
if (k === "conversation_calls" && v && typeof v === "object") {
|
|
5070
|
-
const cc = v;
|
|
5071
|
-
const filtered = {};
|
|
5072
|
-
for (const [ck, cv] of Object.entries(cc)) {
|
|
5073
|
-
if (ck === "passed" || ck === "failed") continue;
|
|
5074
|
-
filtered[ck] = cv;
|
|
5075
|
-
}
|
|
5076
|
-
out[k] = filtered;
|
|
5077
|
-
} else {
|
|
5078
|
-
out[k] = v;
|
|
5079
|
-
}
|
|
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");
|
|
5080
4772
|
}
|
|
5081
|
-
|
|
5082
|
-
}
|
|
5083
|
-
function formatRawCalls(rawCalls, verbose) {
|
|
5084
|
-
return rawCalls.map((raw) => {
|
|
5085
|
-
const formatted = formatConversationResult(raw, { verbose });
|
|
5086
|
-
if (formatted) return formatted;
|
|
5087
|
-
const fallback = raw;
|
|
5088
|
-
return {
|
|
5089
|
-
name: typeof fallback["name"] === "string" ? fallback["name"] : "call",
|
|
5090
|
-
status: typeof fallback["status"] === "string" ? fallback["status"] : "unknown",
|
|
5091
|
-
duration_ms: typeof fallback["duration_ms"] === "number" ? fallback["duration_ms"] : void 0,
|
|
5092
|
-
error: typeof fallback["error"] === "string" ? fallback["error"] : null
|
|
5093
|
-
};
|
|
5094
|
-
});
|
|
4773
|
+
stdoutSync(dim(`Run ID: ${String(output.run_id)}`) + "\n");
|
|
5095
4774
|
}
|
|
5096
4775
|
function printError(message) {
|
|
5097
4776
|
const line = red(bold("error")) + ` ${message}
|
|
@@ -5128,35 +4807,25 @@ function gitSha() {
|
|
|
5128
4807
|
return null;
|
|
5129
4808
|
}
|
|
5130
4809
|
}
|
|
5131
|
-
async function saveRunHistory(
|
|
4810
|
+
async function saveRunHistory(output) {
|
|
5132
4811
|
try {
|
|
4812
|
+
const runId = output.run_id;
|
|
5133
4813
|
const dir = path2.join(process.cwd(), ".vent", "runs");
|
|
5134
4814
|
await fs2.mkdir(dir, { recursive: true });
|
|
5135
4815
|
const now = /* @__PURE__ */ new Date();
|
|
5136
4816
|
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
5137
4817
|
const shortId = runId.slice(0, 8);
|
|
5138
|
-
const
|
|
5139
|
-
const convCalls = aggregate?.conversation_calls;
|
|
5140
|
-
const total = convCalls?.total ?? 0;
|
|
5141
|
-
const passed = convCalls?.passed ?? 0;
|
|
5142
|
-
const failed = convCalls?.failed ?? 0;
|
|
4818
|
+
const calls = output.calls;
|
|
5143
4819
|
const entry = {
|
|
5144
4820
|
run_id: runId,
|
|
5145
4821
|
timestamp: now.toISOString(),
|
|
5146
4822
|
git_sha: gitSha(),
|
|
5147
4823
|
summary: {
|
|
5148
|
-
status:
|
|
5149
|
-
calls_total:
|
|
5150
|
-
|
|
5151
|
-
calls_failed: failed,
|
|
5152
|
-
total_duration_ms: aggregate?.total_duration_ms,
|
|
5153
|
-
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)
|
|
5154
4827
|
},
|
|
5155
|
-
call_results:
|
|
5156
|
-
const meta = e.metadata_json ?? {};
|
|
5157
|
-
const result = meta.result ?? {};
|
|
5158
|
-
return { ...result, status: meta.status };
|
|
5159
|
-
})
|
|
4828
|
+
call_results: calls
|
|
5160
4829
|
};
|
|
5161
4830
|
const filename = `${timestamp}_${shortId}.json`;
|
|
5162
4831
|
const filepath = path2.join(dir, filename);
|
|
@@ -5181,55 +4850,16 @@ function isRemotePlatformAdapter(adapter) {
|
|
|
5181
4850
|
function looksLikeEnvReference(value) {
|
|
5182
4851
|
return typeof value === "string" && /^[A-Z][A-Z0-9_]+$/.test(value);
|
|
5183
4852
|
}
|
|
5184
|
-
function
|
|
5185
|
-
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}`;
|
|
5186
4857
|
}
|
|
5187
|
-
function
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
}
|
|
5193
|
-
if (isMissingResolvedValue(platform.vapi_assistant_id)) {
|
|
5194
|
-
throw new Error("Missing VAPI_ASSISTANT_ID or VAPI_AGENT_ID in .env");
|
|
5195
|
-
}
|
|
5196
|
-
return;
|
|
5197
|
-
case "retell":
|
|
5198
|
-
if (isMissingResolvedValue(platform.retell_api_key)) {
|
|
5199
|
-
throw new Error("Missing RETELL_API_KEY in .env");
|
|
5200
|
-
}
|
|
5201
|
-
if (isMissingResolvedValue(platform.retell_agent_id)) {
|
|
5202
|
-
throw new Error("Missing RETELL_AGENT_ID in .env");
|
|
5203
|
-
}
|
|
5204
|
-
return;
|
|
5205
|
-
case "elevenlabs":
|
|
5206
|
-
if (isMissingResolvedValue(platform.elevenlabs_api_key)) {
|
|
5207
|
-
throw new Error("Missing ELEVENLABS_API_KEY in .env");
|
|
5208
|
-
}
|
|
5209
|
-
if (isMissingResolvedValue(platform.elevenlabs_agent_id)) {
|
|
5210
|
-
throw new Error("Missing ELEVENLABS_AGENT_ID in .env");
|
|
5211
|
-
}
|
|
5212
|
-
return;
|
|
5213
|
-
case "livekit":
|
|
5214
|
-
if (isMissingResolvedValue(platform.livekit_api_key)) {
|
|
5215
|
-
throw new Error("Missing LIVEKIT_API_KEY in .env");
|
|
5216
|
-
}
|
|
5217
|
-
if (isMissingResolvedValue(platform.livekit_api_secret)) {
|
|
5218
|
-
throw new Error("Missing LIVEKIT_API_SECRET in .env");
|
|
5219
|
-
}
|
|
5220
|
-
if (isMissingResolvedValue(platform.livekit_url)) {
|
|
5221
|
-
throw new Error("Missing LIVEKIT_URL in .env");
|
|
5222
|
-
}
|
|
5223
|
-
return;
|
|
5224
|
-
case "bland":
|
|
5225
|
-
if (isMissingResolvedValue(platform.bland_api_key)) {
|
|
5226
|
-
throw new Error("Missing BLAND_API_KEY in .env");
|
|
5227
|
-
}
|
|
5228
|
-
if (isMissingResolvedValue(platform.bland_pathway_id) && isMissingResolvedValue(platform.task) && isMissingResolvedValue(platform.persona_id)) {
|
|
5229
|
-
throw new Error("Missing BLAND_PATHWAY_ID, BLAND_PERSONA_ID, or platform.task in .env");
|
|
5230
|
-
}
|
|
5231
|
-
return;
|
|
5232
|
-
}
|
|
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)`);
|
|
5233
4863
|
}
|
|
5234
4864
|
function resolveRemotePlatformConfig(config) {
|
|
5235
4865
|
const cfg = config;
|
|
@@ -5247,12 +4877,8 @@ function resolveRemotePlatformConfig(config) {
|
|
|
5247
4877
|
const envFields = PLATFORM_ENV_MAP[adapter];
|
|
5248
4878
|
for (const [field, envVar] of Object.entries(envFields)) {
|
|
5249
4879
|
const currentValue = resolved[field];
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
const envValue = process.env[envVar];
|
|
5253
|
-
if (envValue) {
|
|
5254
|
-
resolved[field] = envValue;
|
|
5255
|
-
}
|
|
4880
|
+
if (!currentValue || looksLikeEnvReference(currentValue)) {
|
|
4881
|
+
resolved[field] = process.env[envVar] || void 0;
|
|
5256
4882
|
}
|
|
5257
4883
|
}
|
|
5258
4884
|
if (adapter === "vapi" && !resolved.vapi_assistant_id) {
|
|
@@ -5260,7 +4886,7 @@ function resolveRemotePlatformConfig(config) {
|
|
|
5260
4886
|
if (alt) resolved.vapi_assistant_id = alt;
|
|
5261
4887
|
}
|
|
5262
4888
|
const platform = PlatformConfigSchema.parse(resolved);
|
|
5263
|
-
|
|
4889
|
+
requireFields(adapter, platform);
|
|
5264
4890
|
return platform;
|
|
5265
4891
|
}
|
|
5266
4892
|
|
|
@@ -5320,15 +4946,10 @@ async function runCommand(args) {
|
|
|
5320
4946
|
);
|
|
5321
4947
|
return 2;
|
|
5322
4948
|
}
|
|
5323
|
-
const accessToken = await loadAccessToken();
|
|
5324
|
-
if (!accessToken) {
|
|
5325
|
-
printError("No Vent credentials found. Set VENT_REFRESH_TOKEN or run `npx vent-hq login`.");
|
|
5326
|
-
return 2;
|
|
5327
|
-
}
|
|
5328
4949
|
printInfo("Submitting run\u2026");
|
|
5329
4950
|
let submitResult;
|
|
5330
4951
|
try {
|
|
5331
|
-
const res = await submitRun(
|
|
4952
|
+
const res = await submitRun({ config, agent_session_id: args.session });
|
|
5332
4953
|
submitResult = await res.json();
|
|
5333
4954
|
} catch (err) {
|
|
5334
4955
|
if (err instanceof ApiError && err.status === 401) {
|
|
@@ -5347,14 +4968,13 @@ async function runCommand(args) {
|
|
|
5347
4968
|
printInfo(`Streaming results for run ${run_id}\u2026`);
|
|
5348
4969
|
const abortController = new AbortController();
|
|
5349
4970
|
let exitCode = 0;
|
|
5350
|
-
|
|
5351
|
-
let runCompleteData = null;
|
|
4971
|
+
let runOutput = null;
|
|
5352
4972
|
let stopRequested = false;
|
|
5353
4973
|
let stopPromise = null;
|
|
5354
4974
|
const onSignal = () => {
|
|
5355
4975
|
if (!stopRequested) {
|
|
5356
4976
|
stopRequested = true;
|
|
5357
|
-
stopPromise = apiFetch(`/runs/${run_id}/stop`,
|
|
4977
|
+
stopPromise = apiFetch(`/runs/${run_id}/stop`, {
|
|
5358
4978
|
method: "POST",
|
|
5359
4979
|
signal: AbortSignal.timeout(5e3)
|
|
5360
4980
|
}).catch(() => {
|
|
@@ -5365,15 +4985,10 @@ async function runCommand(args) {
|
|
|
5365
4985
|
process.on("SIGINT", onSignal);
|
|
5366
4986
|
process.on("SIGTERM", onSignal);
|
|
5367
4987
|
try {
|
|
5368
|
-
for await (const event of streamRunEvents(run_id,
|
|
5369
|
-
const meta = event.metadata_json ?? {};
|
|
4988
|
+
for await (const event of streamRunEvents(run_id, abortController.signal)) {
|
|
5370
4989
|
printEvent(event);
|
|
5371
|
-
if (event.event_type === "call_completed") {
|
|
5372
|
-
callResults.push(event);
|
|
5373
|
-
}
|
|
5374
4990
|
if (event.event_type === "run_complete") {
|
|
5375
|
-
|
|
5376
|
-
exitCode = meta["status"] === "pass" ? 0 : 1;
|
|
4991
|
+
runOutput = event.result ?? null;
|
|
5377
4992
|
}
|
|
5378
4993
|
if (event.event_type === "error") {
|
|
5379
4994
|
printError(event.message ?? "Stream connection lost");
|
|
@@ -5389,34 +5004,22 @@ async function runCommand(args) {
|
|
|
5389
5004
|
process.off("SIGINT", onSignal);
|
|
5390
5005
|
process.off("SIGTERM", onSignal);
|
|
5391
5006
|
}
|
|
5392
|
-
if (
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
printWarn("Run result fetch failed; showing the streamed summary only.");
|
|
5400
|
-
}
|
|
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.");
|
|
5401
5014
|
}
|
|
5402
|
-
printSummary(callResults, runCompleteData, run_id, {
|
|
5403
|
-
verbose: args.verbose,
|
|
5404
|
-
rawCalls: Array.isArray(rawRunDetails?.["results"]) ? rawRunDetails["results"] : void 0,
|
|
5405
|
-
runDetails: rawRunDetails ? {
|
|
5406
|
-
created_at: rawRunDetails["created_at"],
|
|
5407
|
-
started_at: rawRunDetails["started_at"],
|
|
5408
|
-
finished_at: rawRunDetails["finished_at"],
|
|
5409
|
-
duration_ms: rawRunDetails["duration_ms"],
|
|
5410
|
-
error_text: rawRunDetails["error_text"],
|
|
5411
|
-
aggregate: rawRunDetails["aggregate"]
|
|
5412
|
-
} : void 0
|
|
5413
|
-
});
|
|
5414
5015
|
}
|
|
5415
|
-
if (
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
}
|
|
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;
|
|
5420
5023
|
}
|
|
5421
5024
|
if (stopPromise) {
|
|
5422
5025
|
await stopPromise;
|
|
@@ -5431,7 +5034,11 @@ import * as net from "node:net";
|
|
|
5431
5034
|
// src/lib/relay.ts
|
|
5432
5035
|
import { spawn } from "node:child_process";
|
|
5433
5036
|
|
|
5434
|
-
//
|
|
5037
|
+
// src/lib/relay-client.ts
|
|
5038
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
5039
|
+
function errorEventMessage(ev) {
|
|
5040
|
+
return ev.message;
|
|
5041
|
+
}
|
|
5435
5042
|
var RelayClient = class {
|
|
5436
5043
|
controlWs = null;
|
|
5437
5044
|
localConnections = /* @__PURE__ */ new Map();
|
|
@@ -5451,10 +5058,10 @@ var RelayClient = class {
|
|
|
5451
5058
|
handler(...args);
|
|
5452
5059
|
}
|
|
5453
5060
|
}
|
|
5454
|
-
async connect(
|
|
5061
|
+
async connect() {
|
|
5455
5062
|
const wsBase = this.config.apiUrl.replace(/^http/, "ws");
|
|
5456
5063
|
const controlUrl = `${wsBase}/relay/control?session_id=${this.config.sessionId}&token=${this.config.relayToken}`;
|
|
5457
|
-
return new Promise((
|
|
5064
|
+
return new Promise((resolve, reject) => {
|
|
5458
5065
|
const ws = new WebSocket(controlUrl);
|
|
5459
5066
|
ws.binaryType = "arraybuffer";
|
|
5460
5067
|
let settled = false;
|
|
@@ -5468,29 +5075,28 @@ var RelayClient = class {
|
|
|
5468
5075
|
const hardTimeout = setTimeout(() => {
|
|
5469
5076
|
settle(() => {
|
|
5470
5077
|
ws.close();
|
|
5471
|
-
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}`));
|
|
5472
5079
|
});
|
|
5473
|
-
},
|
|
5080
|
+
}, CONNECT_TIMEOUT_MS);
|
|
5474
5081
|
ws.addEventListener("open", () => {
|
|
5475
5082
|
this.controlWs = ws;
|
|
5476
5083
|
this.setupControlHandlers(ws);
|
|
5477
|
-
this.emit("connected");
|
|
5478
5084
|
});
|
|
5479
|
-
this.on("ready", () => settle(() =>
|
|
5085
|
+
this.on("ready", () => settle(() => resolve()));
|
|
5480
5086
|
ws.addEventListener("close", (ev) => {
|
|
5481
5087
|
settle(() => reject(new Error(`Relay rejected the connection (${ev.code}${ev.reason ? `: ${ev.reason}` : ""})`)));
|
|
5482
5088
|
});
|
|
5483
5089
|
ws.addEventListener("error", (ev) => {
|
|
5484
5090
|
if (!this.controlWs) {
|
|
5485
|
-
settle(() => reject(new Error(`Failed to connect to relay: ${ev
|
|
5091
|
+
settle(() => reject(new Error(`Failed to connect to relay: ${errorEventMessage(ev) ?? "connection error"}`)));
|
|
5486
5092
|
}
|
|
5487
5093
|
});
|
|
5488
5094
|
});
|
|
5489
5095
|
}
|
|
5490
5096
|
async disconnect() {
|
|
5491
5097
|
this.closed = true;
|
|
5492
|
-
for (const [connId,
|
|
5493
|
-
if (
|
|
5098
|
+
for (const [connId, local] of this.localConnections) {
|
|
5099
|
+
if (local.readyState !== WebSocket.CLOSED) local.close();
|
|
5494
5100
|
this.localConnections.delete(connId);
|
|
5495
5101
|
}
|
|
5496
5102
|
if (this.controlWs) {
|
|
@@ -5523,12 +5129,12 @@ var RelayClient = class {
|
|
|
5523
5129
|
if (frameType !== 1 && frameType !== 2) return;
|
|
5524
5130
|
const connId = new TextDecoder().decode(data.subarray(1, 37));
|
|
5525
5131
|
const payload = data.subarray(37);
|
|
5526
|
-
const
|
|
5527
|
-
if (
|
|
5132
|
+
const local = this.localConnections.get(connId);
|
|
5133
|
+
if (local?.readyState === WebSocket.OPEN) {
|
|
5528
5134
|
if (frameType === 2) {
|
|
5529
|
-
|
|
5135
|
+
local.send(new TextDecoder().decode(payload));
|
|
5530
5136
|
} else {
|
|
5531
|
-
|
|
5137
|
+
local.send(payload);
|
|
5532
5138
|
}
|
|
5533
5139
|
}
|
|
5534
5140
|
return;
|
|
@@ -5541,8 +5147,8 @@ var RelayClient = class {
|
|
|
5541
5147
|
} else if (msg.type === "new_connection" && msg.conn_id) {
|
|
5542
5148
|
this.handleNewConnection(msg.conn_id);
|
|
5543
5149
|
} else if (msg.type === "close" && msg.conn_id) {
|
|
5544
|
-
const
|
|
5545
|
-
if (
|
|
5150
|
+
const local = this.localConnections.get(msg.conn_id);
|
|
5151
|
+
if (local && local.readyState !== WebSocket.CLOSED) local.close();
|
|
5546
5152
|
this.localConnections.delete(msg.conn_id);
|
|
5547
5153
|
}
|
|
5548
5154
|
} catch {
|
|
@@ -5559,58 +5165,44 @@ var RelayClient = class {
|
|
|
5559
5165
|
}
|
|
5560
5166
|
});
|
|
5561
5167
|
ws.addEventListener("error", (ev) => {
|
|
5562
|
-
this.emit("error", new Error(ev
|
|
5168
|
+
this.emit("error", new Error(errorEventMessage(ev) ?? "WebSocket error"));
|
|
5563
5169
|
});
|
|
5564
5170
|
}
|
|
5565
5171
|
handleNewConnection(connId) {
|
|
5566
|
-
const
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
this.
|
|
5575
|
-
}
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
this.localConnections.delete(connId);
|
|
5590
|
-
this.sendControlMessage({ type: "close", conn_id: connId });
|
|
5591
|
-
};
|
|
5592
|
-
localWs.addEventListener("close", () => cleanup("close"));
|
|
5593
|
-
localWs.addEventListener("error", (ev) => {
|
|
5594
|
-
const msg = ev.message ?? "unknown error";
|
|
5595
|
-
this.emit("log", `[relay] local WS error for ${connId}: ${msg}`);
|
|
5596
|
-
cleanup(`error: ${msg}`);
|
|
5597
|
-
});
|
|
5598
|
-
} catch (err) {
|
|
5599
|
-
this.emit("log", `[relay] Failed to connect local agent for ${connId}: ${err}`);
|
|
5600
|
-
}
|
|
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);
|
|
5601
5195
|
}
|
|
5602
5196
|
};
|
|
5603
5197
|
|
|
5604
5198
|
// src/lib/relay.ts
|
|
5605
5199
|
async function startAgentSession(relayConfig) {
|
|
5606
|
-
const
|
|
5200
|
+
const client = new RelayClient({
|
|
5607
5201
|
apiUrl: relayConfig.api_url,
|
|
5608
5202
|
sessionId: relayConfig.session_id,
|
|
5609
5203
|
relayToken: relayConfig.relay_token,
|
|
5610
|
-
agentPort: relayConfig.agent_port
|
|
5611
|
-
|
|
5612
|
-
};
|
|
5613
|
-
const client = new RelayClient(clientConfig);
|
|
5204
|
+
agentPort: relayConfig.agent_port
|
|
5205
|
+
});
|
|
5614
5206
|
await client.connect();
|
|
5615
5207
|
let agentProcess = null;
|
|
5616
5208
|
if (relayConfig.start_command) {
|
|
@@ -5626,8 +5218,6 @@ async function startAgentSession(relayConfig) {
|
|
|
5626
5218
|
process.stdout.write(`Agent process error: ${err.message}
|
|
5627
5219
|
`);
|
|
5628
5220
|
});
|
|
5629
|
-
}
|
|
5630
|
-
if (relayConfig.start_command) {
|
|
5631
5221
|
await waitForHealth(relayConfig.agent_port, relayConfig.health_endpoint);
|
|
5632
5222
|
}
|
|
5633
5223
|
const cleanup = async () => {
|
|
@@ -5654,11 +5244,6 @@ async function waitForHealth(port, endpoint, timeoutMs = 3e4) {
|
|
|
5654
5244
|
|
|
5655
5245
|
// src/commands/agent.ts
|
|
5656
5246
|
async function agentStartCommand(args) {
|
|
5657
|
-
const accessToken = await loadAccessToken();
|
|
5658
|
-
if (!accessToken) {
|
|
5659
|
-
printError("No Vent credentials found. Set VENT_REFRESH_TOKEN or run `npx vent-hq login`.");
|
|
5660
|
-
return 2;
|
|
5661
|
-
}
|
|
5662
5247
|
let parsedConfig;
|
|
5663
5248
|
try {
|
|
5664
5249
|
if (args.file) {
|
|
@@ -5687,7 +5272,7 @@ async function agentStartCommand(args) {
|
|
|
5687
5272
|
const closeSession = async () => {
|
|
5688
5273
|
if (!session) return;
|
|
5689
5274
|
try {
|
|
5690
|
-
await apiFetch(`/agent-sessions/${session.session_id}/close`,
|
|
5275
|
+
await apiFetch(`/agent-sessions/${session.session_id}/close`, {
|
|
5691
5276
|
method: "POST"
|
|
5692
5277
|
});
|
|
5693
5278
|
} catch {
|
|
@@ -5695,11 +5280,11 @@ async function agentStartCommand(args) {
|
|
|
5695
5280
|
};
|
|
5696
5281
|
try {
|
|
5697
5282
|
printInfo("Creating agent session\u2026");
|
|
5698
|
-
const createRes = await apiFetch("/agent-sessions",
|
|
5283
|
+
const createRes = await apiFetch("/agent-sessions", {
|
|
5699
5284
|
method: "POST",
|
|
5700
5285
|
body: JSON.stringify({ config: connection })
|
|
5701
5286
|
});
|
|
5702
|
-
session = await createRes.json();
|
|
5287
|
+
session = AgentSessionResponseSchema.parse(await createRes.json());
|
|
5703
5288
|
printInfo("Connecting relay for local agent\u2026");
|
|
5704
5289
|
relay = await startAgentSession(session);
|
|
5705
5290
|
const sessionData = {
|
|
@@ -5725,13 +5310,8 @@ async function agentStartCommand(args) {
|
|
|
5725
5310
|
}
|
|
5726
5311
|
}
|
|
5727
5312
|
async function agentStopCommand(args) {
|
|
5728
|
-
const accessToken = await loadAccessToken();
|
|
5729
|
-
if (!accessToken) {
|
|
5730
|
-
printError("No Vent credentials found. Set VENT_REFRESH_TOKEN or run `npx vent-hq login`.");
|
|
5731
|
-
return 2;
|
|
5732
|
-
}
|
|
5733
5313
|
try {
|
|
5734
|
-
await apiFetch(`/agent-sessions/${args.sessionId}/close`,
|
|
5314
|
+
await apiFetch(`/agent-sessions/${args.sessionId}/close`, {
|
|
5735
5315
|
method: "POST"
|
|
5736
5316
|
});
|
|
5737
5317
|
printSuccess(`Agent session closed: ${args.sessionId}`, { force: true });
|
|
@@ -5742,7 +5322,7 @@ async function agentStopCommand(args) {
|
|
|
5742
5322
|
}
|
|
5743
5323
|
}
|
|
5744
5324
|
async function waitForSessionExit(relay) {
|
|
5745
|
-
return new Promise((
|
|
5325
|
+
return new Promise((resolve) => {
|
|
5746
5326
|
let settled = false;
|
|
5747
5327
|
const agentProcess = relay.agentProcess;
|
|
5748
5328
|
const finish = (code) => {
|
|
@@ -5753,7 +5333,7 @@ async function waitForSessionExit(relay) {
|
|
|
5753
5333
|
if (agentProcess) {
|
|
5754
5334
|
agentProcess.off("exit", handleAgentExit);
|
|
5755
5335
|
}
|
|
5756
|
-
|
|
5336
|
+
resolve(code);
|
|
5757
5337
|
};
|
|
5758
5338
|
const handleSignal = () => finish(0);
|
|
5759
5339
|
const handleAgentExit = (code, signal) => {
|
|
@@ -5782,12 +5362,12 @@ async function waitForSessionExit(relay) {
|
|
|
5782
5362
|
});
|
|
5783
5363
|
}
|
|
5784
5364
|
function findFreePort() {
|
|
5785
|
-
return new Promise((
|
|
5365
|
+
return new Promise((resolve, reject) => {
|
|
5786
5366
|
const server = net.createServer();
|
|
5787
5367
|
server.listen(0, () => {
|
|
5788
5368
|
const addr = server.address();
|
|
5789
5369
|
const port = addr.port;
|
|
5790
|
-
server.close(() =>
|
|
5370
|
+
server.close(() => resolve(port));
|
|
5791
5371
|
});
|
|
5792
5372
|
server.on("error", reject);
|
|
5793
5373
|
});
|
|
@@ -5840,7 +5420,7 @@ async function deviceAuthFlow() {
|
|
|
5840
5420
|
const parsed = AccessCredentialsSchema.safeParse(data);
|
|
5841
5421
|
if (!parsed.success) return { ok: false, error: "Vent returned invalid sign-in credentials." };
|
|
5842
5422
|
await saveCredentials(parsed.data);
|
|
5843
|
-
return { ok: true
|
|
5423
|
+
return { ok: true };
|
|
5844
5424
|
}
|
|
5845
5425
|
if (data.status === "expired") {
|
|
5846
5426
|
return { ok: false, error: "Session expired. Run `npx vent-hq login` again." };
|
|
@@ -5903,17 +5483,47 @@ function detectGitHubToken() {
|
|
|
5903
5483
|
import * as fs5 from "node:fs/promises";
|
|
5904
5484
|
import * as path3 from "node:path";
|
|
5905
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
|
+
|
|
5906
5489
|
// src/skills/claude-code.md
|
|
5907
|
-
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 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` and `max_turns` are required.\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. 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` 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` = 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`, `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). 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\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, 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 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';
|
|
5908
5491
|
|
|
5909
5492
|
// src/skills/cursor.md
|
|
5910
|
-
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 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` and `max_turns` are required.\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. 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` 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` = 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`, `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). 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\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, 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 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";
|
|
5911
5494
|
|
|
5912
5495
|
// src/skills/codex.md
|
|
5913
|
-
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 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` and `max_turns` are required.\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. 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` 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` = 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`, `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). 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\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, 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 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";
|
|
5914
5497
|
|
|
5915
5498
|
// src/skills/windsurf.md
|
|
5916
|
-
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 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` and `max_turns` are required.\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. 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` 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` = 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`, `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). 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\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, 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 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
|
+
});
|
|
5917
5527
|
|
|
5918
5528
|
// src/lib/setup.ts
|
|
5919
5529
|
var SUITE_SCAFFOLD = JSON.stringify(
|
|
@@ -5921,7 +5531,7 @@ var SUITE_SCAFFOLD = JSON.stringify(
|
|
|
5921
5531
|
connection: {
|
|
5922
5532
|
adapter: "websocket",
|
|
5923
5533
|
start_command: "TODO: command to start your agent (e.g. npm run start)",
|
|
5924
|
-
agent_port:
|
|
5534
|
+
agent_port: DEFAULT_AGENT_PORT
|
|
5925
5535
|
},
|
|
5926
5536
|
calls: {
|
|
5927
5537
|
"happy-path": {
|
|
@@ -5936,19 +5546,19 @@ var SUITE_SCAFFOLD = JSON.stringify(
|
|
|
5936
5546
|
async function installClaudeCode(cwd) {
|
|
5937
5547
|
const dir = path3.join(cwd, ".claude", "skills", "vent");
|
|
5938
5548
|
await fs5.mkdir(dir, { recursive: true });
|
|
5939
|
-
await fs5.writeFile(path3.join(dir, "SKILL.md"),
|
|
5549
|
+
await fs5.writeFile(path3.join(dir, "SKILL.md"), claudeCodeSkill);
|
|
5940
5550
|
printSuccess("Claude Code: .claude/skills/vent/SKILL.md", { force: true });
|
|
5941
5551
|
}
|
|
5942
5552
|
async function installCursor(cwd) {
|
|
5943
5553
|
const dir = path3.join(cwd, ".cursor", "rules");
|
|
5944
5554
|
await fs5.mkdir(dir, { recursive: true });
|
|
5945
|
-
await fs5.writeFile(path3.join(dir, "vent.mdc"),
|
|
5555
|
+
await fs5.writeFile(path3.join(dir, "vent.mdc"), cursorSkill);
|
|
5946
5556
|
printSuccess("Cursor: .cursor/rules/vent.mdc", { force: true });
|
|
5947
5557
|
}
|
|
5948
5558
|
async function installWindsurf(cwd) {
|
|
5949
5559
|
const dir = path3.join(cwd, ".windsurf", "skills", "vent");
|
|
5950
5560
|
await fs5.mkdir(dir, { recursive: true });
|
|
5951
|
-
await fs5.writeFile(path3.join(dir, "SKILL.md"),
|
|
5561
|
+
await fs5.writeFile(path3.join(dir, "SKILL.md"), windsurfSkill);
|
|
5952
5562
|
printSuccess("Windsurf: .windsurf/skills/vent/SKILL.md", { force: true });
|
|
5953
5563
|
}
|
|
5954
5564
|
var VENT_MARKERS = [
|
|
@@ -5964,11 +5574,11 @@ async function installCodex(cwd) {
|
|
|
5964
5574
|
}
|
|
5965
5575
|
const markerIndex = VENT_MARKERS.map((marker) => existing.indexOf(marker)).filter((idx) => idx >= 0).sort((a, b) => a - b)[0];
|
|
5966
5576
|
if (markerIndex != null) {
|
|
5967
|
-
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");
|
|
5968
5578
|
} else if (existing) {
|
|
5969
|
-
await fs5.writeFile(filePath, existing.trimEnd() + "\n\n" +
|
|
5579
|
+
await fs5.writeFile(filePath, existing.trimEnd() + "\n\n" + codexSkill + "\n");
|
|
5970
5580
|
} else {
|
|
5971
|
-
await fs5.writeFile(filePath,
|
|
5581
|
+
await fs5.writeFile(filePath, codexSkill + "\n");
|
|
5972
5582
|
}
|
|
5973
5583
|
printSuccess("Codex: AGENTS.md", { force: true });
|
|
5974
5584
|
}
|
|
@@ -6042,13 +5652,8 @@ async function initCommand() {
|
|
|
6042
5652
|
|
|
6043
5653
|
// src/commands/stop.ts
|
|
6044
5654
|
async function stopCommand(args) {
|
|
6045
|
-
const accessToken = await loadAccessToken();
|
|
6046
|
-
if (!accessToken) {
|
|
6047
|
-
printError("Not authenticated. Run `npx vent-hq init` first.");
|
|
6048
|
-
return 2;
|
|
6049
|
-
}
|
|
6050
5655
|
try {
|
|
6051
|
-
const res = await apiFetch(`/runs/${args.runId}/stop`,
|
|
5656
|
+
const res = await apiFetch(`/runs/${args.runId}/stop`, {
|
|
6052
5657
|
method: "POST"
|
|
6053
5658
|
});
|
|
6054
5659
|
const data = await res.json();
|
|
@@ -6068,26 +5673,10 @@ async function stopCommand(args) {
|
|
|
6068
5673
|
}
|
|
6069
5674
|
|
|
6070
5675
|
// src/lib/dotenv.ts
|
|
6071
|
-
|
|
6072
|
-
import { resolve } from "node:path";
|
|
6073
|
-
function loadDotenv(dir = process.cwd()) {
|
|
5676
|
+
function loadDotenv() {
|
|
6074
5677
|
for (const file of [".env.local", ".env"]) {
|
|
6075
5678
|
try {
|
|
6076
|
-
|
|
6077
|
-
for (const line of content.split("\n")) {
|
|
6078
|
-
const trimmed = line.trim();
|
|
6079
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
6080
|
-
const eqIdx = trimmed.indexOf("=");
|
|
6081
|
-
if (eqIdx === -1) continue;
|
|
6082
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
6083
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
6084
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
6085
|
-
value = value.slice(1, -1);
|
|
6086
|
-
}
|
|
6087
|
-
if (process.env[key] === void 0) {
|
|
6088
|
-
process.env[key] = value;
|
|
6089
|
-
}
|
|
6090
|
-
}
|
|
5679
|
+
process.loadEnvFile(file);
|
|
6091
5680
|
} catch {
|
|
6092
5681
|
}
|
|
6093
5682
|
}
|
|
@@ -6104,17 +5693,21 @@ Commands:
|
|
|
6104
5693
|
login Authenticate via browser
|
|
6105
5694
|
logout Remove saved credentials
|
|
6106
5695
|
Options:
|
|
6107
|
-
--help
|
|
6108
|
-
--version Show version
|
|
5696
|
+
--help, -h Show help
|
|
5697
|
+
--version, -v Show version
|
|
6109
5698
|
|
|
6110
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.`;
|
|
6111
5705
|
var RUN_USAGE = `Usage: vent-hq run -f <suite.json> [options]
|
|
6112
5706
|
|
|
6113
5707
|
Options:
|
|
6114
5708
|
--file, -f Path to suite JSON file (required)
|
|
6115
5709
|
--call Name of the call to run (required if suite has multiple calls)
|
|
6116
|
-
--session, -s Reuse an existing local agent session
|
|
6117
|
-
--verbose, -v Include verbose fields in the result JSON`;
|
|
5710
|
+
--session, -s Reuse an existing local agent session`;
|
|
6118
5711
|
var AGENT_USAGE = `Usage: vent-hq agent <command> [options]
|
|
6119
5712
|
|
|
6120
5713
|
Commands:
|
|
@@ -6127,16 +5720,34 @@ Start options:
|
|
|
6127
5720
|
|
|
6128
5721
|
Stop options:
|
|
6129
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
|
+
};
|
|
6130
5741
|
async function main() {
|
|
6131
5742
|
loadDotenv();
|
|
6132
5743
|
const args = process.argv.slice(2);
|
|
6133
5744
|
const command = args[0];
|
|
6134
|
-
if (!command ||
|
|
6135
|
-
console.log(USAGE);
|
|
5745
|
+
if (!command || args.includes("--help") || args.includes("-h")) {
|
|
5746
|
+
console.log(COMMAND_USAGE[command ?? ""] ?? USAGE);
|
|
6136
5747
|
return 0;
|
|
6137
5748
|
}
|
|
6138
5749
|
if (command === "--version" || command === "-v") {
|
|
6139
|
-
const pkg = await import("./package-
|
|
5750
|
+
const pkg = await import("./package-WDXEE7G3.mjs");
|
|
6140
5751
|
console.log(`vent-hq ${pkg.default.version}`);
|
|
6141
5752
|
return 0;
|
|
6142
5753
|
}
|
|
@@ -6146,17 +5757,12 @@ async function main() {
|
|
|
6146
5757
|
return initCommand();
|
|
6147
5758
|
}
|
|
6148
5759
|
case "run": {
|
|
6149
|
-
if (commandArgs.includes("--help")) {
|
|
6150
|
-
console.log(RUN_USAGE);
|
|
6151
|
-
return 0;
|
|
6152
|
-
}
|
|
6153
5760
|
const { values } = parseArgs({
|
|
6154
5761
|
args: commandArgs,
|
|
6155
5762
|
options: {
|
|
6156
5763
|
file: { type: "string", short: "f" },
|
|
6157
5764
|
call: { type: "string" },
|
|
6158
|
-
session: { type: "string", short: "s" }
|
|
6159
|
-
verbose: { type: "boolean", short: "v", default: false }
|
|
5765
|
+
session: { type: "string", short: "s" }
|
|
6160
5766
|
},
|
|
6161
5767
|
strict: true
|
|
6162
5768
|
});
|
|
@@ -6168,13 +5774,12 @@ async function main() {
|
|
|
6168
5774
|
return runCommand({
|
|
6169
5775
|
file: values.file,
|
|
6170
5776
|
call: values.call,
|
|
6171
|
-
session: values.session
|
|
6172
|
-
verbose: values.verbose
|
|
5777
|
+
session: values.session
|
|
6173
5778
|
});
|
|
6174
5779
|
}
|
|
6175
5780
|
case "agent": {
|
|
6176
5781
|
const subcommand = commandArgs[0];
|
|
6177
|
-
if (!subcommand
|
|
5782
|
+
if (!subcommand) {
|
|
6178
5783
|
console.log(AGENT_USAGE);
|
|
6179
5784
|
return 0;
|
|
6180
5785
|
}
|
|
@@ -6206,9 +5811,9 @@ async function main() {
|
|
|
6206
5811
|
}
|
|
6207
5812
|
case "stop": {
|
|
6208
5813
|
const runId = commandArgs[0];
|
|
6209
|
-
if (!runId
|
|
6210
|
-
console.log(
|
|
6211
|
-
return
|
|
5814
|
+
if (!runId) {
|
|
5815
|
+
console.log(STOP_USAGE);
|
|
5816
|
+
return 2;
|
|
6212
5817
|
}
|
|
6213
5818
|
return stopCommand({ runId });
|
|
6214
5819
|
}
|