@stage-labs/metro 0.1.0-beta.264 → 0.1.0-beta.265

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.
Files changed (51) hide show
  1. package/dist/cli.js +2 -2
  2. package/package.json +1 -1
  3. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  4. package/runtime/node_modules/@metro-labs/core/package.json +1 -0
  5. package/runtime/node_modules/@metro-labs/core/src/call.ts +81 -0
  6. package/runtime/node_modules/@metro-labs/core/src/runner-activity.ts +37 -1
  7. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +35 -2
  8. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +1 -1
  9. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +14 -11
  10. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +3 -0
  11. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-bridge.ts +45 -0
  12. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-permission.ts +25 -0
  13. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +2 -0
  14. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +9 -2
  15. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -0
  16. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +2 -1
  17. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +15 -4
  18. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +20 -1
  19. package/runtime/node_modules/@metro-labs/daemon/src/mcp/speech-schema.ts +11 -0
  20. package/runtime/node_modules/@metro-labs/daemon/src/mcp/speech-send.ts +15 -0
  21. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +3 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/voice/audio.ts +50 -10
  23. package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +20 -9
  24. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +125 -51
  25. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +41 -15
  26. package/runtime/node_modules/@metro-labs/daemon/src/voice/peer.ts +9 -8
  27. package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +15 -3
  28. package/runtime/node_modules/@metro-labs/daemon/src/voice/shared-talk.ts +85 -0
  29. package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +199 -0
  30. package/runtime/node_modules/@metro-labs/daemon/src/voice/speech-queue.ts +173 -0
  31. package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +40 -15
  32. package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +15 -7
  33. package/runtime/runtime.json +1 -1
  34. package/runtime/sdk-runner/node_modules/@metro-labs/core/package.json +1 -0
  35. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/call.ts +81 -0
  36. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/runner-activity.ts +37 -1
  37. package/runtime/sdk-runner/runner.json +1 -1
  38. package/runtime/sdk-runner/src/activity.ts +14 -2
  39. package/runtime/sdk-runner/src/app.ts +5 -4
  40. package/runtime/sdk-runner/src/approvals.ts +14 -5
  41. package/runtime/sdk-runner/src/calls.ts +133 -0
  42. package/runtime/sdk-runner/src/inbox.ts +58 -26
  43. package/runtime/sdk-runner/src/input-queue.ts +70 -0
  44. package/runtime/sdk-runner/src/input-timing.ts +68 -0
  45. package/runtime/sdk-runner/src/link.ts +27 -4
  46. package/runtime/sdk-runner/src/main.ts +0 -4
  47. package/runtime/sdk-runner/src/rules.ts +19 -5
  48. package/runtime/sdk-runner/src/runner.ts +64 -88
  49. package/runtime/sdk-runner/src/session-store.ts +14 -1
  50. package/runtime/sdk-runner/src/session-watch.ts +27 -20
  51. package/runtime/sdk-runner/src/speech.ts +0 -79
@@ -13,6 +13,7 @@ interface Stored {
13
13
  sessionId: string | null;
14
14
  unanswered: Unanswered[];
15
15
  interrupted: Unanswered[];
16
+ context?: number;
16
17
  }
17
18
 
