@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
@@ -99,11 +99,6 @@ export declare const PAP_PROFILE: PapProfile;
99
99
  * (`pap_profile_problem`); `undefined` when it is one Datalayer speaks.
100
100
  */
101
101
  export declare function papProfileProblem(value: unknown): string | undefined;
102
- /**
103
- * Why `interface.voice.input: hands_free` is refused until VOICE.md VO-12 is
104
- * built (decided 14 October 2026): never taken for click-to-talk.
105
- */
106
- export declare const HANDS_FREE_NOT_BUILT = "hands_free is not built yet (VOICE.md VO-12): say push_to_talk \u2014 click the microphone or hold it, speak, and it stops when let go or once the person stops speaking";
107
102
  /** An application's voice (VOICE.md VO-41): off unless said. */
108
103
  export declare const DEFAULT_VOICE: AppVoiceSpec;
109
104
  /** What a page's output may be drawn with (LOOP P-05), and the property of each that shows its value. */
@@ -713,11 +713,6 @@ function parseSurface(data) {
713
713
  composedAt: text(data.composed_at),
714
714
  };
715
715
  }
716
- /**
717
- * Why `interface.voice.input: hands_free` is refused until VOICE.md VO-12 is
718
- * built (decided 14 October 2026): never taken for click-to-talk.
719
- */
720
- export const HANDS_FREE_NOT_BUILT = 'hands_free is not built yet (VOICE.md VO-12): say push_to_talk — click the microphone or hold it, speak, and it stops when let go or once the person stops speaking';
721
716
  /** An application's voice (VOICE.md VO-41): off unless said. */
