@datalayer/agent-runtimes 1.5.28 → 1.5.31

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 (38) hide show
  1. package/dist-embed/chunks/{ChatView-C0PWJ_wj.js → ChatView-C_NOLqwI.js} +3 -3
  2. package/dist-embed/datalayer-app-main.js +83 -83
  3. package/lib/apps/apps/appspec.d.ts +0 -5
  4. package/lib/apps/apps/appspec.js +1 -6
  5. package/lib/apps/apps/checks.js +13 -9
  6. package/lib/apps/embed/element.js +14 -3
  7. package/lib/apps/embed/embedConfig.d.ts +1 -1
  8. package/lib/apps/embed/embedConfig.js +1 -1
  9. package/lib/apps/embed/visitorApp.d.ts +9 -2
  10. package/lib/apps/embed/visitorApp.js +9 -2
  11. package/lib/apps/plugins/chat/ChatView.js +4 -1
  12. package/lib/chat/base/ChatBase.js +12 -3
  13. package/lib/runtimes/variants.d.ts +42 -37
  14. package/lib/runtimes/variants.js +109 -9
  15. package/lib/specs/appspecSchema.js +2 -2
  16. package/lib/specs/envvars.d.ts +4 -0
  17. package/lib/specs/envvars.js +44 -0
  18. package/lib/specs/harnesses.d.ts +22 -0
  19. package/lib/specs/harnesses.js +372 -0
  20. package/lib/specs/index.d.ts +1 -0
  21. package/lib/specs/index.js +1 -0
  22. package/lib/specs/voices.js +42 -0
  23. package/lib/types/agentspecs.d.ts +70 -1
  24. package/lib/voice/ComposerVoice.d.ts +10 -2
  25. package/lib/voice/ComposerVoice.js +10 -2
  26. package/lib/voice/HandsFree.d.ts +55 -0
  27. package/lib/voice/HandsFree.js +211 -0
  28. package/lib/voice/chatVoice.d.ts +2 -1
  29. package/lib/voice/chatVoice.js +5 -3
  30. package/lib/voice/handsFree.d.ts +58 -0
  31. package/lib/voice/handsFree.js +48 -0
  32. package/lib/voice/index.d.ts +10 -2
  33. package/lib/voice/index.js +2 -0
  34. package/package.json +1 -1
  35. package/scripts/codegen/generate_agents.py +56 -0
  36. package/scripts/codegen/generate_envvars.py +3 -2
  37. package/scripts/codegen/generate_harnesses.py +329 -0
  38. package/scripts/voice/measure.py +74 -0
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2025-2026 Datalayer, Inc.
3
+ # Distributed under the terms of the Modified BSD License.
4
+
5
+ """
6
+ Generate Python and TypeScript code from the YAML harness specifications.
7
+
8
+ One YAML per harness under ``agentspecs/harnesses`` (SHIELD.md SH-01, §3.1):
9
+ an agent program driven over the Agent Client Protocol, how it is launched,
10
+ what it needs, what it honours of an agent spec, and the terms it is served
11
+ under. ``register.yaml`` beside them is the register (SH-04, §3.3): a harness
12
+ without an entry is refused here as it is in agentspecs.
13
+
14
+ Usage:
15
+ python generate_harnesses.py \\
16
+ --specs-dir agentspecs/agentspecs/harnesses \\
17
+ --python-output agent_runtimes/specs/harnesses.py \\
18
+ --typescript-output src/specs/harnesses.ts
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ import yaml
27
+ from versioning import ensure_spec_version, version_suffix
28
+
29
+ MCP = ("lend", "own", "none")
30
+ THIRD_PARTIES = ("api_key_only", "permitted", "refused", "organizations_own")
31
+ SKILLS_FORMATS = ("skill-md", "none")
32
+ REGISTER = "register.yaml"
33
+
34
+
35
+ def load_register(specs_dir: Path) -> dict[str, dict[str, Any]]:
36
+ """The register, by harness id."""
37
+ register = yaml.safe_load((specs_dir / REGISTER).read_text()) or {}
38
+ return {str(entry["harness"]): entry for entry in register.get("entries") or []}
39
+
40
+
41
+ def load_harness_specs(specs_dir: Path) -> list[dict[str, Any]]:
42
+ """Every harness spec of a directory, each named for its id, registered, its enums known."""
43
+ register = load_register(specs_dir)
44
+ specs = []
45
+ for yaml_file in sorted(specs_dir.glob("*.yaml")):
46
+ if yaml_file.name == REGISTER:
47
+ continue
48
+ spec = yaml.safe_load(yaml_file.read_text()) or {}
49
+ ensure_spec_version(spec)
50
+ if spec["id"] != yaml_file.stem:
51
+ raise ValueError(
52
+ f"{yaml_file.name}: the file is named for {yaml_file.stem!r}, the spec says {spec['id']!r}"
53
+ )
54
+ entry = register.get(spec["id"])
55
+ if entry is None:
56
+ raise ValueError(
57
+ f"{yaml_file.name}: the harness {spec['id']!r} has no entry in the register"
58
+ )
59
+ acp = spec.get("acp")
60
+ if spec["id"] == "custom":
61
+ if acp is not None:
62
+ raise ValueError(f"{yaml_file.name}: custom names no command")
63
+ elif not acp or not acp.get("command"):
64
+ raise ValueError(f"{yaml_file.name}: an acp block with its command")
65
+ if acp is not None and acp.get("mcp") not in MCP:
66
+ raise ValueError(f"{yaml_file.name}: acp.mcp one of {MCP}")
67
+ terms = spec.get("terms") or {}
68
+ if terms.get("served_to_third_parties") not in THIRD_PARTIES:
69
+ raise ValueError(
70
+ f"{yaml_file.name}: terms.served_to_third_parties one of {THIRD_PARTIES}"
71
+ )
72
+ if terms.get("served_to_third_parties") != entry.get("served_to_third_parties"):
73
+ raise ValueError(
74
+ f"{yaml_file.name}: the register and the spec disagree on third parties"
75
+ )
76
+ skills = spec.get("skills") or {}
77
+ if skills.get("format") not in SKILLS_FORMATS:
78
+ raise ValueError(f"{yaml_file.name}: skills.format one of {SKILLS_FORMATS}")
79
+ spec["_register"] = entry
80
+ specs.append(spec)
81
+ return specs
82
+
83
+
84
+ def _const(spec: dict[str, Any]) -> str:
85
+ return (
86
+ spec["id"].replace("-", "_").upper()
87
+ + "_HARNESS"
88
+ + version_suffix(spec["version"])
89
+ )
90
+
91
+
92
+ def _s(value: Any) -> str:
93
+ return json.dumps(" ".join(str(value or "").split()), ensure_ascii=False)
94
+
95
+
96
+ def _opt(value: Any) -> str:
97
+ return _s(value) if value else "None"
98
+
99
+
100
+ def generate_python_code(specs: list[dict[str, Any]]) -> str:
101
+ lines = [
102
+ "# Copyright (c) 2025-2026 Datalayer, Inc.",
103
+ "# Distributed under the terms of the Modified BSD License.",
104
+ '"""',
105
+ "Harness Catalog.",
106
+ "",
107
+ "An agent program driven over the Agent Client Protocol (SHIELD.md SH-01).",
108
+ "",
109
+ "This file is AUTO-GENERATED from YAML specifications.",
110
+ "DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
111
+ '"""',
112
+ "",
113
+ "from typing import Dict, List, Optional",
114
+ "",
115
+ "from agent_runtimes.types import (",
116
+ " HarnessAcpSpec,",
117
+ " HarnessHonoursSpec,",
118
+ " HarnessRegisterEntry,",
119
+ " HarnessRequiresSpec,",
120
+ " HarnessSkillsSpec,",
121
+ " HarnessSpec,",
122
+ " HarnessTermsSpec,",
123
+ ")",
124
+ ]
125
+ for spec in specs:
126
+ acp = spec.get("acp")
127
+ requires = spec.get("requires") or {}
128
+ terms = spec.get("terms") or {}
129
+ skills = spec.get("skills") or {}
130
+ honours = spec.get("honours") or {}
131
+ entry = spec["_register"]
132
+ lines += [
133
+ "",
134
+ f"{_const(spec)} = HarnessSpec(",
135
+ f" id={_s(spec['id'])},",
136
+ f" version={_s(spec['version'])},",
137
+ f" name={_s(spec['name'])},",
138
+ f" description={_s(spec.get('description'))},",
139
+ f" enabled={bool(spec.get('enabled', True))},",
140
+ f" tags={json.dumps(list(spec.get('tags') or []))},",
141
+ f" docs={_s(spec.get('docs'))},",
142
+ ]
143
+ if acp is None:
144
+ lines.append(" acp=None,")
145
+ else:
146
+ lines += [
147
+ " acp=HarnessAcpSpec(",
148
+ f" command={_s(acp['command'])},",
149
+ f" args={json.dumps(list(acp.get('args') or []))},",
150
+ f" protocol_version={int(acp.get('protocol_version', 1))},",
151
+ f" load_session={bool(acp.get('load_session', False))},",
152
+ f" mcp={_s(acp['mcp'])},",
153
+ f" modes={json.dumps(list(acp.get('modes') or []))},",
154
+ f" env={json.dumps(dict(acp.get('env') or {}))},",
155
+ " ),",
156
+ ]
157
+ lines += [
158
+ " requires=HarnessRequiresSpec(",
159
+ f" secrets={json.dumps(list(requires.get('secrets') or []))},",
160
+ f" binaries={json.dumps(list(requires.get('binaries') or []))},",
161
+ " ),",
162
+ " terms=HarnessTermsSpec(",
163
+ f" served_to_third_parties={_s(terms['served_to_third_parties'])},",
164
+ f" licence={_s(terms.get('licence'))},",
165
+ f" tier_restrictions={_opt(terms.get('tier_restrictions'))},",
166
+ " ),",
167
+ " skills=HarnessSkillsSpec(",
168
+ f" format={_s(skills['format'])},",
169
+ f" dir={_s(skills.get('dir'))},",
170
+ " ),",
171
+ " honours=HarnessHonoursSpec(",
172
+ f" model={bool(honours.get('model', False))},",
173
+ f" subagents={bool(honours.get('subagents', False))},",
174
+ " ),",
175
+ f" instructions_file={_s(spec.get('instructions_file'))},",
176
+ " registered=HarnessRegisterEntry(",
177
+ f" adapter={_s(entry.get('adapter'))},",
178
+ f" version={_s(entry.get('version'))},",
179
+ f" licence_source={_s(entry.get('licence_source'))},",
180
+ f" terms_source={_s(entry.get('terms_source'))},",
181
+ f" credential={_s(entry.get('credential'))},",
182
+ f" binaries={_s(entry.get('binaries'))},",
183
+ f" note={_s(entry.get('note'))},",
184
+ " ),",
185
+ ")",
186
+ ]
187
+ lines += [
188
+ "",
189
+ "",
190
+ "HARNESS_CATALOGUE: Dict[str, HarnessSpec] = {",
191
+ *[f" {_s(spec['id'])}: {_const(spec)}," for spec in specs],
192
+ "}",
193
+ "",
194
+ "",
195
+ "def get_harness(harness_id: str) -> Optional[HarnessSpec]:",
196
+ ' """A harness by id, or None."""',
197
+ " return HARNESS_CATALOGUE.get(harness_id)",
198
+ "",
199
+ "",
200
+ "def list_harnesses() -> List[HarnessSpec]:",
201
+ ' """Every harness of the catalogue."""',
202
+ " return list(HARNESS_CATALOGUE.values())",
203
+ "",
204
+ ]
205
+ return "\n".join(lines)
206
+
207
+
208
+ def _t(value: Any) -> str:
209
+ return json.dumps(" ".join(str(value or "").split()), ensure_ascii=False)
210
+
211
+
212
+ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
213
+ lines = [
214
+ "/*",
215
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
216
+ " * Distributed under the terms of the Modified BSD License.",
217
+ " */",
218
+ "",
219
+ "/**",
220
+ " * Harness Catalog.",
221
+ " *",
222
+ " * An agent program driven over the Agent Client Protocol (SHIELD.md SH-01).",
223
+ " *",
224
+ " * This file is AUTO-GENERATED from YAML specifications.",
225
+ " * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
226
+ " */",
227
+ "",
228
+ "import type { HarnessSpec } from '../types/agentspecs';",
229
+ ]
230
+ for spec in specs:
231
+ acp = spec.get("acp")
232
+ requires = spec.get("requires") or {}
233
+ terms = spec.get("terms") or {}
234
+ skills = spec.get("skills") or {}
235
+ honours = spec.get("honours") or {}
236
+ entry = spec["_register"]
237
+ lines += [
238
+ "",
239
+ f"export const {_const(spec)}: HarnessSpec = {{",
240
+ f" id: {_t(spec['id'])},",
241
+ f" version: {_t(spec['version'])},",
242
+ f" name: {_t(spec['name'])},",
243
+ f" description: {_t(spec.get('description'))},",
244
+ f" enabled: {json.dumps(bool(spec.get('enabled', True)))},",
245
+ f" tags: {json.dumps(list(spec.get('tags') or []))},",
246
+ f" docs: {_t(spec.get('docs'))},",
247
+ ]
248
+ if acp is not None:
249
+ lines += [
250
+ " acp: {",
251
+ f" command: {_t(acp['command'])},",
252
+ f" args: {json.dumps(list(acp.get('args') or []))},",
253
+ f" protocolVersion: {int(acp.get('protocol_version', 1))},",
254
+ f" loadSession: {json.dumps(bool(acp.get('load_session', False)))},",
255
+ f" mcp: {_t(acp['mcp'])},",
256
+ f" modes: {json.dumps(list(acp.get('modes') or []))},",
257
+ f" env: {json.dumps(dict(acp.get('env') or {}))},",
258
+ " },",
259
+ ]
260
+ lines += [
261
+ " requires: {",
262
+ f" secrets: {json.dumps(list(requires.get('secrets') or []))},",
263
+ f" binaries: {json.dumps(list(requires.get('binaries') or []))},",
264
+ " },",
265
+ " terms: {",
266
+ f" servedToThirdParties: {_t(terms['served_to_third_parties'])},",
267
+ f" licence: {_t(terms.get('licence'))},",
268
+ *(
269
+ [f" tierRestrictions: {_t(terms.get('tier_restrictions'))},"]
270
+ if terms.get("tier_restrictions")
271
+ else []
272
+ ),
273
+ " },",
274
+ " skills: {",
275
+ f" format: {_t(skills['format'])},",
276
+ f" dir: {_t(skills.get('dir'))},",
277
+ " },",
278
+ " honours: {",
279
+ f" model: {json.dumps(bool(honours.get('model', False)))},",
280
+ f" subagents: {json.dumps(bool(honours.get('subagents', False)))},",
281
+ " },",
282
+ f" instructionsFile: {_t(spec.get('instructions_file'))},",
283
+ " registered: {",
284
+ f" adapter: {_t(entry.get('adapter'))},",
285
+ f" version: {_t(entry.get('version'))},",
286
+ f" licenceSource: {_t(entry.get('licence_source'))},",
287
+ f" termsSource: {_t(entry.get('terms_source'))},",
288
+ f" credential: {_t(entry.get('credential'))},",
289
+ f" binaries: {_t(entry.get('binaries'))},",
290
+ f" note: {_t(entry.get('note'))},",
291
+ " },",
292
+ "};",
293
+ ]
294
+ lines += [
295
+ "",
296
+ "export const HARNESS_CATALOGUE: Record<string, HarnessSpec> = {",
297
+ *[f" {_t(spec['id'])}: {_const(spec)}," for spec in specs],
298
+ "};",
299
+ "",
300
+ "/** A harness by id. */",
301
+ "export function getHarness(harnessId: string): HarnessSpec | undefined {",
302
+ " return HARNESS_CATALOGUE[harnessId];",
303
+ "}",
304
+ "",
305
+ "/** Every harness of the catalogue. */",
306
+ "export function listHarnesses(): HarnessSpec[] {",
307
+ " return Object.values(HARNESS_CATALOGUE);",
308
+ "}",
309
+ "",
310
+ ]
311
+ return "\n".join(lines)
312
+
313
+
314
+ def main() -> None:
315
+ parser = argparse.ArgumentParser(description=__doc__)
316
+ parser.add_argument("--specs-dir", type=Path, required=True)
317
+ parser.add_argument("--python-output", type=Path, required=True)
318
+ parser.add_argument("--typescript-output", type=Path, required=True)
319
+ args = parser.parse_args()
320
+ specs = load_harness_specs(args.specs_dir)
321
+ args.python_output.write_text(generate_python_code(specs))
322
+ args.typescript_output.write_text(generate_typescript_code(specs))
323
+ print(
324
+ f"Generated {len(specs)} harnesses: {args.python_output}, {args.typescript_output}"
325
+ )
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
@@ -92,6 +92,67 @@ def transcribe(
92
92
  return json.loads(done.stdout.strip().splitlines()[-1])
93
93
 
94
94
 
95
+ def measure_server(
96
+ store: Path,
97
+ voices: Any,
98
+ fixtures: List[Dict[str, Any]],
99
+ wavs: Dict[str, Path],
100
+ measured: Path,
101
+ threads: int,
102
+ ) -> None:
103
+ """Each server speech-to-text model on the fixtures, as the speech service runs it (VO-47).
104
+
105
+ faster-whisper, 8-bit on the CPU with ``threads`` threads, told the
106
+ language, one clip at a time: what it heard and how long it took.
107
+ """
108
+ from faster_whisper import WhisperModel # the speech image's own
109
+
110
+ for model in voices.SPEECH_MODEL_CATALOGUE.values():
111
+ if model["task"] != "stt" or model["where"] != ["server"]:
112
+ continue
113
+ started = time.monotonic()
114
+ whisper = WhisperModel(
115
+ str(store / model["provider"] / model["id"]),
116
+ device="cpu",
117
+ compute_type="int8",
118
+ cpu_threads=threads,
119
+ )
120
+ load_ms = round((time.monotonic() - started) * 1000)
121
+ by_language = {}
122
+ for language in ("en", "fr"):
123
+ clips = [clip for clip in fixtures if clip["language"] == language]
124
+ print(f"{model['id']} on {len(clips)} {language} clips", flush=True)
125
+ results = []
126
+ for clip in clips:
127
+ samples, _ = sf.read(wavs[clip["id"]], dtype="float32")
128
+ started = time.monotonic()
129
+ parts, _ = whisper.transcribe(
130
+ samples, language=language, vad_filter=True
131
+ )
132
+ text = " ".join(part.text.strip() for part in parts).strip()
133
+ results.append(
134
+ {
135
+ "id": clip["id"],
136
+ "heard": text,
137
+ "ms": round((time.monotonic() - started) * 1000),
138
+ "audio_s": round(len(samples) / 16000, 2),
139
+ }
140
+ )
141
+ by_language[language] = {"load_ms": load_ms, "clips": results}
142
+ (measured / f"stt-{model['id']}.json").write_text(
143
+ json.dumps(
144
+ {
145
+ "model": model["id"],
146
+ "runtime": f"faster-whisper, int8 on the CPU, {threads} threads",
147
+ "languages": by_language,
148
+ },
149
+ ensure_ascii=False,
150
+ indent=2,
151
+ )
152
+ + "\n"
153
+ )
154
+
155
+
95
156
  def main() -> int:
96
157
  parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
97
158
  parser.add_argument("--store", type=Path, required=True)
@@ -101,6 +162,14 @@ def main() -> int:
101
162
  default=None,
102
163
  help="Where @huggingface/transformers is installed, when not in this checkout",
103
164
  )
165
+ parser.add_argument(
166
+ "--server",
167
+ action="store_true",
168
+ help="Measure the server's speech-to-text models instead, with faster-whisper (in the speech image)",
169
+ )
170
+ parser.add_argument(
171
+ "--threads", type=int, default=4, help="CPU threads for --server"
172
+ )
104
173
  args = parser.parse_args()
105
174
  voices = catalogue()
106
175
  env = dict(os.environ)
@@ -124,6 +193,11 @@ def main() -> int:
124
193
  wav = Path(scratch) / f"{clip['id']}.wav"
125
194
  sf.write(wav, resample(samples, rate), 16000, subtype="PCM_16")
126
195
  wavs[clip["id"]] = wav
196
+ if args.server:
197
+ measure_server(
198
+ args.store, voices, fixtures, wavs, measured, args.threads
199
+ )
200
+ return 0
127
201
  # --- speech to text, on the device's models ---------------------------------
128
202
  for model in voices.SPEECH_MODEL_CATALOGUE.values():
129
203
  if model["task"] != "stt" or "device" not in model["where"]: