@stage-labs/metro 0.1.0-beta.307 → 0.1.0-beta.308
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +23 -28
- package/dist/child.js +33 -0
- package/dist/claude-env.js +104 -0
- package/dist/cli.js +7 -21
- package/dist/lock.js +62 -0
- package/dist/memory.js +12 -27
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +1 -1
- package/dist/route.js +0 -1
- package/hooks/guard.mjs +1 -5
- package/hooks/session-start.mjs +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +3 -8
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
- package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +32 -112
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
- package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +15 -35
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/dist/background.js +0 -141
- package/dist/claude.js +0 -204
- package/dist/mcp-config.js +0 -27
- package/dist/native-recovery-scan.js +0 -246
- package/dist/native-recovery-state.js +0 -154
- package/dist/native-recovery.js +0 -164
- package/dist/onboarding.js +0 -44
- package/dist/voice.js +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
- package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
- package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { log } from '@metro-labs/core/log';
|
|
2
|
-
import { downsample3, OpusIn, Player, type Playback } from './audio.js';
|
|
3
|
-
import type { CallApprovals } from './approvals.js';
|
|
4
|
-
import { Brain, type Thinking, type ToolAsk } from './brain.js';
|
|
5
|
-
import { Scribe, VAD_SILENCE_SECS } from './scribe.js';
|
|
6
|
-
import { Utterance } from './speech.js';
|
|
7
|
-
import { Chunker } from './speakable.js';
|
|
8
|
-
import { brainModel, languageOf, voiceIdOf, type VoiceConfig } from './store.js';
|
|
9
|
-
|
|
10
|
-
const BARGE_MIN_WORDS = 2;
|
|
11
|
-
|
|
12
|
-
interface Reply {
|
|
13
|
-
utterance: Utterance;
|
|
14
|
-
said: string;
|
|
15
|
-
heard: number;
|
|
16
|
-
chunker: Chunker;
|
|
17
|
-
askedAt: number | null;
|
|
18
|
-
saidAt: number | null;
|
|
19
|
-
firstText: number | null;
|
|
20
|
-
firstAudio: number | null;
|
|
21
|
-
firstPlayed: number | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const toolName = (tool: string): string => tool.split('__').pop() ?? tool;
|
|
25
|
-
|
|
26
|
-
const approvalNote = (tool: string, id: string): string =>
|
|
27
|
-
`[The owner was asked in the chat of this call to approve ${toolName(tool)}; they answer "yes ${id}" or "no ${id}" there. Unless you already said so, tell the caller in one short sentence that it waits for their answer in the chat.]`;
|
|
28
|
-
|
|
29
|
-
const wordsIn = (text: string): number => text.split(/\s+/).filter((word) => /\p{L}/u.test(word)).length;
|
|
30
|
-
|
|
31
|
-
const since = (start: number | null, at: number | null): number | null =>
|
|
32
|
-
start === null || at === null ? null : Math.round(at - start);
|
|
33
|
-
|
|
34
|
-
function heardNote(reply: Reply | null): string | null {
|
|
35
|
-
if (reply === null || reply.said.trim() === '' || reply.heard >= reply.said.trim().length - 2) return null;
|
|
36
|
-
const part = reply.said.slice(0, reply.heard).trim();
|
|
37
|
-
return part === ''
|
|
38
|
-
? '[The caller interrupted you before hearing your reply.]'
|
|
39
|
-
: `[The caller interrupted you. They heard only: "${part}". The rest of your reply was not heard.]`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class Talk implements Thinking {
|
|
43
|
-
private readonly brain: Brain;
|
|
44
|
-
private readonly scribe: Scribe;
|
|
45
|
-
private readonly opus = new OpusIn();
|
|
46
|
-
private readonly player: Player;
|
|
47
|
-
private spare: Utterance | null = null;
|
|
48
|
-
private reply: Reply | null = null;
|
|
49
|
-
private cut: string | null = null;
|
|
50
|
-
private muted = false;
|
|
51
|
-
private askedAt: number | null = null;
|
|
52
|
-
private saidAt: number | null = null;
|
|
53
|
-
private lastHeardAt: number | null = null;
|
|
54
|
-
private readonly delays: number[] = [];
|
|
55
|
-
private live = false;
|
|
56
|
-
private finished = false;
|
|
57
|
-
private spoke = false;
|
|
58
|
-
|
|
59
|
-
constructor(
|
|
60
|
-
private readonly cfg: VoiceConfig,
|
|
61
|
-
out: Playback['send'],
|
|
62
|
-
private readonly ended: (reason: string) => void,
|
|
63
|
-
private readonly approvals: CallApprovals,
|
|
64
|
-
) {
|
|
65
|
-
const playback: Playback = {
|
|
66
|
-
send: out,
|
|
67
|
-
played: (chars) => {
|
|
68
|
-
this.played(chars);
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
this.player = new Player(playback);
|
|
72
|
-
this.player.whenFirstPlayed(() => {
|
|
73
|
-
this.firstPlayed();
|
|
74
|
-
});
|
|
75
|
-
this.player.whenFailed((reason) => {
|
|
76
|
-
if (!this.finished) this.ended(`audio playback failed: ${reason}`);
|
|
77
|
-
});
|
|
78
|
-
this.brain = new Brain(brainModel(cfg), this);
|
|
79
|
-
this.scribe = new Scribe(cfg.apiKey, languageOf(cfg), {
|
|
80
|
-
partial: (text) => {
|
|
81
|
-
this.partial(text);
|
|
82
|
-
},
|
|
83
|
-
committed: (text) => {
|
|
84
|
-
this.committed(text);
|
|
85
|
-
},
|
|
86
|
-
failed: (reason) => {
|
|
87
|
-
this.ended(reason);
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
this.spare = this.utterance();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
prime(context: string): void {
|
|
94
|
-
if (!this.finished) this.brain.tell(context);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
connect(): void {
|
|
98
|
-
if (this.finished || this.live) return;
|
|
99
|
-
this.live = true;
|
|
100
|
-
this.spare ??= this.utterance();
|
|
101
|
-
this.player.start();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
hear(packet: Buffer): void {
|
|
105
|
-
if (this.finished) return;
|
|
106
|
-
const pcm = this.opus.decode(packet);
|
|
107
|
-
if (pcm !== null) this.scribe.send(downsample3(pcm));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
finish(): void {
|
|
111
|
-
if (this.finished) return;
|
|
112
|
-
this.finished = true;
|
|
113
|
-
this.live = false;
|
|
114
|
-
this.approvals.close();
|
|
115
|
-
this.player.stop();
|
|
116
|
-
this.scribe.close();
|
|
117
|
-
this.reply?.utterance.abort();
|
|
118
|
-
this.spare?.abort();
|
|
119
|
-
this.opus.close();
|
|
120
|
-
this.brain.close();
|
|
121
|
-
this.logReplyTime();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
turnStarted(): void {
|
|
125
|
-
if (this.finished) return;
|
|
126
|
-
this.muted = false;
|
|
127
|
-
this.reply = {
|
|
128
|
-
utterance: this.takeUtterance(),
|
|
129
|
-
said: '',
|
|
130
|
-
heard: 0,
|
|
131
|
-
chunker: new Chunker(),
|
|
132
|
-
askedAt: this.askedAt,
|
|
133
|
-
saidAt: this.saidAt,
|
|
134
|
-
firstText: null,
|
|
135
|
-
firstAudio: null,
|
|
136
|
-
firstPlayed: null,
|
|
137
|
-
};
|
|
138
|
-
this.askedAt = null;
|
|
139
|
-
this.saidAt = null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
text(delta: string): void {
|
|
143
|
-
const reply = this.reply;
|
|
144
|
-
if (this.finished || this.muted || reply === null) return;
|
|
145
|
-
reply.firstText ??= performance.now();
|
|
146
|
-
this.speak(reply, reply.chunker.add(delta));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
tool(name: string): void {
|
|
150
|
-
if (this.finished) return;
|
|
151
|
-
log.info({ tool: name }, 'voice: the agent uses a tool during the call');
|
|
152
|
-
if (this.reply !== null && !this.muted) this.speak(this.reply, this.reply.chunker.flush());
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
turnEnded(): void {
|
|
156
|
-
const reply = this.reply;
|
|
157
|
-
if (this.finished || reply === null || this.muted) return;
|
|
158
|
-
this.speak(reply, reply.chunker.flush());
|
|
159
|
-
reply.utterance.end();
|
|
160
|
-
log.info(
|
|
161
|
-
{ firstText: since(reply.askedAt, reply.firstText), firstAudio: since(reply.askedAt, reply.firstAudio), vadSilenceMs: VAD_SILENCE_SECS * 1000 },
|
|
162
|
-
'voice: reply timing in ms from the caller’s words being committed',
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
asked(ask: ToolAsk): void {
|
|
167
|
-
const id = this.finished ? null : this.approvals.ask(ask, (behavior) => {
|
|
168
|
-
this.brain.answer(ask, behavior);
|
|
169
|
-
});
|
|
170
|
-
if (id === null) {
|
|
171
|
-
this.brain.answer(ask, 'deny');
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
log.info({ tool: ask.tool, worker: ask.fromWorker }, 'voice: a tool waits for the owner’s approval in the chat of the call');
|
|
175
|
-
this.brain.tell(approvalNote(ask.tool, id));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
exited(reason: string): void {
|
|
179
|
-
if (this.finished) return;
|
|
180
|
-
log.warn({ reason }, 'voice: the agent session for the call ended');
|
|
181
|
-
this.ended(`agent session ${reason}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private takeUtterance(): Utterance {
|
|
185
|
-
const ready = this.spare?.usable === true ? this.spare : this.utterance();
|
|
186
|
-
this.spare = this.live ? this.utterance() : null;
|
|
187
|
-
if (ready === this.spare) this.spare = null;
|
|
188
|
-
const reply = (): Reply | null => this.reply;
|
|
189
|
-
ready.attach({
|
|
190
|
-
audio: (pcm, chars) => {
|
|
191
|
-
const current = reply();
|
|
192
|
-
if (this.finished || this.muted || current?.utterance !== ready) return;
|
|
193
|
-
this.spoke = true;
|
|
194
|
-
current.firstAudio ??= performance.now();
|
|
195
|
-
this.player.push(pcm, chars);
|
|
196
|
-
},
|
|
197
|
-
done: () => {
|
|
198
|
-
if (!this.finished && !this.muted && this.reply?.utterance === ready) this.player.finish();
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
return ready;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private utterance(): Utterance {
|
|
205
|
-
return new Utterance(this.cfg.apiKey, voiceIdOf(this.cfg), (reason) => {
|
|
206
|
-
log.warn({ reason }, 'voice: text to speech gave no audio');
|
|
207
|
-
if (!this.spoke && !this.finished) this.ended(`ElevenLabs could not speak (${reason})`);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private speak(reply: Reply, parts: string[]): void {
|
|
212
|
-
for (const part of parts) {
|
|
213
|
-
reply.said += `${part} `;
|
|
214
|
-
reply.utterance.say(part);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private played(chars: number): void {
|
|
219
|
-
if (this.reply !== null) this.reply.heard += chars;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private logReplyTime(): void {
|
|
223
|
-
if (this.delays.length === 0) return;
|
|
224
|
-
const sorted = [...this.delays].sort((a, b) => a - b);
|
|
225
|
-
log.info({ middleMs: Math.round(sorted[Math.floor(sorted.length / 2)] ?? 0), replies: sorted.length }, 'voice: reply time of the call, from the caller’s last word heard to the first reply audio sent');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private firstPlayed(): void {
|
|
229
|
-
const reply = this.reply;
|
|
230
|
-
if (reply?.firstPlayed !== null || reply.saidAt === null) return;
|
|
231
|
-
reply.firstPlayed = performance.now();
|
|
232
|
-
this.delays.push(reply.firstPlayed - reply.saidAt);
|
|
233
|
-
log.info({ fromLastWord: since(reply.saidAt, reply.firstPlayed), fromCommit: since(reply.askedAt, reply.firstPlayed) }, 'voice: first reply audio sent to the caller, in ms');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private interrupts(text: string): boolean {
|
|
237
|
-
return !this.muted && this.player.busy && wordsIn(text) >= BARGE_MIN_WORDS;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private cutOff(): void {
|
|
241
|
-
this.cut = heardNote(this.reply);
|
|
242
|
-
this.reply?.utterance.abort();
|
|
243
|
-
this.player.clear();
|
|
244
|
-
this.muted = true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private partial(text: string): void {
|
|
248
|
-
if (this.finished) return;
|
|
249
|
-
if (text !== '') this.lastHeardAt = performance.now();
|
|
250
|
-
if (this.interrupts(text)) this.cutOff();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private committed(text: string): void {
|
|
254
|
-
if (this.finished) return;
|
|
255
|
-
if (this.interrupts(text)) this.cutOff();
|
|
256
|
-
const note = this.cut;
|
|
257
|
-
const now = this.muted;
|
|
258
|
-
this.cut = null;
|
|
259
|
-
this.askedAt = performance.now();
|
|
260
|
-
this.saidAt = this.lastHeardAt ?? this.askedAt;
|
|
261
|
-
this.lastHeardAt = null;
|
|
262
|
-
this.brain.tell(note === null ? text : `${note}\n${text}`, now);
|
|
263
|
-
}
|
|
264
|
-
}
|