@hyperdrive.bot/paseo-protocol 0.3.45 → 0.3.46

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.
@@ -429,6 +429,23 @@ export interface AgentSessionConfig {
429
429
  * Mapped by each provider to its native instruction field.
430
430
  */
431
431
  systemPrompt?: string;
432
+ /**
433
+ * BCP-47 tag for the language the agent must WRITE and SPEAK back in
434
+ * ("en-US", "pt-BR"). Session-scoped on purpose: it describes the work
435
+ * ("this agent works in English"), so it follows the agent across every
436
+ * device, client and resume, and it applies to typed input just as much as
437
+ * to dictated or spoken input.
438
+ *
439
+ * Deliberately a structured field and NOT prose folded into `systemPrompt`:
440
+ * voice mode rewrites `systemPrompt` by regex on every toggle
441
+ * (`stripVoiceModeSystemPrompt`), so anything stored in that string is
442
+ * living in a field another feature routinely edits. The instruction text is
443
+ * composed at prompt-build time instead.
444
+ *
445
+ * Undefined means "no instruction", i.e. the model mirrors the user's own
446
+ * language, which is the pre-existing behaviour.
447
+ */
448
+ outputLanguage?: string;
432
449
  modeId?: string;
433
450
  model?: string;
434
451
  thinkingOptionId?: string;
@@ -0,0 +1,82 @@
1
+ /** BCP-47 tag used when nothing else is known. */
2
+ export declare const DEFAULT_LANGUAGE_TAG = "en-US";
3
+ /**
4
+ * Curated language list offered in the audio-mode pickers.
5
+ *
6
+ * Covers the app's own eight shipped UI locales plus the widely-requested
7
+ * European speech languages. It is intentionally a curated list rather than
8
+ * "everything a provider supports": STT coverage is provider-dependent (the
9
+ * default local Parakeet v2 model is English-only; OpenAI Whisper is not), so a
10
+ * longer list would promise accuracy paseo cannot deliver on the default stack.
11
+ */
12
+ export declare const AUDIO_LANGUAGES: readonly [{
13
+ readonly tag: "en-US";
14
+ readonly label: "English";
15
+ readonly nativeLabel: "English";
16
+ }, {
17
+ readonly tag: "pt-BR";
18
+ readonly label: "Portuguese (Brazil)";
19
+ readonly nativeLabel: "Português (Brasil)";
20
+ }, {
21
+ readonly tag: "es-ES";
22
+ readonly label: "Spanish";
23
+ readonly nativeLabel: "Español";
24
+ }, {
25
+ readonly tag: "fr-FR";
26
+ readonly label: "French";
27
+ readonly nativeLabel: "Français";
28
+ }, {
29
+ readonly tag: "de-DE";
30
+ readonly label: "German";
31
+ readonly nativeLabel: "Deutsch";
32
+ }, {
33
+ readonly tag: "it-IT";
34
+ readonly label: "Italian";
35
+ readonly nativeLabel: "Italiano";
36
+ }, {
37
+ readonly tag: "ja-JP";
38
+ readonly label: "Japanese";
39
+ readonly nativeLabel: "日本語";
40
+ }, {
41
+ readonly tag: "zh-CN";
42
+ readonly label: "Chinese (Simplified)";
43
+ readonly nativeLabel: "简体中文";
44
+ }, {
45
+ readonly tag: "ru-RU";
46
+ readonly label: "Russian";
47
+ readonly nativeLabel: "Русский";
48
+ }, {
49
+ readonly tag: "ar-SA";
50
+ readonly label: "Arabic";
51
+ readonly nativeLabel: "العربية";
52
+ }];
53
+ export type AudioLanguageTag = (typeof AUDIO_LANGUAGES)[number]["tag"];
54
+ /**
55
+ * Normalize any user- or config-supplied tag to canonical BCP-47 casing
56
+ * ("PT-br" -> "pt-BR"). Returns undefined for empty/unusable input rather than
57
+ * inventing a default, so callers decide their own fallback.
58
+ */
59
+ export declare function normalizeLanguageTag(value: string | null | undefined): string | undefined;
60
+ /**
61
+ * BCP-47 -> ISO-639-1, the form the speech daemon and OpenAI Whisper expect
62
+ * ("pt-BR" -> "pt"). This is the ONLY place the conversion happens.
63
+ */
64
+ export declare function toIso639(value: string | null | undefined): string | undefined;
65
+ /**
66
+ * True when the tag resolves to a language paseo can NAME in English.
67
+ *
68
+ * `normalizeLanguageTag` only checks shape, and a 2-3 letter shape is cheap to
69
+ * hit by accident: "not-a-language" normalizes to the syntactically valid
70
+ * subtag "not". Callers that put the name into a prompt must gate on this, so
71
+ * a typo produces NO instruction rather than a confident instruction to write
72
+ * everything in "not".
73
+ */
74
+ export declare function isKnownLanguage(value: string | null | undefined): boolean;
75
+ /**
76
+ * Human-readable English name for a language tag, for embedding in prompts and
77
+ * logs. Falls back to the normalized tag itself so an unlisted-but-valid
78
+ * language still renders as something in a UI or a log line. Prompt builders
79
+ * must gate on {@link isKnownLanguage} first.
80
+ */
81
+ export declare function languageLabel(value: string | null | undefined): string | undefined;
82
+ //# sourceMappingURL=language.d.ts.map
@@ -0,0 +1,148 @@
1
+ // Shared language vocabulary for paseo's two audio modes (dictation + voice mode)
2
+ // and for the agent's output language.
3
+ //
4
+ // Two DIFFERENT parameters live here, and conflating them is the bug this module
5
+ // exists to prevent:
6
+ //
7
+ // - **input language** = the language the human SPEAKS into the microphone.
8
+ // Per device, never per session: the same agent can be open on a phone, a
9
+ // desktop and a watch, each with a different microphone in front of a
10
+ // different mouth. It drives STT only.
11
+ // - **output language** = the language the agent WRITES and SPEAKS back in.
12
+ // Per session, replicated with the agent record, because it is a property of
13
+ // the work ("this agent works in English"), not of any one microphone.
14
+ //
15
+ // The two are deliberately decoupled: speaking Portuguese while the repo, the
16
+ // commits and the docs stay English is a first-class combination, not an edge
17
+ // case.
18
+ //
19
+ // Tag formats: the app edge speaks BCP-47 ("pt-BR"), the speech daemon speaks
20
+ // ISO-639-1 ("pt"). `toIso639` is the single conversion point; do the
21
+ // conversion at the boundary and nowhere else.
22
+ /** BCP-47 tag used when nothing else is known. */
23
+ export const DEFAULT_LANGUAGE_TAG = "en-US";
24
+ /**
25
+ * Curated language list offered in the audio-mode pickers.
26
+ *
27
+ * Covers the app's own eight shipped UI locales plus the widely-requested
28
+ * European speech languages. It is intentionally a curated list rather than
29
+ * "everything a provider supports": STT coverage is provider-dependent (the
30
+ * default local Parakeet v2 model is English-only; OpenAI Whisper is not), so a
31
+ * longer list would promise accuracy paseo cannot deliver on the default stack.
32
+ */
33
+ export const AUDIO_LANGUAGES = [
34
+ { tag: "en-US", label: "English", nativeLabel: "English" },
35
+ { tag: "pt-BR", label: "Portuguese (Brazil)", nativeLabel: "Português (Brasil)" },
36
+ { tag: "es-ES", label: "Spanish", nativeLabel: "Español" },
37
+ { tag: "fr-FR", label: "French", nativeLabel: "Français" },
38
+ { tag: "de-DE", label: "German", nativeLabel: "Deutsch" },
39
+ { tag: "it-IT", label: "Italian", nativeLabel: "Italiano" },
40
+ { tag: "ja-JP", label: "Japanese", nativeLabel: "日本語" },
41
+ { tag: "zh-CN", label: "Chinese (Simplified)", nativeLabel: "简体中文" },
42
+ { tag: "ru-RU", label: "Russian", nativeLabel: "Русский" },
43
+ { tag: "ar-SA", label: "Arabic", nativeLabel: "العربية" },
44
+ ];
45
+ /**
46
+ * English display names keyed by ISO-639-1, used to render a language name
47
+ * inside a system prompt.
48
+ *
49
+ * Deliberately a static map rather than `Intl.DisplayNames`: the value is
50
+ * embedded in a prompt and must be byte-identical across the daemon, the tests
51
+ * and every Node/Hermes runtime paseo runs on. `Intl` availability and output
52
+ * both vary by runtime.
53
+ */
54
+ const LANGUAGE_LABELS_BY_ISO639 = {
55
+ ar: "Arabic",
56
+ de: "German",
57
+ en: "English",
58
+ es: "Spanish",
59
+ fr: "French",
60
+ hi: "Hindi",
61
+ it: "Italian",
62
+ ja: "Japanese",
63
+ ko: "Korean",
64
+ nl: "Dutch",
65
+ pl: "Polish",
66
+ pt: "Portuguese",
67
+ ru: "Russian",
68
+ sv: "Swedish",
69
+ tr: "Turkish",
70
+ zh: "Chinese",
71
+ };
72
+ /** Region subtags worth naming explicitly, because the variants differ in use. */
73
+ const REGION_LABELS = {
74
+ "pt-BR": "Portuguese (Brazil)",
75
+ "pt-PT": "Portuguese (Portugal)",
76
+ "en-US": "English (US)",
77
+ "en-GB": "English (UK)",
78
+ "zh-CN": "Chinese (Simplified)",
79
+ "zh-TW": "Chinese (Traditional)",
80
+ };
81
+ /**
82
+ * Normalize any user- or config-supplied tag to canonical BCP-47 casing
83
+ * ("PT-br" -> "pt-BR"). Returns undefined for empty/unusable input rather than
84
+ * inventing a default, so callers decide their own fallback.
85
+ */
86
+ export function normalizeLanguageTag(value) {
87
+ const trimmed = value?.trim();
88
+ if (!trimmed) {
89
+ return undefined;
90
+ }
91
+ const [primary, ...rest] = trimmed.replace(/_/g, "-").split("-");
92
+ if (!primary) {
93
+ return undefined;
94
+ }
95
+ const language = primary.toLowerCase();
96
+ if (!/^[a-z]{2,3}$/.test(language)) {
97
+ return undefined;
98
+ }
99
+ const region = rest.find((part) => /^[A-Za-z]{2}$/.test(part) || /^[0-9]{3}$/.test(part));
100
+ return region ? `${language}-${region.toUpperCase()}` : language;
101
+ }
102
+ /**
103
+ * BCP-47 -> ISO-639-1, the form the speech daemon and OpenAI Whisper expect
104
+ * ("pt-BR" -> "pt"). This is the ONLY place the conversion happens.
105
+ */
106
+ export function toIso639(value) {
107
+ const normalized = normalizeLanguageTag(value);
108
+ return normalized?.split("-")[0];
109
+ }
110
+ /**
111
+ * True when the tag resolves to a language paseo can NAME in English.
112
+ *
113
+ * `normalizeLanguageTag` only checks shape, and a 2-3 letter shape is cheap to
114
+ * hit by accident: "not-a-language" normalizes to the syntactically valid
115
+ * subtag "not". Callers that put the name into a prompt must gate on this, so
116
+ * a typo produces NO instruction rather than a confident instruction to write
117
+ * everything in "not".
118
+ */
119
+ export function isKnownLanguage(value) {
120
+ const normalized = normalizeLanguageTag(value);
121
+ if (!normalized) {
122
+ return false;
123
+ }
124
+ if (REGION_LABELS[normalized]) {
125
+ return true;
126
+ }
127
+ const iso = normalized.split("-")[0] ?? normalized;
128
+ return Boolean(LANGUAGE_LABELS_BY_ISO639[iso]);
129
+ }
130
+ /**
131
+ * Human-readable English name for a language tag, for embedding in prompts and
132
+ * logs. Falls back to the normalized tag itself so an unlisted-but-valid
133
+ * language still renders as something in a UI or a log line. Prompt builders
134
+ * must gate on {@link isKnownLanguage} first.
135
+ */
136
+ export function languageLabel(value) {
137
+ const normalized = normalizeLanguageTag(value);
138
+ if (!normalized) {
139
+ return undefined;
140
+ }
141
+ const regionLabel = REGION_LABELS[normalized];
142
+ if (regionLabel) {
143
+ return regionLabel;
144
+ }
145
+ const iso = normalized.split("-")[0] ?? normalized;
146
+ return LANGUAGE_LABELS_BY_ISO639[iso] ?? normalized;
147
+ }
148
+ //# sourceMappingURL=language.js.map
@@ -1861,6 +1861,7 @@ export declare const CreateAgentRequestMessageSchema: z.ZodObject<{
1861
1861
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
1862
1862
  }, z.core.$strip>>;
1863
1863
  systemPrompt: z.ZodOptional<z.ZodString>;
1864
+ outputLanguage: z.ZodOptional<z.ZodString>;
1864
1865
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
1865
1866
  type: z.ZodLiteral<"stdio">;
1866
1867
  command: z.ZodString;
@@ -2050,6 +2051,7 @@ export declare const ResumeAgentRequestMessageSchema: z.ZodObject<{
2050
2051
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
2051
2052
  }, z.core.$strip>>>;
2052
2053
  systemPrompt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
2054
+ outputLanguage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
2053
2055
  mcpServers: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
2054
2056
  type: z.ZodLiteral<"stdio">;
2055
2057
  command: z.ZodString;
@@ -4089,6 +4091,7 @@ export declare const SessionInboundMessageSchema: z.ZodDiscriminatedUnion<[z.Zod
4089
4091
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
4090
4092
  }, z.core.$strip>>;
4091
4093
  systemPrompt: z.ZodOptional<z.ZodString>;
4094
+ outputLanguage: z.ZodOptional<z.ZodString>;
4092
4095
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
4093
4096
  type: z.ZodLiteral<"stdio">;
4094
4097
  command: z.ZodString;
@@ -4273,6 +4276,7 @@ export declare const SessionInboundMessageSchema: z.ZodDiscriminatedUnion<[z.Zod
4273
4276
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
4274
4277
  }, z.core.$strip>>>;
4275
4278
  systemPrompt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
4279
+ outputLanguage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
4276
4280
  mcpServers: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
4277
4281
  type: z.ZodLiteral<"stdio">;
4278
4282
  command: z.ZodString;
@@ -21461,6 +21465,7 @@ export declare const WSSessionInboundSchema: z.ZodObject<{
21461
21465
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
21462
21466
  }, z.core.$strip>>;
21463
21467
  systemPrompt: z.ZodOptional<z.ZodString>;
21468
+ outputLanguage: z.ZodOptional<z.ZodString>;
21464
21469
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
21465
21470
  type: z.ZodLiteral<"stdio">;
21466
21471
  command: z.ZodString;
@@ -21645,6 +21650,7 @@ export declare const WSSessionInboundSchema: z.ZodObject<{
21645
21650
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
21646
21651
  }, z.core.$strip>>>;
21647
21652
  systemPrompt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
21653
+ outputLanguage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
21648
21654
  mcpServers: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
21649
21655
  type: z.ZodLiteral<"stdio">;
21650
21656
  command: z.ZodString;
@@ -31087,6 +31093,7 @@ export declare const WSInboundMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
31087
31093
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
31088
31094
  }, z.core.$strip>>;