722
717
  export const DEFAULT_VOICE = {
723
718
  enabled: false,
@@ -731,7 +726,7 @@ function parseVoice(data) {
731
726
  const voice = isData(data) ? data : {};
732
727
  return {
733
728
  enabled: voice.enabled === true,
734
- input: oneOf(voice.input, ['off', 'push_to_talk'], DEFAULT_VOICE.input),
729
+ input: oneOf(voice.input, ['off', 'push_to_talk', 'hands_free'], DEFAULT_VOICE.input),
735
730
  output: oneOf(voice.output, ['off', 'on_request', 'always'], DEFAULT_VOICE.output),
736
731
  voice: text(voice.voice),
737
732
  language: text(voice.language),
@@ -38,8 +38,9 @@ import { getSkillSpec } from '../../specs/skills';
38
38
  import { getTeamSpec } from '../../specs/teams';
39
39
  import { getBackendToolSpec } from '../../specs/backendTools';
40
40
  import { getTrack } from '../../specs/tracks';
41
+ import { unservedHarnessProblem } from '../../runtimes/variants';
41
42
  import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
42
- import { APP_THEME_MODES, APP_THEME_VARIANTS, HANDS_FREE_NOT_BUILT, isAssistantCharacterId, PAGE_OUTPUT_SHOWS, parseAppspec, } from './appspec';
43
+ import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, PAGE_OUTPUT_SHOWS, parseAppspec, } from './appspec';
43
44
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
45
  import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
45
46
  import { HOST_NAME, HOST_USERS, hostToolsOf } from './hostTools';
@@ -397,6 +398,14 @@ function referenceProblems(app, organizationFrames) {
397
398
  if (app.agent && !getAgentspecs(idOf(app.agent)) && !getCog(app.agent)) {
398
399
  missing('agent or Cog', app.agent);
399
400
  }
401
+ // An agent on a catalogue harness is served by the runtime's ACP adapter
402
+ // (SHIELD SH-10); `custom` and a harness that is none are refused, not run
403
+ // on pydantic-ai in their place (SH-01).
404
+ const agent = app.agent ? getAgentspecs(idOf(app.agent)) : undefined;
405
+ const unserved = agent ? unservedHarnessProblem(agent) : undefined;
406
+ if (unserved) {
407
+ problems.push(unserved);
408
+ }
400
409
  if (app.team && !getTeamSpec(idOf(app.team))) {
401
410
  missing('team', app.team);
402
411
  }
@@ -685,7 +694,7 @@ const ENUMS = {
685
694
  visibility: ['private', 'invited', 'organization', 'link', 'public'],
686
695
  mode: ['inline', 'bubble', 'panel', 'assistant'],
687
696
  balloon: ['history', 'current'],
688
- voiceInput: ['off', 'push_to_talk'],
697
+ voiceInput: ['off', 'push_to_talk', 'hands_free'],
689
698
  voiceOutput: ['off', 'on_request', 'always'],
690
699
  voiceWhere: ['auto', 'device', 'server'],
691
700
  themeVariant: APP_THEME_VARIANTS,
@@ -843,14 +852,9 @@ export function documentShapeProblems(document) {
843
852
  if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
844
853
  at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
845
854
  }
846
- // Its voice (VOICE.md VO-41); hands-free refused until it is built (VO-12).
855
+ // Its voice (VOICE.md VO-41, VO-12).
847
856
  mapping(ui.voice, 'interface.voice', voice => {
848
- if (voice.input === 'hands_free') {
849
- at('interface.voice.input', HANDS_FREE_NOT_BUILT);
850
- }
851
- else {
852
- oneOf(voice.input, ENUMS.voiceInput, 'interface.voice.input');
853
- }
857
+ oneOf(voice.input, ENUMS.voiceInput, 'interface.voice.input');
854
858
  oneOf(voice.output, ENUMS.voiceOutput, 'interface.voice.output');
855
859
  oneOf(voice.where, ENUMS.voiceWhere, 'interface.voice.where');
856
860
  });
@@ -383,7 +383,7 @@ export function defineDatalayerAppElement(options = {}) {
383
383
  // A public application, as a visitor without an account (D-07):
384
384
  // an example kept warm or an address open to visitors, run on the
385
385
  // visitors' runtime; a decision framed; a refusal said.
386
- const visitor = await readVisitorApp({ api: this.api(), app });
386
+ const visitor = await readVisitorApp({ api: this.agents(), app });
387
387
  if (visitor.kind === 'decision') {
388
388
  return { kind: 'frame', src: frameSrcOf(this.origin(), visitor.app) };
389
389
  }
@@ -408,11 +408,22 @@ export function defineDatalayerAppElement(options = {}) {
408
408
  }
409
409
  return this.spoken;
410
410
  }
411
- /** Datalayer's services: the `api` attribute, else the platform's, else prod1's. */
411
+ /**
412
+ * ai-agents, where a visitor's address is read and beside which the
413
+ * visitors' runtime and ai-inference answer: the `api` attribute, else
414
+ * the platform's, else r1's. Not Spacer's address (`api()`), which lives
415
+ * on another plane.
416
+ */
417
+ agents() {
418
+ return (this.getAttribute('api') ||
419
+ coreStore.getState().configuration?.aiAgentsUrl ||
420
+ DEFAULT_API).replace(/\/+$/, '');
421
+ }
422
+ /** Spacer, where an application is read with its embed token: the `api` attribute, else the platform's, else prod1's. */
412
423
  api() {
413
424
  return (this.getAttribute('api') ||
414
425
  coreStore.getState().configuration?.spacerUrl ||
415
- DEFAULT_API).replace(/\/+$/, '');
426
+ 'https://prod1.datalayer.run').replace(/\/+$/, '');
416
427
  }
417
428
  async load() {
418
429
  const turn = ++this.loading;
@@ -32,7 +32,7 @@ export declare const EMBED_ATTRIBUTES: {
32
32
  readonly theme: "light, dark or auto (the visitor’s system); auto by default.";
33
33
  readonly font: "A CSS font family for the application’s text; Inter, then the system’s sans-serif, by default.";
34
34
  readonly server: "An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.";
35
- readonly api: "Datalayer’s API, where an application is read with its token; the platform’s by default.";
35
+ readonly api: "Datalayer’s API: where an application is read with its token (Spacer), and where a visitor’s address is read (ai-agents); the platform’s by default.";
36
36
  readonly height: "The height of an inline application, in pixels; 640 by default.";
37
37
  readonly resume: "true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.";
38
38
  readonly language: "The language the visitor reads the application in, as BCP 47 tags it (fr, pt-BR): its translation into it, and the chat’s own words; the visitor’s browser’s by default.";
@@ -23,7 +23,7 @@ export const EMBED_ATTRIBUTES = {
23
23
  theme: 'light, dark or auto (the visitor’s system); auto by default.',
24
24
  font: 'A CSS font family for the application’s text; Inter, then the system’s sans-serif, by default.',
25
25
  server: 'An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.',
26
- api: 'Datalayer’s API, where an application is read with its token; the platform’s by default.',
26
+ api: 'Datalayer’s API: where an application is read with its token (Spacer), and where a visitor’s address is read (ai-agents); the platform’s by default.',
27
27
  height: 'The height of an inline application, in pixels; 640 by default.',
28
28
  resume: 'true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.',
29
29
  language: 'The language the visitor reads the application in, as BCP 47 tags it (fr, pt-BR): its translation into it, and the chat’s own words; the visitor’s browser’s by default.',
@@ -42,8 +42,15 @@ export declare const VISITORS_PATH = "/api/runtime-visitors";
42
42
  export declare const VISITOR_EXAMPLES: readonly string[];
43
43
  /** The key of an application at its address, as a visitor's token names it. */
44
44
  export declare const AT_PREFIX = "at:";
45
- /** Datalayer's services, where the element names none (`api`). */
46
- export declare const DEFAULT_API = "https://prod1.datalayer.run";
45
+ /**
46
+ * Datalayer's services for a visitor, where the element names none (`api`)
47
+ * and the platform's configuration says none: ai-agents, where an address is
48
+ * read, and beside it the visitors' runtime and ai-inference, which mints the
49
+ * visitor's token. Not Spacer's address, which reads an application with its
50
+ * embed token: the two live on different planes (ai-agents on r1, Spacer on
51
+ * prod1), found 15 October 2026 on datalayer.ai's embed.
52
+ */
53
+ export declare const DEFAULT_API = "https://r1.datalayer.run";
47
54
  /** An application's id on Datalayer: a ULID. */
48
55
  export declare const isAppUid: (value: string) => boolean;
49
56
  /** What `app` with no token resolves to. */
@@ -14,8 +14,15 @@ export const VISITOR_EXAMPLES = [
14
14
  ];
15
15
  /** The key of an application at its address, as a visitor's token names it. */
16
16
  export const AT_PREFIX = 'at:';
17
- /** Datalayer's services, where the element names none (`api`). */
18
- export const DEFAULT_API = 'https://prod1.datalayer.run';
17
+ /**
18
+ * Datalayer's services for a visitor, where the element names none (`api`)
19
+ * and the platform's configuration says none: ai-agents, where an address is
20
+ * read, and beside it the visitors' runtime and ai-inference, which mints the
21
+ * visitor's token. Not Spacer's address, which reads an application with its
22
+ * embed token: the two live on different planes (ai-agents on r1, Spacer on
23
+ * prod1), found 15 October 2026 on datalayer.ai's embed.
24
+ */
25
+ export const DEFAULT_API = 'https://r1.datalayer.run';
19
26
  /** An application's id on Datalayer: a ULID. */
20
27
  export const isAppUid = (value) => /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
21
28
  /** An address, as ai-agents spells one. */
@@ -1700,7 +1700,10 @@ export default function ChatView({ workspace }) {
1700
1700
  footerExtras: promptActions.length > 0 || (voice && voice.input !== 'off') ? (_jsxs(_Fragment, { children: [promptActions.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.promptAction, props: { workspace } })) : null, voice && voice.input !== 'off' ? (_jsx(ComposerVoice, { voice: voice, input: draft, setInput: setDraft, disabled: chatDisabled,
1701
1701
  // The chat's own speaker, never a second one: the microphone
1702
1702
  // opening stops it, and *Stop speaking* while it speaks.
1703
- speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(), onInterimChange: setVoiceInterim,
1703
+ speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(),
1704
+ // Hands-free (VO-12, VO-13): the run in progress, stopped when
1705
+ // the person speaks over it.
1706
+ running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterimChange: setVoiceInterim,
1704
1707
  // The menu offers when answers are heard where the
1705
1708
  // application lets them be (VO-21, VO-30).
1706
1709
  answers: Boolean(config?.voice?.output &&
@@ -2968,6 +2968,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2968
2968
  const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
2969
2969
  useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
2970
2970
  const speakingNow = voiceSpeaking || ownVoice.state.speaking;
2971
+ // Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
2972
+ const [interruptedIds, setInterruptedIds] = useState(() => new Set());
2971
2973
  const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
2972
2974
  const readAnswerAloud = onReadAloud ?? (ownVoice.speaker ? ownVoice.read : undefined);
2973
2975
  const handleSend = useCallback(async (messageOverride,
@@ -3724,10 +3726,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3724
3726
  .reverse()
3725
3727
  .find((item) => !isToolCallMessage(item) && item.role === 'assistant')?.id
3726
3728
  : undefined;
3727
- const readAloudFooter = readAnswerAloud
3729
+ const readAloudFooter = readAnswerAloud || interruptedIds.size
3728
3730
  ? (message) => {
3729
3731
  const text = getMessageText(message);
3730
- return readsAloud(message, text, writingId) ? (_jsx(ReadAloud, { onRead: () => readAnswerAloud(text) })) : null;
3732
+ return (_jsxs(_Fragment, { children: [interruptedIds.has(message.id) ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontStyle: 'italic' }, "data-interrupted": "", children: "interrupted" })) : null, readAnswerAloud && readsAloud(message, text, writingId) ? (_jsx(ReadAloud, { onRead: () => readAnswerAloud(text) })) : null] }));
3731
3733
  }
3732
3734
  : undefined;
3733
3735
  const messagesContent = children ? (children) : (_jsxs(Box, { display: "flex", flexDirection: "column", minHeight: 0,
@@ -3745,7 +3747,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3745
3747
  // so every chat with a headless notebook gets the same transcript.
3746
3748
  notebookToolSurfacesId: notebookToolSurfacesId, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, renderMessageFooter: readAloudFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }), trailingContent] }));
3747
3749
  // The microphone, in the composer's footer (VO-10, VO-11).
3748
- const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking, onInterimChange: setVoiceInterim,
3750
+ const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking,
3751
+ // Hands-free (VO-12, VO-13): the run in progress, stopped when the
3752
+ // person speaks over it, and the answer marked interrupted.
3753
+ running: isLoading, onStopRun: handleStop, onInterrupted: () => {
3754
+ if (writingId) {
3755
+ setInterruptedIds(ids => new Set([...ids, writingId]));
3756
+ }
3757
+ }, onInterimChange: setVoiceInterim,
3749
3758
  // The menu offers when answers are heard where the application
3750
3759
  // lets them be (VO-21, VO-30).
3751
3760
  answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
@@ -1,31 +1,3 @@
1
- /**
2
- * Agent runtime variants: where an agent's loop runs, and what runs it.
3
- *
4
- * There have always been two ways to start an agent — a local agent-runtimes
5
- * server and a cloud one — and they were named after the service each talked
6
- * to (`local-agent-runtimes`, `backend-services`). That naming held while both
7
- * ran the same framework. A browser agent breaks it: it talks to no service at
8
- * all, and it runs a different framework.
9
- *
10
- * So a variant names the two things that actually vary, in that order:
11
- *
12
- * <location>-<harness> browser-vercelai
13
- * local-pydanticai
14
- * cloud-pydanticai
15
- *
16
- * The point of splitting them is that most code cares about exactly one half.
17
- * Anything about *reaching* a runtime — a URL, a lifecycle, a sandbox — is a
18
- * question of location. Anything about *turning the loop* — the SDK, the tool
19
- * protocol, the streaming format — is a question of harness. Code that branches
20
- * on the whole variant string has to be edited every time either axis gains a
21
- * value; code that asks {@link locationOf} or {@link harnessOf} does not.
22
- *
23
- * That is what makes the next ones cheap. `cloud-ax` is a new harness against
24
- * an existing location, so only harness-specific code changes; `browser-ax` is
25
- * a combination of two things that already exist.
26
- *
27
- * @module runtimes/variants
28
- */
29
1
  import type { Agentspec } from '../types/agentspecs';
30
2
  /** Where the agent's loop runs. */
31
3
  export type AgentLocation = 'browser' | 'local' | 'cloud';
@@ -58,6 +30,27 @@ export declare const DEFAULT_AGENT_RUNTIME_VARIANT: AgentRuntimeVariant;
58
30
  export declare const SPEC_HARNESS_BY_HARNESS: Record<AgentHarness, string>;
59
31
  /** The reverse, for reading an agentspec's declared harness. */
60
32
  export declare const HARNESS_BY_SPEC_HARNESS: Record<string, AgentHarness>;
33
+ /** The one catalogue harness whose command an organization's own spec supplies. */
34
+ export declare const CUSTOM_HARNESS = "custom";
35
+ /**
36
+ * Why no variant runs an agent on this harness, in a sentence, or undefined
37
+ * when one does.
38
+ *
39
+ * A harness of the catalogue (`agentspecs/harnesses`, SHIELD.md SH-01) — Claude
40
+ * Code, Codex, Gemini CLI, goose, OpenCode, OpenHands, fast-agent — is served
41
+ * by the Datalayer runtime through its ACP adapter (`ACPAgent`, SHIELD SH-10):
42
+ * the runtime launches the harness as its subprocess, so the runtime's
43
+ * locations run it and the browser, which has no process to launch, does not.
44
+ * Two are refused: `custom`, which names no command in the shared catalogue
45
+ * (an organization's own harness spec names it), and a harness neither built
46
+ * in nor of the catalogue. Nothing runs such an agent in its place.
47
+ */
48
+ export declare function unservedHarnessProblem(spec: Pick<Agentspec, 'id' | 'harness'>): string | undefined;
49
+ /**
50
+ * Whether a harness id names a harness the Datalayer runtime serves through
51
+ * its ACP adapter: one of the catalogue, launchable, so not `custom`.
52
+ */
53
+ export declare function isServedCatalogueHarness(harness: string): boolean;
61
54
  /**
62
55
  * The names the two original ways to start an agent went by.
63
56
  *
@@ -113,7 +106,7 @@ export declare function variantForLocation(location: AgentLocation): AgentRuntim
113
106
  * An agentspec says which framework it *prefers* — `harness:` is where the
114
107
  * agent normally runs, not a demand — and a variant says which framework a
115
108
  * location can actually provide. Where they disagree the location wins, which
116
- * is why the browser accepts any spec:
109
+ * is why the browser accepts any spec built for a framework:
117
110
  *
118
111
  * harness: pydantic-ai + browser → runs, with the Vercel AI SDK
119
112
  *
@@ -125,15 +118,22 @@ export declare function variantForLocation(location: AgentLocation): AgentRuntim
125
118
  * The remote locations are stricter: a spec built for the browser harness
126
119
  * assumes frontend tools and no server, so running it on a runtime would be
127
120
  * running something else.
121
+ *
122
+ * A harness of the catalogue (Claude Code, Codex, …) is a program, not a
123
+ * framework: the Datalayer runtime launches it as its subprocess and serves it
124
+ * through its ACP adapter (SHIELD SH-10), so the runtime's locations run it
125
+ * and the browser does not — the page has no process to launch, and nothing
126
+ * is run in the harness's place.
128
127
  */
129
128
  export declare function variantSupportsSpecHarness(variant: AgentRuntimeVariant, specHarness: string | undefined | null): boolean;
130
129
  /**
131
130
  * The variants that could run an agent with this declared harness.
132
131
  *
133
132
  * What a host offers a person choosing where to run something. The browser is
134
- * always among them — it can run anything, because it overrides the harness —
135
- * while an agent built for the browser is not offered a remote runtime that
136
- * would be running something else.
133
+ * among them for any framework — it can run those, because it overrides the
134
+ * harness — while an agent built for the browser is not offered a remote
135
+ * runtime that would be running something else, and an agent on a catalogue
136
+ * harness is offered the runtimes alone, which launch it.
137
137
  */
138
138
  export declare function variantsForSpecHarness(specHarness: string | undefined | null): AgentRuntimeVariant[];
139
139
  /**
@@ -147,19 +147,24 @@ export declare function variantsForSpecHarness(specHarness: string | undefined |
147
147
  */
148
148
  export declare function legacyTargetOf(variant: AgentRuntimeVariant): RuntimeCreationTarget;
149
149
  /**
150
- * The harness a spec asks for, in the variant spelling.
150
+ * The harness a spec asks for: a framework in the variant spelling
151
+ * (`pydanticai`, `vercelai`), or a catalogue harness by its id (`claude-code`).
151
152
  *
152
153
  * Defaulted rather than optional: every agent is run by something, and a spec
153
154
  * that says nothing is run by the framework that has always run it. That
154
- * default is what lets every spec written before this field stay correct.
155
+ * default is what lets every spec written before this field stay correct. A
156
+ * catalogue harness keeps its own id, because no variant spells it: the
157
+ * variant that serves it is the Datalayer runtime's, whose own loop is
158
+ * pydantic-ai's and whose ACP adapter turns the harness's.
155
159
  */
156
- export declare function specHarnessOf(spec: Pick<Agentspec, 'harness'>): AgentHarness;
160
+ export declare function specHarnessOf(spec: Pick<Agentspec, 'id' | 'harness'>): string;
157
161
  /**
158
162
  * The variants that could run this agent.
159
163
  *
160
164
  * What a host offers when someone picks where to run something: an agent built
161
165
  * for the browser harness should not be offered a cloud runtime that cannot
162
- * turn its loop, and a server-side agent should not be offered the page.
166
+ * turn its loop, a server-side agent should not be offered the page, and an
167
+ * agent on a catalogue harness is offered the runtimes that launch it.
163
168
  */
164
169
  export declare function variantsForSpec(spec: Pick<Agentspec, 'harness'>): AgentRuntimeVariant[];
165
170
  /**
@@ -169,4 +174,4 @@ export declare function variantsForSpec(spec: Pick<Agentspec, 'harness'>): Agent
169
174
  * variant that can run it at all — so a `harness: vercel-ai` agent lands in
170
175
  * the browser without every caller having to know that is where it belongs.
171
176
  */
172
- export declare function defaultVariantForSpec(spec: Pick<Agentspec, 'harness'>): AgentRuntimeVariant;
177
+ export declare function defaultVariantForSpec(spec: Pick<Agentspec, 'id' | 'harness'>): AgentRuntimeVariant;
@@ -2,6 +2,40 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /*
6
+ * Copyright (c) 2023-2026 Datalayer, Inc.
7
+ *
8
+ * MIT License
9
+ */
10
+ /**
11
+ * Agent runtime variants: where an agent's loop runs, and what runs it.
12
+ *
13
+ * There have always been two ways to start an agent — a local agent-runtimes
14
+ * server and a cloud one — and they were named after the service each talked
15
+ * to (`local-agent-runtimes`, `backend-services`). That naming held while both
16
+ * ran the same framework. A browser agent breaks it: it talks to no service at
17
+ * all, and it runs a different framework.
18
+ *
19
+ * So a variant names the two things that actually vary, in that order:
20
+ *
21
+ * <location>-<harness> browser-vercelai
22
+ * local-pydanticai
23
+ * cloud-pydanticai
24
+ *
25
+ * The point of splitting them is that most code cares about exactly one half.
26
+ * Anything about *reaching* a runtime — a URL, a lifecycle, a sandbox — is a
27
+ * question of location. Anything about *turning the loop* — the SDK, the tool
28
+ * protocol, the streaming format — is a question of harness. Code that branches
29
+ * on the whole variant string has to be edited every time either axis gains a
30
+ * value; code that asks {@link locationOf} or {@link harnessOf} does not.
31
+ *
32
+ * That is what makes the next ones cheap. `cloud-ax` is a new harness against
33
+ * an existing location, so only harness-specific code changes; `browser-ax` is
34
+ * a combination of two things that already exist.
35
+ *
36
+ * @module runtimes/variants
37
+ */
38
+ import { getHarness } from '../specs/harnesses';
5
39
  /**
6
40
  * The combinations that exist today.
7
41
  *
@@ -32,6 +66,44 @@ export const HARNESS_BY_SPEC_HARNESS = {
32
66
  'pydantic-ai': 'pydanticai',
33
67
  'vercel-ai': 'vercelai',
34
68
  };
69
+ /** The one catalogue harness whose command an organization's own spec supplies. */
70
+ export const CUSTOM_HARNESS = 'custom';
71
+ /**
72
+ * Why no variant runs an agent on this harness, in a sentence, or undefined
73
+ * when one does.
74
+ *
75
+ * A harness of the catalogue (`agentspecs/harnesses`, SHIELD.md SH-01) — Claude
76
+ * Code, Codex, Gemini CLI, goose, OpenCode, OpenHands, fast-agent — is served
77
+ * by the Datalayer runtime through its ACP adapter (`ACPAgent`, SHIELD SH-10):
78
+ * the runtime launches the harness as its subprocess, so the runtime's
79
+ * locations run it and the browser, which has no process to launch, does not.
80
+ * Two are refused: `custom`, which names no command in the shared catalogue
81
+ * (an organization's own harness spec names it), and a harness neither built
82
+ * in nor of the catalogue. Nothing runs such an agent in its place.
83
+ */
84
+ export function unservedHarnessProblem(spec) {
85
+ const harness = spec.harness ?? '';
86
+ if (!harness || harness in HARNESS_BY_SPEC_HARNESS) {
87
+ return undefined;
88
+ }
89
+ const catalogued = getHarness(harness);
90
+ if (!catalogued) {
91
+ return `The agent “${spec.id}” runs on “${harness}”, which is no harness.`;
92
+ }
93
+ if (harness === CUSTOM_HARNESS) {
94
+ return `The agent “${spec.id}” runs on the harness “custom”, which names no command: an organization's own harness spec names it, and is never typed into a form.`;
95
+ }
96
+ return undefined;
97
+ }
98
+ /**
99
+ * Whether a harness id names a harness the Datalayer runtime serves through
100
+ * its ACP adapter: one of the catalogue, launchable, so not `custom`.
101
+ */
102
+ export function isServedCatalogueHarness(harness) {
103
+ return (harness !== CUSTOM_HARNESS &&
104
+ !(harness in HARNESS_BY_SPEC_HARNESS) &&
105
+ getHarness(harness) !== undefined);
106
+ }
35
107
  const VARIANT_BY_LEGACY_TARGET = {
36
108
  'backend-services': 'cloud-pydanticai',
37
109
  'local-agent-runtimes': 'local-pydanticai',
@@ -104,7 +176,7 @@ export function variantForLocation(location) {
104
176
  * An agentspec says which framework it *prefers* — `harness:` is where the
105
177
  * agent normally runs, not a demand — and a variant says which framework a
106
178
  * location can actually provide. Where they disagree the location wins, which
107
- * is why the browser accepts any spec:
179
+ * is why the browser accepts any spec built for a framework:
108
180
  *
109
181
  * harness: pydantic-ai + browser → runs, with the Vercel AI SDK
110
182
  *
@@ -116,8 +188,19 @@ export function variantForLocation(location) {
116
188
  * The remote locations are stricter: a spec built for the browser harness
117
189
  * assumes frontend tools and no server, so running it on a runtime would be
118
190
  * running something else.
191
+ *
192
+ * A harness of the catalogue (Claude Code, Codex, …) is a program, not a
193
+ * framework: the Datalayer runtime launches it as its subprocess and serves it
194
+ * through its ACP adapter (SHIELD SH-10), so the runtime's locations run it
195
+ * and the browser does not — the page has no process to launch, and nothing
196
+ * is run in the harness's place.
119
197
  */
120
198
  export function variantSupportsSpecHarness(variant, specHarness) {
199
+ if (specHarness && !(specHarness in HARNESS_BY_SPEC_HARNESS)) {
200
+ // A catalogue harness runs where a runtime can launch it; `custom` and a
201
+ // harness that is none run nowhere.
202
+ return (isServedCatalogueHarness(specHarness) && needsRuntimeService(variant));
203
+ }
121
204
  if (runsInBrowser(variant)) {
122
205
  return true;
123
206
  }
@@ -131,9 +214,10 @@ export function variantSupportsSpecHarness(variant, specHarness) {
131
214
  * The variants that could run an agent with this declared harness.
132
215
  *
133
216
  * What a host offers a person choosing where to run something. The browser is
134
- * always among them — it can run anything, because it overrides the harness —
135
- * while an agent built for the browser is not offered a remote runtime that
136
- * would be running something else.
217
+ * among them for any framework — it can run those, because it overrides the
218
+ * harness — while an agent built for the browser is not offered a remote
219
+ * runtime that would be running something else, and an agent on a catalogue
220
+ * harness is offered the runtimes alone, which launch it.
137
221
  */
138
222
  export function variantsForSpecHarness(specHarness) {
139
223
  return AGENT_RUNTIME_VARIANTS.filter(variant => variantSupportsSpecHarness(variant, specHarness));
@@ -153,21 +237,32 @@ export function legacyTargetOf(variant) {
153
237
  : 'local-agent-runtimes';
154
238
  }
155
239
  /**
156
- * The harness a spec asks for, in the variant spelling.
240
+ * The harness a spec asks for: a framework in the variant spelling
241
+ * (`pydanticai`, `vercelai`), or a catalogue harness by its id (`claude-code`).
157
242
  *
158
243
  * Defaulted rather than optional: every agent is run by something, and a spec
159
244
  * that says nothing is run by the framework that has always run it. That
160
- * default is what lets every spec written before this field stay correct.
245
+ * default is what lets every spec written before this field stay correct. A
246
+ * catalogue harness keeps its own id, because no variant spells it: the
247
+ * variant that serves it is the Datalayer runtime's, whose own loop is
248
+ * pydantic-ai's and whose ACP adapter turns the harness's.
161
249
  */
162
250
  export function specHarnessOf(spec) {
163
- return HARNESS_BY_SPEC_HARNESS[spec.harness ?? ''] ?? 'pydanticai';
251
+ const problem = unservedHarnessProblem(spec);
252
+ if (problem) {
253
+ // Never pydantic-ai in its place: said, and refused (SHIELD SH-01).
254
+ throw new Error(problem);
255
+ }
256
+ const harness = spec.harness ?? '';
257
+ return HARNESS_BY_SPEC_HARNESS[harness] ?? (harness || 'pydanticai');
164
258
  }
165
259
  /**
166
260
  * The variants that could run this agent.
167
261
  *
168
262
  * What a host offers when someone picks where to run something: an agent built
169
263
  * for the browser harness should not be offered a cloud runtime that cannot
170
- * turn its loop, and a server-side agent should not be offered the page.
264
+ * turn its loop, a server-side agent should not be offered the page, and an
265
+ * agent on a catalogue harness is offered the runtimes that launch it.
171
266
  */
172
267
  export function variantsForSpec(spec) {
173
268
  return variantsForSpecHarness(spec.harness);
@@ -183,5 +278,10 @@ export function defaultVariantForSpec(spec) {
183
278
  if (variantSupportsSpecHarness(DEFAULT_AGENT_RUNTIME_VARIANT, spec.harness)) {
184
279
  return DEFAULT_AGENT_RUNTIME_VARIANT;
185
280
  }
186
- return variantsForSpec(spec)[0] ?? DEFAULT_AGENT_RUNTIME_VARIANT;
281
+ const [first] = variantsForSpec(spec);
282
+ if (!first) {
283
+ throw new Error(unservedHarnessProblem(spec) ??
284
+ `No variant runs the agent “${spec.id}” on “${spec.harness}”.`);
285
+ }
286
+ return first;
187
287
  }
@@ -2522,8 +2522,8 @@ export const APPSPEC_SCHEMA = {
2522
2522
  type: 'string',
2523
2523
  },
2524
2524
  VoiceInput: {
2525
- description: 'How a person talks to the application (VOICE.md VO-10).\n\n`hands_free` (VO-12) is not one yet: it is refused in a sentence until it\nis built, never taken for click-to-talk.',
2526
- enum: ['off', 'push_to_talk'],
2525
+ description: 'How a person talks to the application (VOICE.md VO-10, VO-12).\n\n`push_to_talk`: the microphone in the composer, clicked or held.\n`hands_free`: a conversation by voice — the composer offers *Talk with\nit*; a turn ends after a short silence and is sent, the answer is said,\nand listening goes on; speaking over the agent stops its voice (VO-13).\nHands-free implies answers said as they are written (`output: always`).',
2526
+ enum: ['off', 'push_to_talk', 'hands_free'],
2527
2527
  title: 'VoiceInput',
2528
2528
  type: 'string',
2529
2529
  },
@@ -8,13 +8,17 @@
8
8
  */
9
9
  import type { EnvvarSpec } from '../types';
10
10
  export declare const ALPHAVANTAGE_API_KEY_SPEC_0_0_1: EnvvarSpec;
11
+ export declare const ANTHROPIC_API_KEY_SPEC_0_0_1: EnvvarSpec;
11
12
  export declare const EARTHDATA_PASSWORD_SPEC_0_0_1: EnvvarSpec;
12
13
  export declare const EARTHDATA_USERNAME_SPEC_0_0_1: EnvvarSpec;
14
+ export declare const GEMINI_API_KEY_SPEC_0_0_1: EnvvarSpec;
13
15
  export declare const GITHUB_TOKEN_SPEC_0_0_1: EnvvarSpec;
14
16
  export declare const GOOGLE_OAUTH_CLIENT_ID_SPEC_0_0_1: EnvvarSpec;
15
17
  export declare const GOOGLE_OAUTH_CLIENT_SECRET_SPEC_0_0_1: EnvvarSpec;
16
18
  export declare const HF_TOKEN_SPEC_0_0_1: EnvvarSpec;
17
19
  export declare const KAGGLE_API_TOKEN_SPEC_0_0_1: EnvvarSpec;
20
+ export declare const LLM_API_KEY_SPEC_0_0_1: EnvvarSpec;
21
+ export declare const OPENAI_API_KEY_SPEC_0_0_1: EnvvarSpec;
18
22
  export declare const SLACK_BOT_TOKEN_SPEC_0_0_1: EnvvarSpec;
19
23
  export declare const SLACK_CHANNEL_IDS_SPEC_0_0_1: EnvvarSpec;
20
24
  export declare const SLACK_TEAM_ID_SPEC_0_0_1: EnvvarSpec;