18
19
  function inputId(raw: unknown): Uuid | undefined {
@@ -35,11 +36,18 @@ function entry(raw: unknown): Unanswered | null {
35
36
 
36
37
  const UNREADABLE = 'The saved Agent SDK state cannot be read. Restore it before starting.';
37
38
 
39
+ function contextOf(raw: unknown): number | undefined {
40
+ if (raw === undefined) return undefined;
41
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0) throw new Error(UNREADABLE);
42
+ return raw;
43
+ }
44
+
38
45
  function stored(raw: unknown): Stored {
39
46
  if (!isRecord(raw)) throw new Error(UNREADABLE);
40
47
  const id = raw.sessionId ?? null;
41
48
  if (id !== null && (typeof id !== 'string' || !SESSION_ID.test(id))) throw new Error(UNREADABLE);
42
- return { sessionId: id, unanswered: entries(raw.unanswered), interrupted: entries(raw.interrupted) };
49
+ const context = contextOf(raw.context);
50
+ return { sessionId: id, unanswered: entries(raw.unanswered), interrupted: entries(raw.interrupted), ...(context === undefined ? {} : { context }) };
43
51
  }
44
52
 
45
53
  function entries(raw: unknown): Unanswered[] {
@@ -86,6 +94,11 @@ export class SessionStore {
86
94
  this.write({ ...this.read(), unanswered });
87
95
  }
88
96
 
97
+ saveContext(context: number): void {
98
+ const stored = this.read();
99
+ if (stored.context !== context) this.write({ ...stored, context });
100
+ }
101
+
89
102
  private read(): Stored {
90
103
  try {
91
104
  return stored(JSON.parse(readFileSync(this.path, 'utf8')));
@@ -1,8 +1,6 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
 
3
3
  const METRO_TOOL = /^mcp__metro__(.+)$/;
4
- const GATE_CAP_MS = 5_000;
5
- const COMPACT_CAP_MS = 20_000;
6
4
 
7
5
  export const uuidsOf = (m: Record<string, unknown>): string[] | null => {
8
6
  if (Array.isArray(m.user_message_uuids)) return m.user_message_uuids.filter((u): u is string => typeof u === 'string');
@@ -24,21 +22,14 @@ function toolResultIds(message: unknown): string[] {
24
22
  return message.content.filter(isRecord).filter((b) => b.type === 'tool_result' && typeof b.tool_use_id === 'string').map((b) => String(b.tool_use_id));
25
23
  }
26
24
 
27
- function capped(waiters: (() => void)[], capMs: number): Promise<void> {
28
- return new Promise((resolve) => {
29
- const timer = setTimeout(resolve, capMs);
30
- waiters.push(() => {
31
- clearTimeout(timer);
32
- resolve();
33
- });
34
- });
35
- }
36
25
 
37
26
  export class SessionWatch {
38
27
  private readonly writes = new Set<string>();
39
28
  private readonly idle: (() => void)[] = [];
40
29
  private readonly compacted: (() => void)[] = [];
41
30
  private compactingNow = false;
31
+ private busy = false;
32
+ private sessionStateSeen = false;
42
33
  private lastContext = 0;
43
34
  private streamUsage: Record<string, unknown> = {};
44
35
 
@@ -56,26 +47,42 @@ export class SessionWatch {
56
47
  return this.writes.size > 0;
57
48
  }
58
49
 
50
+ get safe(): boolean { return !this.busy && !this.writing && !this.compacting; }
51
+
52
+ dispatched(): void { this.busy = true; }
53
+ restoreContext(context: number): void { this.lastContext = context; }
54
+
59
55
  observe(m: Record<string, unknown>): void {
60
- if (m.type === 'system') this.system(m);
61
- else if (m.type === 'result') this.settle();
62
- if (m.parent_tool_use_id !== null) return;
56
+ if (m.parent_tool_use_id !== null && m.parent_tool_use_id !== undefined) return;
57
+ this.lifecycle(m);
63
58
  if (m.type === 'assistant' && isRecord(m.message)) this.assistant(m.message);
64
59
  else if (m.type === 'stream_event' && isRecord(m.event)) this.stream(m.event);
65
60
  else if (m.type === 'user') for (const id of toolResultIds(m.message)) this.done(id);
66
61
  }
67
62
 
68
- whenNotWriting(capMs = GATE_CAP_MS): Promise<void> {
69
- return this.writing ? capped(this.idle, capMs) : Promise.resolve();
63
+ whenNotWriting(): Promise<void> {
64
+ return this.writing ? new Promise((resolve) => { this.idle.push(resolve); }) : Promise.resolve();
70
65
  }
71
66
 
72
- whenNotCompacting(capMs = COMPACT_CAP_MS): Promise<void> {
73
- return this.compactingNow ? capped(this.compacted, capMs) : Promise.resolve();
67
+ whenNotCompacting(): Promise<void> {
68
+ return this.compactingNow ? new Promise((resolve) => { this.compacted.push(resolve); }) : Promise.resolve();
69
+ }
70
+
71
+ private lifecycle(m: Record<string, unknown>): void {
72
+ if (m.type === 'system') this.system(m);
73
+ else if (m.type === 'result' && !this.sessionStateSeen) {
74
+ this.busy = typeof m.queued_turn_count === 'number' && m.queued_turn_count > 0;
75
+ this.settle();
76
+ }
77
+ if (m.type === 'assistant' || m.type === 'stream_event' || startedCommand(m) !== null) this.busy = true;
74
78
  }
75
79
 
76
80
  private system(m: Record<string, unknown>): void {
77
- if (m.subtype === 'init') this.settle();
78
- else if (m.subtype === 'status') this.setCompacting(m.status === 'compacting');
81
+ if (m.subtype === 'session_state_changed') {
82
+ this.sessionStateSeen = true;
83
+ if (m.state === 'running' || m.state === 'requires_action') this.busy = true;
84
+ else if (m.state === 'idle') { this.busy = false; this.settle(); }
85
+ } else if (m.subtype === 'status') this.setCompacting(m.status === 'compacting');
79
86
  else if (m.subtype === 'compact_boundary') {
80
87
  this.setCompacting(false);
81
88
  this.lastContext = 0;
@@ -1,79 +0,0 @@
1
- import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
2
- import { isRecord } from '@metro-labs/core/is-record';
3
- import type { InputKind } from './inbox.js';
4
- import { startedCommand, uuidsOf } from './session-watch.js';
5
-
6
- export interface SpeechSink {
7
- say(text: string): void;
8
- done(): void;
9
- stalled?(): void;
10
- }
11
-
12
-
13
- function textDelta(event: unknown): string | null {
14
- if (!isRecord(event) || event.type !== 'content_block_delta' || !isRecord(event.delta)) return null;
15
- return event.delta.type === 'text_delta' && typeof event.delta.text === 'string' ? event.delta.text : null;
16
- }
17
-
18
- export class SpeechRouter {
19
- private answering: string[] = [];
20
- private speaking = false;
21
- private live = false;
22
-
23
- constructor(
24
- private readonly kindOf: (uuid: string) => InputKind | undefined,
25
- private readonly sink: SpeechSink,
26
- ) {}
27
-
28
- get callLive(): boolean {
29
- return this.live;
30
- }
31
-
32
- get talking(): boolean {
33
- return this.speaking;
34
- }
35
-
36
- setCallLive(on: boolean): void {
37
- this.live = on;
38
- if (!on) this.endTurn();
39
- }
40
-
41
- observe(message: SDKMessage): void {
42
- const m: Record<string, unknown> = { ...message };
43
- if (m.type === 'result') {
44
- this.endTurn();
45
- this.answering = [];
46
- return;
47
- }
48
- if (m.type === 'system' && m.subtype === 'init') this.endTurn();
49
- const started = startedCommand(m);
50
- if (started !== null) this.answer([started]);
51
- if (m.parent_tool_use_id === null) this.mainThread(m);
52
- }
53
-
54
- private answer(uuids: readonly string[]): void {
55
- this.answering = [...new Set([...this.answering, ...uuids])];
56
- }
57
-
58
- private mainThread(m: Record<string, unknown>): void {
59
- if (m.type !== 'stream_event' && m.type !== 'assistant') return;
60
- const uuids = uuidsOf(m);
61
- if (uuids !== null) this.answer(uuids);
62
- const text = m.type === 'stream_event' ? textDelta(m.event) : null;
63
- if (text === null || text === '' || !this.speaks()) return;
64
- this.speaking = true;
65
- this.sink.say(text);
66
- }
67
-
68
- private speaks(): boolean {
69
- if (!this.live) return false;
70
- if (this.answering.length === 0) return true;
71
- return this.answering.some((uuid) => this.kindOf(uuid) === 'call');
72
- }
73
-
74
- private endTurn(): void {
75
- if (!this.speaking) return;
76
- this.speaking = false;
77
- this.sink.done();
78
- }
79
- }