agent-relay-sdk 0.2.16 → 0.2.18

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 (60) hide show
  1. package/dist/bus-client.d.ts +5 -4
  2. package/dist/bus-client.d.ts.map +1 -1
  3. package/dist/claim-tracker.d.ts +1 -18
  4. package/dist/claim-tracker.d.ts.map +1 -1
  5. package/dist/claim-tracker.js +2 -1
  6. package/dist/claim-tracker.js.map +1 -1
  7. package/dist/context-probe.d.ts +4 -6
  8. package/dist/context-probe.d.ts.map +1 -1
  9. package/dist/context-probe.js +11 -5
  10. package/dist/context-probe.js.map +1 -1
  11. package/dist/fs-name.d.ts +2 -1
  12. package/dist/fs-name.d.ts.map +1 -1
  13. package/dist/fs-name.js +7 -4
  14. package/dist/fs-name.js.map +1 -1
  15. package/dist/http-client.d.ts +3 -10
  16. package/dist/http-client.d.ts.map +1 -1
  17. package/dist/http-client.js +1 -1
  18. package/dist/http-client.js.map +1 -1
  19. package/dist/process-utils.d.ts +0 -2
  20. package/dist/process-utils.d.ts.map +1 -1
  21. package/dist/process-utils.js +1 -1
  22. package/dist/process-utils.js.map +1 -1
  23. package/dist/protocol.d.ts +11 -17
  24. package/dist/protocol.d.ts.map +1 -1
  25. package/dist/protocol.js +7 -7
  26. package/dist/protocol.js.map +1 -1
  27. package/dist/provider-base.d.ts +3 -26
  28. package/dist/provider-base.d.ts.map +1 -1
  29. package/dist/provider-base.js +1 -1
  30. package/dist/provider-base.js.map +1 -1
  31. package/dist/provider-catalog.d.ts +10 -10
  32. package/dist/provider-catalog.d.ts.map +1 -1
  33. package/dist/provider-catalog.js +1 -1
  34. package/dist/provider-catalog.js.map +1 -1
  35. package/dist/reconnect.d.ts +2 -1
  36. package/dist/reconnect.d.ts.map +1 -1
  37. package/dist/speech-text.d.ts +10 -2
  38. package/dist/speech-text.d.ts.map +1 -1
  39. package/dist/speech-text.js +65 -1
  40. package/dist/speech-text.js.map +1 -1
  41. package/dist/sse.d.ts +2 -1
  42. package/dist/sse.d.ts.map +1 -1
  43. package/dist/types.d.ts +35 -46
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +24 -3
  46. package/dist/types.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/bus-client.ts +4 -4
  49. package/src/claim-tracker.ts +4 -4
  50. package/src/context-probe.ts +14 -8
  51. package/src/fs-name.ts +6 -3
  52. package/src/http-client.ts +3 -3
  53. package/src/process-utils.ts +1 -1
  54. package/src/protocol.ts +17 -17
  55. package/src/provider-base.ts +4 -4
  56. package/src/provider-catalog.ts +10 -10
  57. package/src/reconnect.ts +1 -1
  58. package/src/speech-text.ts +69 -2
  59. package/src/sse.ts +1 -1
  60. package/src/types.ts +54 -20
@@ -6,7 +6,7 @@ import type { ContextProbeMetrics } from "./types.js";
6
6
  import { sanitizeFsName } from "./fs-name.js";
7
7
  import { isRecord, stringValue } from "./types.js";
8
8
 