31089
31095
  systemPrompt: z.ZodOptional<z.ZodString>;
31096
+ outputLanguage: z.ZodOptional<z.ZodString>;
31090
31097
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
31091
31098
  type: z.ZodLiteral<"stdio">;
31092
31099
  command: z.ZodString;
@@ -31271,6 +31278,7 @@ export declare const WSInboundMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
31271
31278
  claude: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
31272
31279
  }, z.core.$strip>>>;
31273
31280
  systemPrompt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
31281
+ outputLanguage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
31274
31282
  mcpServers: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
31275
31283
  type: z.ZodLiteral<"stdio">;
31276
31284
  command: z.ZodString;
package/dist/messages.js CHANGED
@@ -516,6 +516,11 @@ const AgentSessionConfigSchema = z.object({
516
516
  .partial()
517
517
  .optional(),
518
518
  systemPrompt: z.string().optional(),
519
+ // BCP-47 tag the agent must write and speak back in. Session-scoped, so it
520
+ // follows the agent across devices and survives resume. See
521
+ // AgentSessionConfig.outputLanguage in agent-types.ts for why it is a
522
+ // structured field rather than prose inside systemPrompt.
523
+ outputLanguage: z.string().optional(),
519
524
  mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
520
525
  // Per-run tool allowlist, in Claude Code permission-rule syntax ("Bash",
521
526
  // "Bash(git:*)", "mcp__server__tool"). Optional: absent means "no allowlist",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/paseo-protocol",
3
- "version": "0.3.45",
3
+ "version": "0.3.46",
4
4
  "description": "Paseo shared protocol schemas and wire types",
5
5
  "files": [
6
6
  "dist",