@datalayer/agent-runtimes 1.5.59 → 1.5.60

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.
@@ -1720,7 +1720,9 @@ export default function ChatView({ workspace }) {
1720
1720
  playbackLevel: () => controlsRef.current?.voiceLevel?.() ?? 0,
1721
1721
  // Hands-free (VO-12, VO-13): the run in progress, stopped when
1722
1722
  // the person speaks over it, and the answer marked interrupted.
1723
- running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterrupted: () => controlsRef.current?.markInterrupted(), onInterimChange: setVoiceInterim,
1723
+ running: busy,
1724
+ // Spoken over, the application's session stays open (VO-46).
1725
+ onStopRun: () => (controlsRef.current?.interrupt ?? controlsRef.current?.stop)?.(), onInterrupted: () => controlsRef.current?.markInterrupted(), onInterimChange: setVoiceInterim,
1724
1726
  // The menu offers when answers are heard where the
1725
1727
  // application lets them be (VO-21, VO-30).
1726
1728
  answers: Boolean(config?.voice?.output &&
@@ -10,6 +10,7 @@ import { ButtonGlow } from './display/ButtonGlow';
10
10
  import { useViewportDrag } from './useViewportDrag';
11
11
  import { disabledChatViewModes, resolveMountPoint } from './viewModes';
12
12
  import { AssistantStage } from './assistant/AssistantStage';
13
+ import { ASSISTANT_CLEARANCE_VAR } from './assistant/clearance';
13
14
  import { decisionsAskerAt } from './assistant/decisions';
14
15
  import { DecisionAsk } from './assistant/DecisionAsk';
15
16
  import { useSpokenAnswers } from '../voice/useSpokenAnswers';
@@ -854,6 +855,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
854
855
  : undefined;
855
856
  /* The window itself; where it goes is decided below. */
856
857
  const chatWindow = (_jsxs(Box, { ref: popupRef, className: className, sx: {
858
+ // Its own conversation stands beside the character, never under it:
859
+ // its prompt's controls keep their own padding (T-27).
860
+ ...(assistantShown ? { [ASSISTANT_CLEARANCE_VAR]: 'initial' } : {}),
857
861
  position: viewMode === 'panel' && !isMobile && (panelInFlow || dockedInMount)
858
862
  ? 'relative'
859
863
  : 'fixed',
@@ -59,7 +59,7 @@ import { useVoiceSettings } from '../../voice/useVoiceSettings';
59
59
  import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
60
60
  import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
61
61
  import { saidMessageId, useSaidSentence } from '../../voice/captions';
62
- import { lineToSay } from '../../voice/sayAloud';
62
+ import { isVoiceInterrupted, lineToSay, } from '../../voice/sayAloud';
63
63
  import { isFloatingChatViewMode } from '../viewModes';
64
64
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
65
65
  import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
@@ -2986,6 +2986,16 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2986
2986
  */
2987
2987
  const [voiceSaid, setVoiceSaid] = useState('');
2988
2988
  voiceSayRef.current = activity => {
2989
+ if (isVoiceInterrupted(activity)) {
2990
+ // Spoken over, the runtime says (VO-46): its voice stops.
2991
+ if (onStopSpeaking) {
2992
+ onStopSpeaking();
2993
+ }
2994
+ else if (ownVoice.speaker) {
2995
+ ownVoice.stop();
2996
+ }
2997
+ return;
2998
+ }
2989
2999
  const line = lineToSay(activity);
2990
3000
  if (!line) {
2991
3001
  return;
@@ -3251,7 +3261,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3251
3261
  applyServerApprovalDecision,
3252
3262
  ]);
3253
3263
  // ---- handleStop ----
3254
- const handleStop = useCallback(() => {
3264
+ // Spoken over (VO-46, VO-13), an application's session is told so and
3265
+ // stays open; Stop leaves it to be resumed.
3266
+ const stopTurn = useCallback((spokenOver) => {
3255
3267
  stoppedRef.current = true;
3256
3268
  abortControllerRef.current?.abort();
3257
3269
  // Best-effort cancellation without tearing down adapter/session.
@@ -3262,7 +3274,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3262
3274
  adapter.stopGeneration();
3263
3275
  }
3264
3276
  // Also tell the backend to stop (server-side cancellation).
3265
- if (typeof adapter.terminateSession === 'function') {
3277
+ if (spokenOver && typeof adapter.interruptSession === 'function') {
3278
+ void adapter.interruptSession().catch(() => { });
3279
+ }
3280
+ else if (typeof adapter.terminateSession === 'function') {
3266
3281
  void adapter.terminateSession().catch(() => { });
3267
3282
  }
3268
3283
  else if (typeof adapter.terminateAgent === 'function') {
@@ -3314,6 +3329,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3314
3329
  protocol?.authToken,
3315
3330
  protocol?.agentId,
3316
3331
  ]);
3332
+ const handleStop = useCallback(() => stopTurn(false), [stopTurn]);
3333
+ const handleSpokenOver = useCallback(() => stopTurn(true), [stopTurn]);
3317
3334
  /*
3318
3335
  * How a message's display presses what it offered (RU-4,
3319
3336
  * RU-W): with the token of the offer, as the run's press — which
@@ -3390,6 +3407,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3390
3407
  voiceLevel: playbackLevel,
3391
3408
  // The answer spoken over, marked for a host's own composer (VO-13).
3392
3409
  markInterrupted,
3410
+ // Spoken over: the run ends, the application's session stays open (VO-46).
3411
+ interrupt: handleSpokenOver,
3393
3412
  });
3394
3413
  return () => {
3395
3414
  onSendReady(null);
@@ -3400,6 +3419,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3400
3419
  onSendMessage,
3401
3420
  handleSend,
3402
3421
  handleStop,
3422
+ handleSpokenOver,
3403
3423
  handleNewChat,
3404
3424
  stopSpeaking,
3405
3425
  playbackLevel,
@@ -3808,7 +3828,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3808
3828
  playbackLevel: playbackLevel,
3809
3829
  // Hands-free (VO-12, VO-13): the run in progress, stopped when the
3810
3830
  // person speaks over it, and the answer marked interrupted.
3811
- running: isLoading, onStopRun: handleStop, onInterrupted: markInterrupted, onInterimChange: setVoiceInterim,
3831
+ running: isLoading,
3832
+ // Spoken over, the application's session stays open (VO-46).
3833
+ onStopRun: handleSpokenOver, onInterrupted: markInterrupted, onInterimChange: setVoiceInterim,
3812
3834
  // The menu offers when answers are heard where the application
3813
3835
  // lets them be (VO-21, VO-30).
3814
3836
  answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
@@ -151,6 +151,9 @@ export function InputPrompt({ input, setInput, isLoading, kernelStatus, connecti
151
151
  {
152
152
  id: 'session-controls',
153
153
  px: padding,
154
+ // Its right end — the voice controls — is never under
155
+ // the floating assistant in its corner (T-27).
156
+ clearsAssistant: true,
154
157
  py: 0.5,
155
158
  minHeight: 36,
156
159
  bordered: true,
@@ -18,6 +18,19 @@
18
18
  * @module chat/prompt/stack
19
19
  */
20
20
  import type { ReactNode } from 'react';
21
+ /**
22
+ * A theme space step (`px={2}`) as a CSS length, from the theme's scale or
23
+ * else Primer's own.
24
+ */
25
+ export declare function spaceLength(step: number, scale?: readonly (string | number)[]): string;
26
+ /**
27
+ * The right padding of a band that `clearsAssistant`: the floating
28
+ * assistant's clearance while it stands in a bottom corner (what it covers
29
+ * from the window's right edge, `--assistant-clearance`), so that the
30
+ * character is never drawn over the band's controls and never takes their
31
+ * clicks; else the band's own `padding`, a CSS length.
32
+ */
33
+ export declare function clearOfAssistant(padding: string): string;
21
34
  /** One band in a region. */
22
35
  export type PromptStack = {
23
36
  /** Identity, for React and for a host reasoning about which band is which. */
@@ -69,6 +82,15 @@ export type PromptStack = {
69
82
  * showing the page behind it — a pale stripe down the right-hand side.
70
83
  */
71
84
  layout?: 'row' | 'block';
85
+ /**
86
+ * Keep the band's right end clear of the floating assistant while it
87
+ * stands in a bottom corner: its right padding becomes the assistant's
88
+ * clearance (`--assistant-clearance`), so that the character is never
89
+ * drawn over the band's controls and never takes their clicks. For a band
90
+ * that runs to the page's right edge, as the composer's session controls
91
+ * do on an application's page.
92
+ */
93
+ clearsAssistant?: boolean;
72
94
  };
73
95
  export type PromptStacksProps = {
74
96
  /** The bands, top to bottom. */
@@ -1,5 +1,37 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useTheme } from '@primer/react';
2
3
  import { Box } from '@datalayer/primer-addons';
4
+ import { ASSISTANT_CLEARANCE_VAR } from '../assistant/clearance';
5
+ /** Primer's space scale, for a band drawn without a theme. */
6
+ const PRIMER_SPACE = [
7
+ '0',
8
+ '4px',
9
+ '8px',
10
+ '16px',
11
+ '24px',
12
+ '32px',
13
+ '40px',
14
+ '48px',
15
+ '64px',
16
+ ];
17
+ /**
18
+ * A theme space step (`px={2}`) as a CSS length, from the theme's scale or
19
+ * else Primer's own.
20
+ */
21
+ export function spaceLength(step, scale = PRIMER_SPACE) {
22
+ const value = scale[step] ?? PRIMER_SPACE[step] ?? `${step * 8}px`;
23
+ return typeof value === 'number' ? `${value}px` : value;
24
+ }
25
+ /**
26
+ * The right padding of a band that `clearsAssistant`: the floating
27
+ * assistant's clearance while it stands in a bottom corner (what it covers
28
+ * from the window's right edge, `--assistant-clearance`), so that the
29
+ * character is never drawn over the band's controls and never takes their
30
+ * clicks; else the band's own `padding`, a CSS length.
31
+ */
32
+ export function clearOfAssistant(padding) {
33
+ return `var(${ASSISTANT_CLEARANCE_VAR}, ${padding})`;
34
+ }
3
35
  /**
4
36
  * Whether a band would draw anything.
5
37
  *
@@ -12,12 +44,19 @@ function isRendered(stack) {
12
44
  /** Render a region's bands, skipping the ones with nothing to show. */
13
45
  export function PromptStacks({ stacks, disabled = false, }) {
14
46
  const visible = stacks.filter(isRendered);
47
+ const { theme } = useTheme();
15
48
  if (visible.length === 0) {
16
49
  return null;
17
50
  }
18
51
  return (_jsx(_Fragment, { children: visible.map(stack => {
19
52
  const isDisabled = disabled || stack.disabled;
20
- return (_jsx(Box, { "data-prompt-stack": stack.id, "aria-disabled": isDisabled || undefined, px: stack.px ?? 2, py: stack.py ?? 1, minHeight: stack.minHeight, display: stack.layout === 'block' ? undefined : 'flex', alignItems: stack.layout === 'block' ? undefined : 'center', gap: stack.layout === 'block' ? undefined : (stack.gap ?? 2), pt: stack.pt === undefined ? undefined : stack.pt, pb: stack.pb === undefined ? undefined : stack.pb, borderTop: stack.bordered ? '1px solid' : undefined, borderColor: stack.bordered ? 'border.default' : undefined, bg: stack.subtle ? 'canvas.subtle' : undefined, opacity: isDisabled ? 0.5 : undefined, pointerEvents: isDisabled ? 'none' : undefined, children: stack.content }, stack.id));
53
+ const px = stack.px ?? 2;
54
+ const right = Array.isArray(px) ? px[px.length - 1] : px;
55
+ return (_jsx(Box, { "data-prompt-stack": stack.id, "aria-disabled": isDisabled || undefined, px: px, "data-clears-assistant": stack.clearsAssistant ? '' : undefined, sx: stack.clearsAssistant
56
+ ? {
57
+ paddingRight: clearOfAssistant(spaceLength(right, theme?.space)),
58
+ }
59
+ : undefined, py: stack.py ?? 1, minHeight: stack.minHeight, display: stack.layout === 'block' ? undefined : 'flex', alignItems: stack.layout === 'block' ? undefined : 'center', gap: stack.layout === 'block' ? undefined : (stack.gap ?? 2), pt: stack.pt === undefined ? undefined : stack.pt, pb: stack.pb === undefined ? undefined : stack.pb, borderTop: stack.bordered ? '1px solid' : undefined, borderColor: stack.bordered ? 'border.default' : undefined, bg: stack.subtle ? 'canvas.subtle' : undefined, opacity: isDisabled ? 0.5 : undefined, pointerEvents: isDisabled ? 'none' : undefined, children: stack.content }, stack.id));
21
60
  }) }));
22
61
  }
23
62
  export default PromptStacks;
@@ -15,5 +15,5 @@ export type PageTag = {
15
15
  export declare const PAGE_TAGE_NONE: PageTag;
16
16
  export declare const PAGE_TAGS: PageTag[];
17
17
  export declare const PAGE_TAGS_BY_NAME_MAP: Map<PageTagName | undefined, PageTag>;
18
- export declare const PAGE_TAGS_BY_COLOR_MAP: Map<"default" | "green" | "lime" | "blue" | "coral" | "gray" | "indigo" | "orange" | "pink" | "purple" | "red" | "teal" | "yellow" | "lemon", PageTag>;
18
+ export declare const PAGE_TAGS_BY_COLOR_MAP: Map<"blue" | "coral" | "gray" | "green" | "indigo" | "lime" | "orange" | "pink" | "purple" | "red" | "teal" | "yellow" | "default" | "lemon", PageTag>;
19
19
  export {};
@@ -66,6 +66,14 @@ export declare class AGUIAdapter extends BaseProtocolAdapter {
66
66
  * Terminate a running agent thread on the backend
67
67
  */
68
68
  terminateAgent(threadId?: string): Promise<void>;
69
+ /**
70
+ * The person spoke over the agent (VO-46, VO-13): an application's
71
+ * session is told (`POST {sessions}/{uid}/interrupt`) — what runs is
72
+ * cancelled, its code's `@app.interrupted` runs, and it stays open for
73
+ * what is said next, where Stop would leave it waiting to be resumed.
74
+ * Another agent's run is stopped, as Stop stops it.
75
+ */
76
+ interruptSession(threadId?: string): Promise<void>;
69
77
  /**
70
78
  * Stop an application's session (`POST {sessions}/{uid}/stop`): what runs
71
79
  * is cancelled and it waits to be resumed. Its thread is the session; with
@@ -126,6 +126,32 @@ export class AGUIAdapter extends BaseProtocolAdapter {
126
126
  // Ignore errors - agent may have already completed
127
127
  }
128
128
  }
129
+ /**
130
+ * The person spoke over the agent (VO-46, VO-13): an application's
131
+ * session is told (`POST {sessions}/{uid}/interrupt`) — what runs is
132
+ * cancelled, its code's `@app.interrupted` runs, and it stays open for
133
+ * what is said next, where Stop would leave it waiting to be resumed.
134
+ * Another agent's run is stopped, as Stop stops it.
135
+ */
136
+ async interruptSession(threadId) {
137
+ const sessions = this.aguiConfig.appSessions;
138
+ const thread = threadId || this.currentThreadId;
139
+ if (!sessions) {
140
+ await this.terminateAgent(threadId);
141
+ return;
142
+ }
143
+ if (!thread) {
144
+ return;
145
+ }
146
+ const url = `${sessions.replace(/\/+$/, '')}/${encodeURIComponent(thread)}/interrupt`;
147
+ const response = await this.fetchOf()(url, {
148
+ method: 'POST',
149
+ headers: this.buildHeaders(),
150
+ });
151
+ if (!response.ok) {
152
+ throw new Error(`The session ${thread} could not be told it was spoken over (${response.status}).`);
153
+ }
154
+ }
129
155
  /**
130
156
  * Stop an application's session (`POST {sessions}/{uid}/stop`): what runs
131
157
  * is cancelled and it waits to be resumed. Its thread is the session; with
@@ -700,6 +700,12 @@ export interface ChatBaseProps {
700
700
  * for a host whose composer holds the microphone.
701
701
  */
702
702
  markInterrupted: () => void;
703
+ /**
704
+ * The person spoke over the run (VO-46, VO-13): it ends as Stop ends
705
+ * it, but an application's session is told it was spoken over and
706
+ * stays open for what is said next — Stop leaves it to be resumed.
707
+ */
708
+ interrupt?: () => void;
703
709
  } | null) => void;
704
710
  /**
705
711
  * Reports whether the agent's voice is speaking, for a host that draws the
@@ -14,6 +14,11 @@
14
14
  export declare const VOICE_SAY_EVENT = "voice.say";
15
15
  /** The running application's voice, at a run's start. */
16
16
  export declare const VOICE_SETTINGS_EVENT = "voice.settings";
17
+ /**
18
+ * The person spoke over the agent, the runtime says (VO-46): a client
19
+ * playing its voice stops it.
20
+ */
21
+ export declare const VOICE_INTERRUPTED_EVENT = "voice.interrupted";
17
22
  /** The longest line said aloud: the runtime refuses longer ones. */
18
23
  export declare const SAY_ALOUD_MAX_CHARACTERS = 200;
19
24
  /** A custom event or data part, as the chat's adapters give it. */
@@ -21,5 +26,7 @@ export interface VoiceActivity {
21
26
  type: string;
22
27
  data: unknown;
23
28
  }
29
+ /** Whether an activity is the runtime's `voice.interrupted` (VO-46). */
30
+ export declare const isVoiceInterrupted: (activity: VoiceActivity) => boolean;
24
31
  /** The line a `voice.say` says, or `undefined` for anything else. */
25
32
  export declare function lineToSay(activity: VoiceActivity): string | undefined;
@@ -18,9 +18,16 @@
18
18
  export const VOICE_SAY_EVENT = 'voice.say';
19
19
  /** The running application's voice, at a run's start. */
20
20
  export const VOICE_SETTINGS_EVENT = 'voice.settings';
21
+ /**
22
+ * The person spoke over the agent, the runtime says (VO-46): a client
23
+ * playing its voice stops it.
24
+ */
25
+ export const VOICE_INTERRUPTED_EVENT = 'voice.interrupted';
21
26
  /** The longest line said aloud: the runtime refuses longer ones. */
22
27
  export const SAY_ALOUD_MAX_CHARACTERS = 200;
23
28
  const nameOf = (type) => type.replace(/^data-/, '');
29
+ /** Whether an activity is the runtime's `voice.interrupted` (VO-46). */
30
+ export const isVoiceInterrupted = (activity) => nameOf(activity.type) === VOICE_INTERRUPTED_EVENT;
24
31
  /** The line a `voice.say` says, or `undefined` for anything else. */
25
32
  export function lineToSay(activity) {
26
33
  if (nameOf(activity.type) !== VOICE_SAY_EVENT) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.5.59",
3
+ "version": "1.5.60",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",