@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.
- package/dist-embed/chunks/{ChatView-C0PWJ_wj.js → ChatView-C_NOLqwI.js} +3 -3
- package/dist-embed/datalayer-app-main.js +83 -83
- package/lib/apps/apps/appspec.d.ts +0 -5
- package/lib/apps/apps/appspec.js +1 -6
- package/lib/apps/apps/checks.js +13 -9
- package/lib/apps/embed/element.js +14 -3
- package/lib/apps/embed/embedConfig.d.ts +1 -1
- package/lib/apps/embed/embedConfig.js +1 -1
- package/lib/apps/embed/visitorApp.d.ts +9 -2
- package/lib/apps/embed/visitorApp.js +9 -2
- package/lib/apps/plugins/chat/ChatView.js +4 -1
- package/lib/chat/base/ChatBase.js +12 -3
- package/lib/runtimes/variants.d.ts +42 -37
- package/lib/runtimes/variants.js +109 -9
- package/lib/specs/appspecSchema.js +2 -2
- package/lib/specs/envvars.d.ts +4 -0
- package/lib/specs/envvars.js +44 -0
- package/lib/specs/harnesses.d.ts +22 -0
- package/lib/specs/harnesses.js +372 -0
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/voices.js +42 -0
- package/lib/types/agentspecs.d.ts +70 -1
- package/lib/voice/ComposerVoice.d.ts +10 -2
- package/lib/voice/ComposerVoice.js +10 -2
- package/lib/voice/HandsFree.d.ts +55 -0
- package/lib/voice/HandsFree.js +211 -0
- package/lib/voice/chatVoice.d.ts +2 -1
- package/lib/voice/chatVoice.js +5 -3
- package/lib/voice/handsFree.d.ts +58 -0
- package/lib/voice/handsFree.js +48 -0
- package/lib/voice/index.d.ts +10 -2
- package/lib/voice/index.js +2 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_agents.py +56 -0
- package/scripts/codegen/generate_envvars.py +3 -2
- package/scripts/codegen/generate_harnesses.py +329 -0
- 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. */
|
package/lib/apps/apps/appspec.js
CHANGED
|
@@ -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),
|
package/lib/apps/apps/checks.js
CHANGED
|
@@ -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,
|
|
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
|
|
855
|
+
// Its voice (VOICE.md VO-41, VO-12).
|
|
847
856
|
mapping(ui.voice, 'interface.voice', voice => {
|
|
848
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
46
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
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?.(),
|
|
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,
|
|
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
|
-
*
|
|
135
|
-
* while an agent built for the browser is not offered a remote
|
|
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
|
|
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'>):
|
|
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,
|
|
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;
|
package/lib/runtimes/variants.js
CHANGED
|
@@ -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
|
-
*
|
|
135
|
-
* while an agent built for the browser is not offered a remote
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
},
|
package/lib/specs/envvars.d.ts
CHANGED
|
@@ -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;
|