@app-vitals/agent-runtime 0.7.0 → 0.7.1
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/boot.ts +32 -2
- package/package.json +3 -3
- package/voice.ts +51 -0
- package/voice.unit.test.ts +74 -1
package/boot.ts
CHANGED
|
@@ -111,7 +111,13 @@ import {
|
|
|
111
111
|
} from "./sync-config-clone.ts";
|
|
112
112
|
import { createTranscriptUploader } from "./transcript-uploader.ts";
|
|
113
113
|
import { resolveDisplayName, resolveUserEmail } from "./users.ts";
|
|
114
|
-
import {
|
|
114
|
+
import {
|
|
115
|
+
buildVoiceConfig,
|
|
116
|
+
revertRemovedVoiceEnv,
|
|
117
|
+
synthesizeSpeech as synthesizeSpeechStatic,
|
|
118
|
+
transcribeAudio as transcribeAudioStatic,
|
|
119
|
+
VOICE_ENV_KEYS,
|
|
120
|
+
} from "./voice.ts";
|
|
115
121
|
import {
|
|
116
122
|
buildProductionDeps as buildWorktreeReaperDeps,
|
|
117
123
|
reconcileStaleWorktrees,
|
|
@@ -178,6 +184,16 @@ export async function startAgent(
|
|
|
178
184
|
// saved to the admin DB after boot only reach this process via syncConfig()'s
|
|
179
185
|
// `applyEnvToProcess(bundle.env)`, so re-reading `config.slack.*`
|
|
180
186
|
// here would keep seeing the boot-time (absent) values forever.
|
|
187
|
+
// COS-62.1: voice settings are likewise resolved from live process.env at
|
|
188
|
+
// call time (per-agent ELEVENLABS_* arrive via syncConfig after boot). The
|
|
189
|
+
// config arg the call sites pass is the boot snapshot and is ignored.
|
|
190
|
+
const synthesizeSpeech: typeof synthesizeSpeechStatic = (
|
|
191
|
+
text,
|
|
192
|
+
_cfg,
|
|
193
|
+
...rest
|
|
194
|
+
) => synthesizeSpeechStatic(text, buildVoiceConfig(), ...rest);
|
|
195
|
+
const transcribeAudio: typeof transcribeAudioStatic = (path, _cfg, ...rest) =>
|
|
196
|
+
transcribeAudioStatic(path, buildVoiceConfig(), ...rest);
|
|
181
197
|
function buildSlackAppConfig() {
|
|
182
198
|
return {
|
|
183
199
|
botToken: process.env.SLACK_BOT_TOKEN ?? "",
|
|
@@ -357,7 +373,7 @@ export async function startAgent(
|
|
|
357
373
|
process.env.SENTRY_DSN ? Sentry : undefined,
|
|
358
374
|
undefined, // fileDownloaderFn — default
|
|
359
375
|
config.voice,
|
|
360
|
-
|
|
376
|
+
transcribeAudio, // transcribeAudioFn — live-env wrapper
|
|
361
377
|
synthesizeSpeech,
|
|
362
378
|
(userId, client) => resolveDisplayName(userId, client),
|
|
363
379
|
undefined, // botUserId — resolved by Bolt
|
|
@@ -461,6 +477,10 @@ export async function startAgent(
|
|
|
461
477
|
|
|
462
478
|
if (runtimeClient && agentId) {
|
|
463
479
|
let configNotFoundLogged = false;
|
|
480
|
+
const voiceEnvBaseline: Record<string, string | undefined> = {
|
|
481
|
+
...process.env,
|
|
482
|
+
};
|
|
483
|
+
let bundleVoiceKeys = new Set<string>();
|
|
464
484
|
|
|
465
485
|
async function syncConfig() {
|
|
466
486
|
if (!runtimeClient || !agentId) return;
|
|
@@ -472,6 +492,16 @@ export async function startAgent(
|
|
|
472
492
|
// federation the Anthropic credential keys never reach process.env,
|
|
473
493
|
// even if a stale admin still serves them.
|
|
474
494
|
const changed = applyEnvToProcess(bundle.env);
|
|
495
|
+
changed.push(
|
|
496
|
+
...revertRemovedVoiceEnv(
|
|
497
|
+
bundleVoiceKeys,
|
|
498
|
+
bundle.env,
|
|
499
|
+
voiceEnvBaseline,
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
bundleVoiceKeys = new Set(
|
|
503
|
+
VOICE_ENV_KEYS.filter((k) => k in bundle.env),
|
|
504
|
+
);
|
|
475
505
|
if (changed.length > 0) {
|
|
476
506
|
console.log(`[config-sync] updated: ${changed.join(", ")}`);
|
|
477
507
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-vitals/agent-runtime",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/app-vitals/agent.git",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"./workspace-paths": "./workspace-paths.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@app-vitals/agent-admin": "0.7.
|
|
44
|
-
"@app-vitals/agent-lib": "0.7.
|
|
43
|
+
"@app-vitals/agent-admin": "0.7.1",
|
|
44
|
+
"@app-vitals/agent-lib": "0.7.1",
|
|
45
45
|
"@octokit/auth-app": "^8.2.0",
|
|
46
46
|
"@sentry/bun": "^10.63.0",
|
|
47
47
|
"@slack/bolt": "^5.0.0",
|
package/voice.ts
CHANGED
|
@@ -16,6 +16,56 @@ export interface VoiceConfig {
|
|
|
16
16
|
whisperServiceUrl?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Env vars that make up VoiceConfig — the keys per-agent env may set/remove live. */
|
|
20
|
+
export const VOICE_ENV_KEYS = [
|
|
21
|
+
"GROQ_API_KEY",
|
|
22
|
+
"ELEVENLABS_API_KEY",
|
|
23
|
+
"ELEVENLABS_VOICE_ID",
|
|
24
|
+
"PIPER_VOICE",
|
|
25
|
+
"WHISPER_SERVICE_URL",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve VoiceConfig from the live process env (COS-62.1). Per-agent env only
|
|
30
|
+
* reaches process.env via syncConfig()'s applyEnvToProcess after boot, so the
|
|
31
|
+
* boot-time `config.voice` snapshot can never contain it — mirrors
|
|
32
|
+
* buildSlackAppConfig() in boot.ts. Empty strings count as unset.
|
|
33
|
+
*/
|
|
34
|
+
export function buildVoiceConfig(
|
|
35
|
+
env: Record<string, string | undefined> = process.env,
|
|
36
|
+
): VoiceConfig {
|
|
37
|
+
return {
|
|
38
|
+
groqApiKey: env.GROQ_API_KEY || undefined,
|
|
39
|
+
elevenLabsApiKey: env.ELEVENLABS_API_KEY || undefined,
|
|
40
|
+
voiceId: env.ELEVENLABS_VOICE_ID || undefined,
|
|
41
|
+
piperVoice: env.PIPER_VOICE || undefined,
|
|
42
|
+
whisperServiceUrl: env.WHISPER_SERVICE_URL || undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* applyEnvToProcess only ever sets keys, so a voice key removed from the
|
|
48
|
+
* per-agent bundle would linger in process.env until restart. Restores each
|
|
49
|
+
* previously bundle-supplied voice key that is now absent to its pre-sync
|
|
50
|
+
* value (`baseline`: the pod env, e.g. fleet-wide admin.extraEnv) or deletes
|
|
51
|
+
* it. Returns the keys reverted.
|
|
52
|
+
*/
|
|
53
|
+
export function revertRemovedVoiceEnv(
|
|
54
|
+
previousBundleKeys: ReadonlySet<string>,
|
|
55
|
+
bundleEnv: Record<string, string>,
|
|
56
|
+
baseline: Record<string, string | undefined>,
|
|
57
|
+
processEnv: Record<string, string | undefined> = process.env,
|
|
58
|
+
): string[] {
|
|
59
|
+
const reverted: string[] = [];
|
|
60
|
+
for (const key of VOICE_ENV_KEYS) {
|
|
61
|
+
if (!previousBundleKeys.has(key) || key in bundleEnv) continue;
|
|
62
|
+
if (baseline[key] === undefined) delete processEnv[key];
|
|
63
|
+
else processEnv[key] = baseline[key];
|
|
64
|
+
reverted.push(key);
|
|
65
|
+
}
|
|
66
|
+
return reverted;
|
|
67
|
+
}
|
|
68
|
+
|
|
19
69
|
// DI seam for the whisper-svc HTTP client — replaces spawnFn from the old local-Whisper path
|
|
20
70
|
export type WhisperClientFn = (
|
|
21
71
|
audioBuffer: Buffer,
|
|
@@ -203,6 +253,7 @@ export async function synthesizeSpeech(
|
|
|
203
253
|
mkdirSync(VOICE_DIR, { recursive: true });
|
|
204
254
|
|
|
205
255
|
const elevenKey = voiceConfig.elevenLabsApiKey;
|
|
256
|
+
console.log(`[voice] TTS backend: ${elevenKey ? "elevenlabs" : "piper"}`);
|
|
206
257
|
if (elevenKey) {
|
|
207
258
|
return synthesizeElevenLabs(
|
|
208
259
|
text,
|
package/voice.unit.test.ts
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { describe, expect, mock, test } from "bun:test";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildVoiceConfig,
|
|
23
|
+
makeWhisperSvcClient,
|
|
24
|
+
revertRemovedVoiceEnv,
|
|
25
|
+
} from "./voice.ts";
|
|
22
26
|
|
|
23
27
|
describe("makeWhisperSvcClient — onerahmet /asr contract", () => {
|
|
24
28
|
test("POSTs to the /asr endpoint of the service URL", async () => {
|
|
@@ -103,3 +107,72 @@ describe("makeWhisperSvcClient — onerahmet /asr contract", () => {
|
|
|
103
107
|
expect(result).toBeNull();
|
|
104
108
|
});
|
|
105
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("buildVoiceConfig — live env (COS-62.1)", () => {
|
|
112
|
+
test("picks up ELEVENLABS_* set after an earlier build (env mutation)", () => {
|
|
113
|
+
const env: Record<string, string | undefined> = {};
|
|
114
|
+
expect(buildVoiceConfig(env).elevenLabsApiKey).toBeUndefined();
|
|
115
|
+
|
|
116
|
+
env.ELEVENLABS_API_KEY = "xi-key";
|
|
117
|
+
env.ELEVENLABS_VOICE_ID = "nPczCjzI2devNBz1zQrb";
|
|
118
|
+
const cfg = buildVoiceConfig(env);
|
|
119
|
+
expect(cfg.elevenLabsApiKey).toBe("xi-key");
|
|
120
|
+
expect(cfg.voiceId).toBe("nPczCjzI2devNBz1zQrb");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("falls back to unset once the key is removed or empty", () => {
|
|
124
|
+
const env: Record<string, string | undefined> = { ELEVENLABS_API_KEY: "k" };
|
|
125
|
+
delete env.ELEVENLABS_API_KEY;
|
|
126
|
+
expect(buildVoiceConfig(env).elevenLabsApiKey).toBeUndefined();
|
|
127
|
+
env.ELEVENLABS_API_KEY = "";
|
|
128
|
+
expect(buildVoiceConfig(env).elevenLabsApiKey).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("revertRemovedVoiceEnv", () => {
|
|
133
|
+
test("deletes a removed per-agent key with no pod baseline", () => {
|
|
134
|
+
const proc: Record<string, string | undefined> = {
|
|
135
|
+
ELEVENLABS_API_KEY: "k",
|
|
136
|
+
ELEVENLABS_VOICE_ID: "v",
|
|
137
|
+
};
|
|
138
|
+
const reverted = revertRemovedVoiceEnv(
|
|
139
|
+
new Set(["ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID"]),
|
|
140
|
+
{},
|
|
141
|
+
{},
|
|
142
|
+
proc,
|
|
143
|
+
);
|
|
144
|
+
expect(reverted.sort()).toEqual([
|
|
145
|
+
"ELEVENLABS_API_KEY",
|
|
146
|
+
"ELEVENLABS_VOICE_ID",
|
|
147
|
+
]);
|
|
148
|
+
expect(proc.ELEVENLABS_API_KEY).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("restores the fleet/pod value when the per-agent override is removed", () => {
|
|
152
|
+
const proc: Record<string, string | undefined> = {
|
|
153
|
+
ELEVENLABS_API_KEY: "agent",
|
|
154
|
+
};
|
|
155
|
+
revertRemovedVoiceEnv(
|
|
156
|
+
new Set(["ELEVENLABS_API_KEY"]),
|
|
157
|
+
{},
|
|
158
|
+
{ ELEVENLABS_API_KEY: "fleet" },
|
|
159
|
+
proc,
|
|
160
|
+
);
|
|
161
|
+
expect(proc.ELEVENLABS_API_KEY).toBe("fleet");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("leaves keys alone that are still in the bundle or never came from it", () => {
|
|
165
|
+
const proc: Record<string, string | undefined> = {
|
|
166
|
+
ELEVENLABS_API_KEY: "a",
|
|
167
|
+
GROQ_API_KEY: "pod",
|
|
168
|
+
};
|
|
169
|
+
const reverted = revertRemovedVoiceEnv(
|
|
170
|
+
new Set(["ELEVENLABS_API_KEY"]),
|
|
171
|
+
{ ELEVENLABS_API_KEY: "a" },
|
|
172
|
+
{},
|
|
173
|
+
proc,
|
|
174
|
+
);
|
|
175
|
+
expect(reverted).toEqual([]);
|
|
176
|
+
expect(proc.GROQ_API_KEY).toBe("pod");
|
|
177
|
+
});
|
|
178
|
+
});
|