9
- export interface ContextProbeOptions {
9
+ interface ContextProbeOptions {
10
10
  wrapCommand?: string;
11
11
  agentId?: string;
12
12
  stateDir?: string;
@@ -16,13 +16,13 @@ export interface ContextProbeOptions {
16
16
  parsePatterns?: ParsePattern[];
17
17
  }
18
18
 
19
- export interface ParsePattern {
19
+ interface ParsePattern {
20
20
  name: string;
21
21
  regex: RegExp;
22
22
  extract(match: RegExpMatchArray): Partial<ContextProbeMetrics>;
23
23
  }
24
24
 
25
- export interface ContextProbeRunResult {
25
+ interface ContextProbeRunResult {
26
26
  metrics: ContextProbeMetrics | null;
27
27
  stateFile?: string;
28
28
  wrappedExitCode?: number;
@@ -31,9 +31,15 @@ export interface ContextProbeRunResult {
31
31
  output: string;
32
32
  }
33
33
 
34
- export const DEFAULT_CONTEXT_PROBE_STATE_DIR = tmpdir();
34
+ // Lazy on purpose: calling tmpdir() at module top level made the whole SDK
35
+ // barrel browser-hostile — a bare-barrel import crashed the dashboard because
36
+ // node:os.tmpdir is undefined in the browser (#281). A function defers the
37
+ // node-only call to runtime, so importing this module never executes it.
38
+ function defaultContextProbeStateDir(): string {
39
+ return tmpdir();
40
+ }
35
41
 
36
- export const DEFAULT_CONTEXT_PROBE_PATTERNS: ParsePattern[] = [
42
+ const DEFAULT_CONTEXT_PROBE_PATTERNS: ParsePattern[] = [
37
43
  {
38
44
  name: "percent",
39
45
  regex: /(\d+(?:\.\d+)?)%\s*(?:context|ctx)?/i,
@@ -54,7 +60,7 @@ export const DEFAULT_CONTEXT_PROBE_PATTERNS: ParsePattern[] = [
54
60
  },
55
61
  ];
56
62
 
57
- export function contextProbeStatePath(agentId: string, stateDir = DEFAULT_CONTEXT_PROBE_STATE_DIR): string {
63
+ export function contextProbeStatePath(agentId: string, stateDir = defaultContextProbeStateDir()): string {
58
64
  return join(stateDir, `agent-relay-context-${safeStateId(agentId)}.json`);
59
65
  }
60
66
 
@@ -104,7 +110,7 @@ export function contextStateFromProbeMetrics(metrics: ContextProbeMetrics) {
104
110
  };
105
111
  }
106
112
 
107
- export function writeContextProbeState(metrics: ContextProbeMetrics, stateDir = DEFAULT_CONTEXT_PROBE_STATE_DIR): string {
113
+ function writeContextProbeState(metrics: ContextProbeMetrics, stateDir = defaultContextProbeStateDir()): string {
108
114
  const file = contextProbeStatePath(metrics.agentId, stateDir);
109
115
  mkdirSync(dirname(file), { recursive: true });
110
116
  const tmp = `${file}.${process.pid}.tmp`;
@@ -113,7 +119,7 @@ export function writeContextProbeState(metrics: ContextProbeMetrics, stateDir =
113
119
  return file;
114
120
  }
115
121
 
116
- export function readContextProbeState(agentId: string, stateDir = DEFAULT_CONTEXT_PROBE_STATE_DIR): ContextProbeMetrics | null {
122
+ export function readContextProbeState(agentId: string, stateDir = defaultContextProbeStateDir()): ContextProbeMetrics | null {
117
123
  try {
118
124
  const parsed = JSON.parse(readFileSync(contextProbeStatePath(agentId, stateDir), "utf8")) as unknown;
119
125
  return isContextProbeMetrics(parsed) ? parsed : null;
package/src/fs-name.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Allowed characters (everything else is replaced): a-z A-Z 0-9 . _ -
9
9
 
10
- export interface SanitizeFsNameOptions {
10
+ interface SanitizeFsNameOptions {
11
11
  /** Truncate the result to this many chars. Omit for no truncation. */
12
12
  maxLen?: number;
13
13
  /** Character substituted for disallowed input. Default "_". */
@@ -37,11 +37,14 @@ export function sanitizeFsName(value: string, opts: SanitizeFsNameOptions = {}):
37
37
  if (opts.collapseReplacement) {
38
38
  s = s.replace(new RegExp(`${escapeRe(repl)}+`, "g"), repl);
39
39
  }
40
+ if (opts.lowercase) s = s.toLowerCase();
41
+ if (opts.maxLen !== undefined) s = s.slice(0, opts.maxLen);
42
+ // Edge-trim AFTER truncating: a maxLen cut that lands on a replacement char
43
+ // (e.g. a UUID sliced mid-segment) would otherwise leave a dangling separator
44
+ // — the "branch ref severed at `…b7e1-`" bug (#282).
40
45
  if (opts.trimEdge) {
41
46
  const e = escapeRe(repl);
42
47
  s = s.replace(new RegExp(`^(?:${e})+|(?:${e})+$`, "g"), "");
43
48
  }
44
- if (opts.lowercase) s = s.toLowerCase();
45
- if (opts.maxLen !== undefined) s = s.slice(0, opts.maxLen);
46
49
  return s || opts.fallback || "";
47
50
  }
@@ -1,20 +1,20 @@
1
1
  import type { AgentCard, Artifact, ArtifactKind, ArtifactSensitivity, PollQuery, RegisterAgentInput, SendMessageInput, Message, MessageDeliveryState, MessageDeliveryStatus, ReplyObligation, Task, TaskStatusInput } from "./types.js";
2
2
  import { RELAY_TOKEN_HEADER } from "./contracts.js";
3
3
 
4
- export interface HttpClientOptions {
4
+ interface HttpClientOptions {
5
5
  baseUrl: string;
6
6
  token?: string;
7
7
  timeout?: number;
8
8
  }
9
9
 
10
- export interface TransportOptions {
10
+ interface TransportOptions {
11
11
  /** External abort signal, combined (AbortSignal.any) with the internal timeout. */
12
12
  signal?: AbortSignal;
13
13
  /** Override the default request timeout (ms). `null` disables it — for streaming (SSE). */
14
14
  timeoutMs?: number | null;
15
15
  }
16
16
 
17
- export class RelayHttpError extends Error {
17
+ class RelayHttpError extends Error {
18
18
  constructor(
19
19
  readonly method: string,
20
20
  readonly path: string,
@@ -15,7 +15,7 @@ export function parseProcStateIsZombie(statusText: string): boolean {
15
15
  }
16
16
 
17
17
  /** True iff <pid> exists and its /proc state is zombie. Non-Linux → false. */
18
- export function isZombie(pid: number): boolean {
18
+ function isZombie(pid: number): boolean {
19
19
  try {
20
20
  return parseProcStateIsZombie(readFileSync(`/proc/${pid}/status`, "utf8"));
21
21
  } catch {
package/src/protocol.ts CHANGED
@@ -24,7 +24,7 @@ export interface RegisterFrame extends BusFrame {
24
24
  };
25
25
  }
26
26
 
27
- export interface HeartbeatFrame extends BusFrame {
27
+ interface HeartbeatFrame extends BusFrame {
28
28
  type: "heartbeat";
29
29
  payload: {
30
30
  status: Exclude<BusAgentStatus, "offline">;
@@ -32,7 +32,7 @@ export interface HeartbeatFrame extends BusFrame {
32
32
  };
33
33
  }
34
34
 
35
- export interface CommandFrame extends BusFrame {
35
+ interface CommandFrame extends BusFrame {
36
36
  type: "command";
37
37
  id: string;
38
38
  payload: {
@@ -42,7 +42,7 @@ export interface CommandFrame extends BusFrame {
42
42
  };
43
43
  }
44
44
 
45
- export interface SubscribeFrame extends BusFrame {
45
+ interface SubscribeFrame extends BusFrame {
46
46
  type: "subscribe";
47
47
  id: string;
48
48
  payload: {
@@ -51,7 +51,7 @@ export interface SubscribeFrame extends BusFrame {
51
51
  };
52
52
  }
53
53
 
54
- export interface StatusFrame extends BusFrame {
54
+ interface StatusFrame extends BusFrame {
55
55
  type: "status";
56
56
  payload: {
57
57
  agentStatus: BusAgentStatus;
@@ -60,14 +60,14 @@ export interface StatusFrame extends BusFrame {
60
60
  };
61
61
  }
62
62
 
63
- export interface AckFrame extends BusFrame {
63
+ interface AckFrame extends BusFrame {
64
64
  type: "ack";
65
65
  payload: {
66
66
  frameId: string;
67
67
  };
68
68
  }
69
69
 
70
- export interface ResumeFrame extends BusFrame {
70
+ interface ResumeFrame extends BusFrame {
71
71
  type: "resume";
72
72
  id: string;
73
73
  payload: {
@@ -87,7 +87,7 @@ export interface RegisteredFrame extends BusFrame {
87
87
  };
88
88
  }
89
89
 
90
- export interface EventPayload {
90
+ interface EventPayload {
91
91
  seq: number;
92
92
  eventType: string;
93
93
  source: string;
@@ -102,7 +102,7 @@ export interface EventFrame extends BusFrame {
102
102
  payload: EventPayload;
103
103
  }
104
104
 
105
- export interface CommandResultFrame extends BusFrame {
105
+ interface CommandResultFrame extends BusFrame {
106
106
  type: "command.result";
107
107
  payload: {
108
108
  commandId: string;
@@ -118,7 +118,7 @@ export interface CommandStatusUpdateParams {
118
118
  error?: string;
119
119
  }
120
120
 
121
- export interface ResumedFrame extends BusFrame {
121
+ interface ResumedFrame extends BusFrame {
122
122
  type: "resumed";
123
123
  payload: {
124
124
  events: EventPayload[];
@@ -136,7 +136,7 @@ export interface ErrorFrame extends BusFrame {
136
136
  };
137
137
  }
138
138
 
139
- export type ClientBusFrame =
139
+ type ClientBusFrame =
140
140
  | RegisterFrame
141
141
  | HeartbeatFrame
142
142
  | CommandFrame
@@ -162,13 +162,13 @@ export class BusProtocolError extends Error {
162
162
  }
163
163
  }
164
164
 
165
- export function isRegisterFrame(f: BusFrame): f is RegisterFrame { return f.type === "register"; }
166
- export function isHeartbeatFrame(f: BusFrame): f is HeartbeatFrame { return f.type === "heartbeat"; }
167
- export function isCommandFrame(f: BusFrame): f is CommandFrame { return f.type === "command"; }
168
- export function isSubscribeFrame(f: BusFrame): f is SubscribeFrame { return f.type === "subscribe"; }
169
- export function isStatusFrame(f: BusFrame): f is StatusFrame { return f.type === "status"; }
170
- export function isAckFrame(f: BusFrame): f is AckFrame { return f.type === "ack"; }
171
- export function isResumeFrame(f: BusFrame): f is ResumeFrame { return f.type === "resume"; }
165
+ function isRegisterFrame(f: BusFrame): f is RegisterFrame { return f.type === "register"; }
166
+ function isHeartbeatFrame(f: BusFrame): f is HeartbeatFrame { return f.type === "heartbeat"; }
167
+ function isCommandFrame(f: BusFrame): f is CommandFrame { return f.type === "command"; }
168
+ function isSubscribeFrame(f: BusFrame): f is SubscribeFrame { return f.type === "subscribe"; }
169
+ function isStatusFrame(f: BusFrame): f is StatusFrame { return f.type === "status"; }
170
+ function isAckFrame(f: BusFrame): f is AckFrame { return f.type === "ack"; }
171
+ function isResumeFrame(f: BusFrame): f is ResumeFrame { return f.type === "resume"; }
172
172
 
173
173
  export function parseBusFrame(data: string | ArrayBuffer | Uint8Array): BusFrame {
174
174
  const text = typeof data === "string" ? data : new TextDecoder().decode(data);
@@ -11,7 +11,7 @@ export interface ProviderAdapter {
11
11
  formatDelivery?(messages: Message[]): string;
12
12
  }
13
13
 
14
- export interface SpawnConfig {
14
+ interface SpawnConfig {
15
15
  cwd: string;
16
16
  label?: string;
17
17
  approvalMode?: string;
@@ -19,13 +19,13 @@ export interface SpawnConfig {
19
19
  env?: Record<string, string>;
20
20
  }
21
21
 
22
- export interface SpawnResult {
22
+ interface SpawnResult {
23
23
  agentId: string;
24
24
  pid?: number;
25
25
  sessionId?: string;
26
26
  }
27
27
 
28
- export interface ProviderBaseOptions {
28
+ interface ProviderBaseOptions {
29
29
  relayUrl: string;
30
30
  adapter: ProviderAdapter;
31
31
  agentId: string;
@@ -36,7 +36,7 @@ export interface ProviderBaseOptions {
36
36
  token?: string;
37
37
  }
38
38
 
39
- export class ProviderBase {
39
+ class ProviderBase {
40
40
  readonly client: RelayBusClient;
41
41
  private running = false;
42
42
  private readonly pending = new Map<number, Message>();
@@ -3,25 +3,25 @@ import type { SpawnProvider } from "./types.js";
3
3
  /** Valid reasoning-effort levels — runtime tuple is the single source of truth. */
4
4
  export const VALID_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
5
5
  export type ProviderEffort = (typeof VALID_EFFORTS)[number];
6
- export function isProviderEffort(value: unknown): value is ProviderEffort {
6
+ function isProviderEffort(value: unknown): value is ProviderEffort {
7
7
  return typeof value === "string" && (VALID_EFFORTS as readonly string[]).includes(value);
8
8
  }
9
- export type ProviderCatalogValueSource = "catalog" | "provider" | "runtime" | "override";
10
- export type ProviderCatalogConfidence = "declared" | "verified" | "estimated" | "unknown";
9
+ type ProviderCatalogValueSource = "catalog" | "provider" | "runtime" | "override";
10
+ type ProviderCatalogConfidence = "declared" | "verified" | "estimated" | "unknown";
11
11
 
12
- export interface ProviderCatalogValue<T> {
12
+ interface ProviderCatalogValue<T> {
13
13
  value: T;
14
14
  source: ProviderCatalogValueSource;
15
15
  confidence: ProviderCatalogConfidence;
16
16
  lastUpdatedAt?: number;
17
17
  }
18
18
 
19
- export interface ProviderModelLimits {
19
+ interface ProviderModelLimits {
20
20
  contextWindowTokens?: ProviderCatalogValue<number>;
21
21
  maxOutputTokens?: ProviderCatalogValue<number>;
22
22
  }
23
23
 
24
- export interface ProviderModelModalities {
24
+ interface ProviderModelModalities {
25
25
  input: {
26
26
  text: boolean;
27
27
  image?: boolean;
@@ -37,7 +37,7 @@ export interface ProviderModelModalities {
37
37
  };
38
38
  }
39
39
 
40
- export interface ProviderModelToolCapabilities {
40
+ interface ProviderModelToolCapabilities {
41
41
  code?: boolean;
42
42
  review?: boolean;
43
43
  debug?: boolean;
@@ -50,7 +50,7 @@ export interface ProviderModelToolCapabilities {
50
50
  imageEditing?: boolean;
51
51
  }
52
52
 
53
- export interface ProviderModelCapabilities {
53
+ interface ProviderModelCapabilities {
54
54
  modalities: ProviderModelModalities;
55
55
  tools?: ProviderModelToolCapabilities;
56
56
  source: ProviderCatalogValueSource;
@@ -75,13 +75,13 @@ export interface ProviderCatalogEntry {
75
75
  models: ProviderModelCatalogEntry[];
76
76
  }
77
77
 
78
- export interface ProviderSelection {
78
+ interface ProviderSelection {
79
79
  provider: SpawnProvider;
80
80
  model?: string;
81
81
  effort?: ProviderEffort;
82
82
  }
83
83
 
84
- export interface ResolvedProviderSelection {
84
+ interface ResolvedProviderSelection {
85
85
  provider: SpawnProvider;
86
86
  modelAlias?: string;
87
87
  providerModel?: string;
package/src/reconnect.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
 
3
- export interface ReconnectOptions {
3
+ interface ReconnectOptions {
4
4
  initialMs: number;
5
5
  maxMs: number;
6
6
  jitterMs: number;
@@ -111,7 +111,7 @@ function collapseTables(text: string): string {
111
111
  * by appending an entry. Order matters: earlier rules see the raw text, later
112
112
  * rules see the output of earlier ones (see the ordering notes in SPEECH_RULES).
113
113
  */
114
- export interface SpeechRule {
114
+ interface SpeechRule {
115
115
  readonly name: string
116
116
  readonly pattern: RegExp
117
117
  readonly replace: string | ((substring: string, ...args: string[]) => string)
@@ -131,13 +131,33 @@ function spaceDigits(frac: string): string {
131
131
  return frac.split('').join(' ')
132
132
  }
133
133
 
134
+ /**
135
+ * Spell a token one character at a time so the engine reads each symbol on its
136
+ * own: digits stay as digits, letters are upper-cased so they're heard as the
137
+ * letter name ("e" → "E" = "ee") rather than a syllable. Used for commit hashes
138
+ * ("834543e" → "8 3 4 5 4 3 E") and acronyms ("id" → "I D").
139
+ */
140
+ function spellOut(token: string): string {
141
+ return token
142
+ .split('')
143
+ .map((c) => (/[0-9]/.test(c) ? c : c.toUpperCase()))
144
+ .join(' ')
145
+ }
146
+
134
147
  /**
135
148
  * Ordered normalization rules. ORDER IS LOAD-BEARING:
136
149
  * - URLs/paths first, before anything mangles their slashes/dots.
137
150
  * - `~` → "approximately" before number rules consume the digits after it.
138
151
  * - number ranges ("5-7") before unit expansion, so the unit attaches once: "5 to 7 seconds".
152
+ * - versions ("0.27.0") before decimals, so all dots become "dot" uniformly
153
+ * instead of the decimal rule eating only the first pair and stranding ".0".
139
154
  * - decimals before unit expansion and before the sentence splitter sees the dot.
140
- * - unit expansion last among the number rules.
155
+ * - commit-hash spelling before nothing in particular, but after decimals so a
156
+ * real number is never mistaken for a hash.
157
+ * - "id" acronym expansion (camelCase suffix before the standalone word).
158
+ * - unit expansion among the number rules.
159
+ * - interior-dot LAST, so it only mops up dots the structured rules left behind
160
+ * (identifiers like "branch.landed", "router.ts") — never a decimal or version.
141
161
  */
142
162
  export const SPEECH_RULES: readonly SpeechRule[] = [
143
163
  // Bare URLs read character-by-character → say "link".
@@ -148,14 +168,46 @@ export const SPEECH_RULES: readonly SpeechRule[] = [
148
168
  pattern: /(?:\/[A-Za-z0-9._-]+){2,}\/?/g,
149
169
  replace: (m) => ' ' + m.replace(/[/._-]+/g, ' ').trim() + ' ',
150
170
  },
171
+ // A lone slash between words ("src/router.ts", "TCP/IP") → a space, so the
172
+ // engine doesn't read "slash". Absolute multi-segment paths are already gone.
173
+ { name: 'slash', pattern: /(?<=[A-Za-z0-9])\/(?=[A-Za-z0-9])/g, replace: ' ' },
151
174
  // Empty-paren code identifiers ("fetchSpeech()") → drop the parens.
152
175
  { name: 'func-call', pattern: /\b([A-Za-z_$][\w$]*)\(\)/g, replace: '$1' },
153
176
  // Approximation tilde ("~0.1", "~5") → "approximately".
154
177
  { name: 'approx', pattern: /~(?=\s*[\d.])/g, replace: 'approximately ' },
155
178
  // Numeric ranges ("5-7", "100–200") → "X to Y".
156
179
  { name: 'range', pattern: /(\d)\s*[-–—]\s*(?=\d)/g, replace: '$1 to ' },
180
+ // Dotted versions ("0.27.0", "v1.2.3", IPs) → every dot spoken as "dot".
181
+ // Runs BEFORE decimal so 3+-part versions don't get half-eaten into "point".
182
+ // A leading v/V is voiced as "version" (unambiguous here — v then digits then ≥2 dots).
183
+ {
184
+ name: 'version',
185
+ pattern: /\b(v?)(\d+(?:\.\d+){2,})\b/gi,
186
+ replace: (_m, v: string, nums: string) =>
187
+ `${v ? 'version ' : ''}${nums.replace(/\./g, ' dot ')}`,
188
+ },
157
189
  // Decimals ("0.1", "3.14") → "0 point 1" so the sentence splitter keeps them whole.
158
190
  { name: 'decimal', pattern: /\b(\d+)\.(\d+)\b/g, replace: (_m, i: string, f: string) => `${i} point ${spaceDigits(f)}` },
191
+ // Commit hashes ("834543e", "3e0c66f85bd5") → spell each char so the engine
192
+ // doesn't read them as one giant number. Heuristic: 7–40 hex chars containing
193
+ // BOTH a letter and a digit — excludes plain numbers and plain words.
194
+ {
195
+ name: 'hash',
196
+ pattern: /\b(?=[0-9a-f]{7,40}\b)(?=[0-9a-f]*[a-f])(?=[0-9a-f]*\d)[0-9a-f]{7,40}\b/gi,
197
+ replace: (m: string) => spellOut(m),
198
+ },
199
+ // camelCase "Id" suffix ("messageId", "threadIds") → "message I D", "thread I Ds".
200
+ {
201
+ name: 'id-camel',
202
+ pattern: /([a-z])(Id|ID)(s)?\b/g,
203
+ replace: (_m, pre: string, _acr: string, plural: string) => `${pre} I D${plural ? 's' : ''}`,
204
+ },
205
+ // Standalone "id"/"Id"/"ID" word → "I D" (so it's not read as "iiid").
206
+ {
207
+ name: 'id-word',
208
+ pattern: /\b(?:id|Id|ID)(s)?\b/g,
209
+ replace: (_m, plural: string) => `I D${plural ? 's' : ''}`,
210
+ },
159
211
  // Byte units ("165KB", "2 MB") → spelled out (case-insensitive).
160
212
  {
161
213
  name: 'byte-unit',
@@ -165,6 +217,21 @@ export const SPEECH_RULES: readonly SpeechRule[] = [
165
217
  // Time units ("500ms", "5s") → spelled out. Guard against 4-digit years for "s".
166
218
  { name: 'ms-unit', pattern: /\b(\d{1,5})\s?ms\b/g, replace: (_m, n: string) => `${n} ${UNIT_WORDS.ms}` },
167
219
  { name: 's-unit', pattern: /\b(\d{1,3})\s?s\b/g, replace: (_m, n: string) => `${n} ${UNIT_WORDS.s}` },
220
+ // Heteronym "live": the engine defaults to the verb /lɪv/ ("I want to live"),
221
+ // but in deployment chatter it's the adjective /laɪv/ ("the version is live").
222
+ // Respell → "lyve" ONLY after a high-confidence adjective cue, so the verb is
223
+ // never touched. NOTE: heteronym sense can't be fully disambiguated by regex —
224
+ // this deliberately covers only the predicate/"go live" phrasings agents use.
225
+ {
226
+ name: 'live-adjective',
227
+ pattern:
228
+ /\b(is|are|was|were|be|been|being|now|and|stays?|staying|went|go|goes|going|deployed|it's|that's|here's|there's|we're|you're|they're)\s+live\b/gi,
229
+ replace: (_m, cue: string) => `${cue} lyve`,
230
+ },
231
+ // Interior dot — any dot glued directly to a following alphanumeric ("branch.landed",
232
+ // "router.ts", "Node.js") → " dot ". Runs LAST: real sentence-ending dots (followed by
233
+ // a space or end-of-string) are untouched, and versions/decimals were already consumed.
234
+ { name: 'interior-dot', pattern: /\.(?=[A-Za-z0-9])/g, replace: ' dot ' },
168
235
  ]
169
236
 
170
237
  /**
package/src/sse.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // (voice connector). One home for the `event:`/`data:`/`retry:` field parse
4
4
  // that was hand-rolled in connectors/voice and dashboard/src/lib/api.ts.
5
5
 
6
- export interface ParsedSseFrame {
6
+ interface ParsedSseFrame {
7
7
  /** Event name; defaults to "message" per the SSE spec when no `event:` field. */
8
8
  event: string;
9
9
  /** Each `data:` line, in order. Join with "\n" to reconstruct the payload. */
package/src/types.ts CHANGED
@@ -26,8 +26,8 @@ export interface AgentCard {
26
26
 
27
27
  export type AgentStatus = "online" | "idle" | "busy" | "stale" | "offline";
28
28
 
29
- export type CapabilitySource = "catalog" | "provider" | "runtime" | "override" | "estimate";
30
- export type CapabilityConfidence = "declared" | "reported" | "verified" | "estimated" | "unknown";
29
+ type CapabilitySource = "catalog" | "provider" | "runtime" | "override" | "estimate";
30
+ type CapabilityConfidence = "declared" | "reported" | "verified" | "estimated" | "unknown";
31
31
 
32
32
  export interface ProviderCapabilities {
33
33
  lifecycle: {
@@ -99,7 +99,7 @@ export interface ProviderCapabilities {
99
99
  lastUpdatedAt: number;
100
100
  }
101
101
 
102
- export type ContextLifecycleState =
102
+ type ContextLifecycleState =
103
103
  | "fresh"
104
104
  | "primed"
105
105
  | "working"
@@ -249,7 +249,7 @@ export interface ContextBudget {
249
249
  priorityCutoff: 1 | 2 | 3;
250
250
  }
251
251
 
252
- export interface TaskHistorySummary {
252
+ interface TaskHistorySummary {
253
253
  taskId: number;
254
254
  agentId: string;
255
255
  status: string;
@@ -303,7 +303,7 @@ export interface MemoryBrokerContext {
303
303
 
304
304
  export type MemoryBrokerKind = "sqlite" | "http" | "command";
305
305
 
306
- export interface SqliteMemoryBrokerConfig {
306
+ interface SqliteMemoryBrokerConfig {
307
307
  type: "sqlite";
308
308
  }
309
309
 
@@ -332,17 +332,44 @@ export type MessageKind =
332
332
  | "system"
333
333
  | "session";
334
334
 
335
+ /**
336
+ * Mechanical message kinds: the relay's own lifecycle/observability lane, not
337
+ * agent-directed messaging. They bypass delivery resolution (never re-delivered
338
+ * into a session) and — when targeting a reserved sink — the recipient-constraint
339
+ * auth check (a managed token's `targets`/`policies`/`agents` constraints gate which
340
+ * *agents* it may message, not a session-mirror capture to the reserved sink; #284).
341
+ * Keep in sync with the `MessageKind` union.
342
+ */
343
+ export const MECHANICAL_MESSAGE_KINDS: readonly MessageKind[] = ["system", "control", "session"];
344
+
345
+ export function isMechanicalMessageKind(kind: string | undefined): boolean {
346
+ return MECHANICAL_MESSAGE_KINDS.includes((kind ?? "") as MessageKind);
347
+ }
348
+
349
+ /**
350
+ * Reserved built-in identities that are not real agents: the `user` chat/mirror sink
351
+ * and the `system` lifecycle sender. They are never registered, polled, or delivered to
352
+ * like agents, so recipient constraints don't apply to mechanical posts addressed to them.
353
+ */
354
+ export const RESERVED_AGENT_IDS: readonly string[] = ["user", "system"];
355
+
356
+ export function isReservedAgentId(id: string | undefined): boolean {
357
+ return id === "user" || id === "system";
358
+ }
359
+
335
360
  /**
336
361
  * Session-mirror event taxonomy. Every `kind: "session"` message carries a
337
362
  * `payload.session` of this shape so the dashboard can render the live provider
338
363
  * session faithfully regardless of which surface (chat box or web terminal)
339
- * started the turn. `prompt`/`response` render as chat bubbles; `reasoning`,
340
- * `tool`, and `notice` render discreetly (collapsed activity, never bubbles).
364
+ * started the turn. `prompt`/`response` render as chat bubbles; `narration` is
365
+ * the agent's intermediate spoken text between tool calls (the terminal's `●`
366
+ * lines) and renders inline in the turn's activity trace; `reasoning`, `tool`,
367
+ * and `notice` render discreetly (collapsed/inline activity, never bubbles).
341
368
  * A legacy session message with no `payload.session` is treated as a `response`.
342
369
  */
343
- export type SessionEventType = "prompt" | "response" | "reasoning" | "tool" | "notice";
370
+ type SessionEventType = "prompt" | "response" | "narration" | "reasoning" | "tool" | "notice";
344
371
 
345
- export type SessionEventOrigin = "chat" | "terminal" | "provider";
372
+ type SessionEventOrigin = "chat" | "terminal" | "provider";
346
373
 
347
374
  export interface MessageSessionMeta {
348
375
  type: SessionEventType;
@@ -435,6 +462,10 @@ export interface Message {
435
462
  body: string;
436
463
  threadId?: number;
437
464
  replyTo?: number;
465
+ // Server-owned reply obligation (#283). Absent/true = the message wants a response (default);
466
+ // false = a notification (FYI, merge notice, lifecycle event) that must NOT be replied to.
467
+ // The server keys footer rendering and the reply-obligation tracker off this, not off `from`.
468
+ replyExpected?: boolean;
438
469
  claimable?: boolean;
439
470
  claimedBy?: string;
440
471
  claimedAt?: number;
@@ -538,6 +569,9 @@ export interface SendMessageInput {
538
569
  subject?: string;
539
570
  body: string;
540
571
  replyTo?: number;
572
+ // Defaults to true server-side. Set false to mark a notification (no reply wanted) — the
573
+ // server suppresses the reply-scaffold footer and appends a one-line no-reply nudge (#283).
574
+ replyExpected?: boolean;
541
575
  claimable?: boolean;
542
576
  idempotencyKey?: string;
543
577
  maxAgeSeconds?: number;
@@ -1141,14 +1175,14 @@ export type OrchestratorStatus = "online" | "offline";
1141
1175
  /** Spawn providers — the runtime tuple is the single source of truth; the type derives from it. */
1142
1176
  export const SPAWN_PROVIDERS = ["claude", "codex"] as const;
1143
1177
  export type SpawnProvider = (typeof SPAWN_PROVIDERS)[number];
1144
- export function isSpawnProvider(value: unknown): value is SpawnProvider {
1178
+ function isSpawnProvider(value: unknown): value is SpawnProvider {
1145
1179
  return typeof value === "string" && (SPAWN_PROVIDERS as readonly string[]).includes(value);
1146
1180
  }
1147
1181
 
1148
1182
  /** Approval modes — runtime tuple + derived type. */
1149
1183
  export const APPROVAL_MODES = ["open", "guarded", "read-only"] as const;
1150
1184
  export type SpawnApprovalMode = (typeof APPROVAL_MODES)[number];
1151
- export function isApprovalMode(value: unknown): value is SpawnApprovalMode {
1185
+ function isApprovalMode(value: unknown): value is SpawnApprovalMode {
1152
1186
  return typeof value === "string" && (APPROVAL_MODES as readonly string[]).includes(value);
1153
1187
  }
1154
1188
 
@@ -1194,7 +1228,7 @@ export interface WorkspaceProbe {
1194
1228
  error?: string;
1195
1229
  }
1196
1230
 
1197
- export interface WorkspaceGitCommit {
1231
+ interface WorkspaceGitCommit {
1198
1232
  sha: string;
1199
1233
  message: string;
1200
1234
  at?: number;
@@ -1539,7 +1573,7 @@ export interface ManagedAgentState {
1539
1573
  updatedAt: number;
1540
1574
  }
1541
1575
 
1542
- export type SpawnPolicyMode = "always-on" | "on-demand";
1576
+ type SpawnPolicyMode = "always-on" | "on-demand";
1543
1577
 
1544
1578
  export type AgentProfileProvider = SpawnProvider | "any";
1545
1579
  export type AgentProfileBase = "host" | "minimal" | "isolated";
@@ -1912,7 +1946,7 @@ export interface OrchestratorRuntimeInput {
1912
1946
  providerCatalog?: ProviderCatalogSummary[];
1913
1947
  }
1914
1948
 
1915
- export interface OrchestratorSpawnInput {
1949
+ interface OrchestratorSpawnInput {
1916
1950
  provider: SpawnProvider;
1917
1951
  model?: string;
1918
1952
  effort?: SpawnEffort;
@@ -2007,7 +2041,7 @@ export interface ProviderCatalogSummary {
2007
2041
  }>;
2008
2042
  }
2009
2043
 
2010
- export interface OrchestratorSpawnResult {
2044
+ interface OrchestratorSpawnResult {
2011
2045
  orchestratorId: string;
2012
2046
  provider: SpawnProvider;
2013
2047
  sessionName?: string;
@@ -2048,19 +2082,19 @@ export interface RecipeAgent {
2048
2082
  env?: Record<string, string>;
2049
2083
  }
2050
2084
 
2051
- export interface RecipeWorkflow {
2085
+ interface RecipeWorkflow {
2052
2086
  trigger?: string;
2053
2087
  fanOut?: "all" | "first";
2054
2088
  collect?: string;
2055
2089
  routing?: RecipeRoute[];
2056
2090
  }
2057
2091
 
2058
- export interface RecipeRoute {
2092
+ interface RecipeRoute {
2059
2093
  pattern: string;
2060
2094
  pipeline: string[];
2061
2095
  }
2062
2096
 
2063
- export interface RecipeLifecycle {
2097
+ interface RecipeLifecycle {
2064
2098
  mode?: "persistent" | "ephemeral";
2065
2099
  idleTimeoutMs?: number;
2066
2100
  memory?: RecipeMemoryPolicy;
@@ -2177,7 +2211,7 @@ export interface TokenConstraints {
2177
2211
  canDelegate?: boolean;
2178
2212
  }
2179
2213
 
2180
- export type TokenScope =
2214
+ type TokenScope =
2181
2215
  | "system:admin"
2182
2216
  | "token:read"
2183
2217
  | "token:write"
@@ -2282,7 +2316,7 @@ export function errMessage(error: unknown): string {
2282
2316
  // --- Relay connection defaults ---
2283
2317
 
2284
2318
  /** Default port the relay server listens on. */
2285
- export const DEFAULT_RELAY_PORT = 4850;
2319
+ const DEFAULT_RELAY_PORT = 4850;
2286
2320
 
2287
2321
  /** Default relay base URL. Loopback spelling settled on `127.0.0.1` (not `localhost`). */
2288
2322
  export const DEFAULT_RELAY_URL = `http://127.0.0.1:${DEFAULT_RELAY_PORT}`;