@agent-native/core 0.152.0 → 0.152.1
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/corpus/README.md +1 -1
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +196 -30
- package/corpus/templates/content/changelog/2026-08-03-choose-text-or-a-heading-from-one-compact-formatting-control.md +6 -0
- package/corpus/templates/content/docs/solutions/evidence/content-text-selector/README.md +26 -0
- package/dist/agent/production-agent.d.ts +8 -0
- package/dist/agent/production-agent.js +66 -0
- package/dist/client/AssistantChat.js +3 -1
- package/dist/client/guided-questions.d.ts +15 -1
- package/dist/client/guided-questions.js +33 -15
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/webhook-handler.js +11 -3
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/context-tools.js +27 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
import {
|
|
3
3
|
IconBold,
|
|
4
|
+
IconCheck,
|
|
5
|
+
IconChevronDown,
|
|
4
6
|
IconItalic,
|
|
5
7
|
IconStrikethrough,
|
|
6
8
|
IconCode,
|
|
7
9
|
IconLink,
|
|
8
10
|
IconMessageCircle,
|
|
9
|
-
IconH1,
|
|
10
|
-
IconH2,
|
|
11
|
-
IconH3,
|
|
12
|
-
IconH4,
|
|
13
11
|
} from "@tabler/icons-react";
|
|
14
12
|
import {
|
|
15
13
|
NodeSelection,
|
|
@@ -20,8 +18,13 @@ import {
|
|
|
20
18
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
21
19
|
import type { Editor } from "@tiptap/react";
|
|
22
20
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
23
|
-
import { useCallback, useEffect, useState } from "react";
|
|
21
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
24
22
|
|
|
23
|
+
import {
|
|
24
|
+
Popover,
|
|
25
|
+
PopoverContent,
|
|
26
|
+
PopoverTrigger,
|
|
27
|
+
} from "@/components/ui/popover";
|
|
25
28
|
import {
|
|
26
29
|
Tooltip,
|
|
27
30
|
TooltipContent,
|
|
@@ -56,6 +59,15 @@ type SelectionFillRange = {
|
|
|
56
59
|
to: number;
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
type TextStyle = "paragraph" | 1 | 2 | 3 | 4 | 5 | 6;
|
|
63
|
+
|
|
64
|
+
function activeTextStyle(editor: Editor): TextStyle {
|
|
65
|
+
for (const level of [1, 2, 3, 4, 5, 6] as const) {
|
|
66
|
+
if (editor.isActive("heading", { level })) return level;
|
|
67
|
+
}
|
|
68
|
+
return "paragraph";
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
const selectionFillPluginKey = new PluginKey<SelectionFillRange | null>(
|
|
60
72
|
"contentSelectionFill",
|
|
61
73
|
);
|
|
@@ -106,6 +118,86 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
106
118
|
const t = useT();
|
|
107
119
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
108
120
|
const [linkUrl, setLinkUrl] = useState("");
|
|
121
|
+
const [textStyleOpen, setTextStyleOpen] = useState(false);
|
|
122
|
+
const textStyleSelection = useRef<{ from: number; to: number } | null>(null);
|
|
123
|
+
const textStyleApplied = useRef(false);
|
|
124
|
+
const restoreEditorFocusOnClose = useRef(false);
|
|
125
|
+
const [textStyle, setTextStyle] = useState<TextStyle>(() =>
|
|
126
|
+
activeTextStyle(editor),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const syncTextStyle = () => {
|
|
131
|
+
setTextStyle(activeTextStyle(editor));
|
|
132
|
+
const { from, to } = editor.state.selection;
|
|
133
|
+
if (from !== to) textStyleSelection.current = { from, to };
|
|
134
|
+
};
|
|
135
|
+
editor.on("selectionUpdate", syncTextStyle);
|
|
136
|
+
editor.on("transaction", syncTextStyle);
|
|
137
|
+
syncTextStyle();
|
|
138
|
+
return () => {
|
|
139
|
+
editor.off("selectionUpdate", syncTextStyle);
|
|
140
|
+
editor.off("transaction", syncTextStyle);
|
|
141
|
+
};
|
|
142
|
+
}, [editor]);
|
|
143
|
+
|
|
144
|
+
const textStyles = [
|
|
145
|
+
{
|
|
146
|
+
value: "paragraph" as const,
|
|
147
|
+
shortLabel: t("editor.slash.text"),
|
|
148
|
+
menuLabel: "T",
|
|
149
|
+
label: t("editor.slash.text"),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
value: 1 as const,
|
|
153
|
+
shortLabel: "H1",
|
|
154
|
+
menuLabel: "H1",
|
|
155
|
+
label: t("editor.heading1"),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
value: 2 as const,
|
|
159
|
+
shortLabel: "H2",
|
|
160
|
+
menuLabel: "H2",
|
|
161
|
+
label: t("editor.heading2"),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
value: 3 as const,
|
|
165
|
+
shortLabel: "H3",
|
|
166
|
+
menuLabel: "H3",
|
|
167
|
+
label: t("editor.heading3"),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
value: 4 as const,
|
|
171
|
+
shortLabel: "H4",
|
|
172
|
+
menuLabel: "H4",
|
|
173
|
+
label: t("editor.heading4"),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
const selectedTextStyle =
|
|
177
|
+
textStyles.find((style) => style.value === textStyle) ??
|
|
178
|
+
(typeof textStyle === "number"
|
|
179
|
+
? {
|
|
180
|
+
value: textStyle,
|
|
181
|
+
shortLabel: `H${textStyle}`,
|
|
182
|
+
menuLabel: `H${textStyle}`,
|
|
183
|
+
label: textStyle === 5 ? t("editor.heading5") : t("editor.heading6"),
|
|
184
|
+
}
|
|
185
|
+
: textStyles[0]);
|
|
186
|
+
|
|
187
|
+
const applyTextStyle = (style: TextStyle) => {
|
|
188
|
+
const chain = editor.chain();
|
|
189
|
+
if (textStyleSelection.current) {
|
|
190
|
+
chain.setTextSelection(textStyleSelection.current);
|
|
191
|
+
}
|
|
192
|
+
if (style === "paragraph") {
|
|
193
|
+
chain.setParagraph().focus().run();
|
|
194
|
+
} else {
|
|
195
|
+
chain.setHeading({ level: style }).focus().run();
|
|
196
|
+
}
|
|
197
|
+
textStyleApplied.current = true;
|
|
198
|
+
setTextStyle(style);
|
|
199
|
+
setTextStyleOpen(false);
|
|
200
|
+
};
|
|
109
201
|
|
|
110
202
|
const openLinkInput = useCallback(() => {
|
|
111
203
|
setLinkUrl(editor.getAttributes("link").href || "");
|
|
@@ -251,30 +343,7 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
251
343
|
isActive: () => editor.isActive("code"),
|
|
252
344
|
},
|
|
253
345
|
{ type: "divider" as const },
|
|
254
|
-
{
|
|
255
|
-
icon: IconH1,
|
|
256
|
-
title: t("editor.heading1"),
|
|
257
|
-
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
258
|
-
isActive: () => editor.isActive("heading", { level: 1 }),
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
icon: IconH2,
|
|
262
|
-
title: t("editor.heading2"),
|
|
263
|
-
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
264
|
-
isActive: () => editor.isActive("heading", { level: 2 }),
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
icon: IconH3,
|
|
268
|
-
title: t("editor.heading3"),
|
|
269
|
-
action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
270
|
-
isActive: () => editor.isActive("heading", { level: 3 }),
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
icon: IconH4,
|
|
274
|
-
title: t("editor.heading4"),
|
|
275
|
-
action: () => editor.chain().focus().toggleHeading({ level: 4 }).run(),
|
|
276
|
-
isActive: () => editor.isActive("heading", { level: 4 }),
|
|
277
|
-
},
|
|
346
|
+
{ type: "text-style" as const },
|
|
278
347
|
{ type: "divider" as const },
|
|
279
348
|
{
|
|
280
349
|
icon: IconLink,
|
|
@@ -354,7 +423,7 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
354
423
|
</div>
|
|
355
424
|
) : (
|
|
356
425
|
<div
|
|
357
|
-
className="flex items-center gap-0.5
|
|
426
|
+
className="flex max-w-[calc(100vw-1rem)] items-center gap-0.5"
|
|
358
427
|
onMouseDown={(e) => e.preventDefault()}
|
|
359
428
|
>
|
|
360
429
|
{items.map((item, i) => {
|
|
@@ -363,6 +432,103 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
363
432
|
<div key={`d-${i}`} className="w-px h-5 bg-border mx-0.5" />
|
|
364
433
|
);
|
|
365
434
|
}
|
|
435
|
+
if ("type" in item && item.type === "text-style") {
|
|
436
|
+
return (
|
|
437
|
+
<Popover
|
|
438
|
+
key="text-style"
|
|
439
|
+
open={textStyleOpen}
|
|
440
|
+
onOpenChange={(open) => {
|
|
441
|
+
if (open) {
|
|
442
|
+
textStyleApplied.current = false;
|
|
443
|
+
restoreEditorFocusOnClose.current = false;
|
|
444
|
+
const { from, to } = editor.state.selection;
|
|
445
|
+
if (from !== to)
|
|
446
|
+
textStyleSelection.current = { from, to };
|
|
447
|
+
}
|
|
448
|
+
setTextStyleOpen(open);
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
<PopoverTrigger asChild>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
aria-label={`${t("editor.slash.turnInto")}: ${selectedTextStyle.label}`}
|
|
455
|
+
className="flex h-8 min-w-14 items-center justify-between gap-1 rounded px-2 text-sm font-medium text-popover-foreground/85 hover:bg-accent hover:text-accent-foreground"
|
|
456
|
+
>
|
|
457
|
+
<span>{selectedTextStyle.shortLabel}</span>
|
|
458
|
+
<IconChevronDown size={14} strokeWidth={2} />
|
|
459
|
+
</button>
|
|
460
|
+
</PopoverTrigger>
|
|
461
|
+
<PopoverContent
|
|
462
|
+
portalled={false}
|
|
463
|
+
align="start"
|
|
464
|
+
sideOffset={24}
|
|
465
|
+
className="w-44 p-1"
|
|
466
|
+
onEscapeKeyDown={() => {
|
|
467
|
+
restoreEditorFocusOnClose.current = true;
|
|
468
|
+
window.setTimeout(() => editor.commands.focus(), 0);
|
|
469
|
+
}}
|
|
470
|
+
onCloseAutoFocus={(event) => {
|
|
471
|
+
if (
|
|
472
|
+
textStyleApplied.current ||
|
|
473
|
+
restoreEditorFocusOnClose.current
|
|
474
|
+
) {
|
|
475
|
+
event.preventDefault();
|
|
476
|
+
}
|
|
477
|
+
if (restoreEditorFocusOnClose.current) {
|
|
478
|
+
editor.commands.focus();
|
|
479
|
+
}
|
|
480
|
+
textStyleApplied.current = false;
|
|
481
|
+
restoreEditorFocusOnClose.current = false;
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
|
|
485
|
+
{t("editor.slash.turnInto")}
|
|
486
|
+
</div>
|
|
487
|
+
<div
|
|
488
|
+
role="menu"
|
|
489
|
+
aria-label={t("editor.slash.turnInto")}
|
|
490
|
+
className="flex flex-col gap-0.5"
|
|
491
|
+
>
|
|
492
|
+
{textStyles.map((style) => {
|
|
493
|
+
const isSelected = style.value === textStyle;
|
|
494
|
+
return (
|
|
495
|
+
<button
|
|
496
|
+
key={style.value}
|
|
497
|
+
type="button"
|
|
498
|
+
role="menuitemradio"
|
|
499
|
+
aria-checked={isSelected}
|
|
500
|
+
onPointerDown={(event) => {
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
event.stopPropagation();
|
|
503
|
+
applyTextStyle(style.value);
|
|
504
|
+
}}
|
|
505
|
+
onClick={(event) => {
|
|
506
|
+
if (event.detail === 0) {
|
|
507
|
+
applyTextStyle(style.value);
|
|
508
|
+
}
|
|
509
|
+
}}
|
|
510
|
+
className={cn(
|
|
511
|
+
"flex h-8 w-full items-center gap-2 rounded px-2 text-left text-sm",
|
|
512
|
+
isSelected
|
|
513
|
+
? "bg-accent text-accent-foreground"
|
|
514
|
+
: "text-popover-foreground hover:bg-accent hover:text-accent-foreground",
|
|
515
|
+
)}
|
|
516
|
+
>
|
|
517
|
+
<span className="w-6 shrink-0 text-xs font-semibold text-muted-foreground">
|
|
518
|
+
{style.menuLabel}
|
|
519
|
+
</span>
|
|
520
|
+
<span className="flex-1">{style.label}</span>
|
|
521
|
+
{isSelected ? (
|
|
522
|
+
<IconCheck size={15} strokeWidth={2.25} />
|
|
523
|
+
) : null}
|
|
524
|
+
</button>
|
|
525
|
+
);
|
|
526
|
+
})}
|
|
527
|
+
</div>
|
|
528
|
+
</PopoverContent>
|
|
529
|
+
</Popover>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
366
532
|
const {
|
|
367
533
|
icon: Icon,
|
|
368
534
|
title,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Content text selector visual evidence
|
|
2
|
+
|
|
3
|
+
Matched desktop captures for the compact Text/H1-H4 selector in the Content editor.
|
|
4
|
+
|
|
5
|
+
## Before
|
|
6
|
+
|
|
7
|
+
- Source revision: `dcc028cb7d67a0f9279f9235cdc4c16710ce5689`
|
|
8
|
+
- Route: `http://127.0.0.1:3317/page/j4a5KAEDBHXY`
|
|
9
|
+
- Viewport: `1496 x 846`, desktop Chrome
|
|
10
|
+
- Fixture account: disposable local account `toolbar-demo@example.com`
|
|
11
|
+
- Fixture page: `Toolbar comparison`
|
|
12
|
+
- Fixture paragraph: `Select this sentence to compare the formatting toolbar.`
|
|
13
|
+
- UI state: Personal workspace expanded, agent panel closed, full paragraph selected, formatting bubble open, text-style submenu closed
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## After
|
|
18
|
+
|
|
19
|
+
- Source: this pull request's production build
|
|
20
|
+
- Route: `http://127.0.0.1:3317/page/vDisteHisXMq`
|
|
21
|
+
- Viewport: `1496 x 846`, desktop Chrome
|
|
22
|
+
- Fixture account, page title, paragraph, sidebar state, and agent-panel state match the before capture
|
|
23
|
+
- UI state: full paragraph selected, formatting bubble open, Text selector expanded, Text checked, H1-H4 visible
|
|
24
|
+
- The route differs because the isolated worktree uses its own disposable local fixture database; the rendered fixture and viewport are matched
|
|
25
|
+
|
|
26
|
+

|
|
@@ -235,6 +235,14 @@ export interface ActionEntry {
|
|
|
235
235
|
* `needsApproval` option. See `packages/core/docs/content/actions.mdx`.
|
|
236
236
|
*/
|
|
237
237
|
needsApproval?: boolean | ((args: any, ctx?: import("../action.js").ActionRunContext) => boolean | Promise<boolean>);
|
|
238
|
+
/**
|
|
239
|
+
* The action hands control back to the user: once it succeeds the loop stops
|
|
240
|
+
* the turn instead of asking the model for another step, and any remaining
|
|
241
|
+
* tool calls in the same assistant message do not execute. Only for actions
|
|
242
|
+
* whose whole purpose is to wait on a human (`ask-question`) — telling the
|
|
243
|
+
* model to stop in the tool result does not make it stop.
|
|
244
|
+
*/
|
|
245
|
+
endsTurn?: boolean;
|
|
238
246
|
/** Which framework tool group contributed this action. Set by the framework,
|
|
239
247
|
* never by an app: apps own their action names, and a tagged action is one
|
|
240
248
|
* the app can switch off wholesale through `frameworkTools`. Tagged actions
|
|
@@ -3823,6 +3823,23 @@ export async function runAgentLoop(opts) {
|
|
|
3823
3823
|
emptyFinalResponseRetries = 0;
|
|
3824
3824
|
flushUnstreamedAssistantText();
|
|
3825
3825
|
let requestedActionStop = null;
|
|
3826
|
+
// An `endsTurn` action ran and handed control to the user. Distinct from
|
|
3827
|
+
// `requestedActionStop`, which also covers failure stops that must not
|
|
3828
|
+
// suppress the remaining tool calls.
|
|
3829
|
+
let turnYieldedToUser = false;
|
|
3830
|
+
// Called by every path that completes a tool call successfully — a fresh
|
|
3831
|
+
// invocation, a journal replay, a ledger recovery. A replayed
|
|
3832
|
+
// `ask-question` that skipped this would let the resumed run ask a second
|
|
3833
|
+
// time over the card the user is already looking at.
|
|
3834
|
+
const noteToolCallSucceeded = (actionEntry) => {
|
|
3835
|
+
if (actionEntry.endsTurn !== true)
|
|
3836
|
+
return;
|
|
3837
|
+
turnYieldedToUser = true;
|
|
3838
|
+
requestedActionStop ??= {
|
|
3839
|
+
message: "Waiting for your answer before continuing.",
|
|
3840
|
+
errorCode: "awaiting-user-input",
|
|
3841
|
+
};
|
|
3842
|
+
};
|
|
3826
3843
|
const noteRepeatedToolCall = (toolName, input) => {
|
|
3827
3844
|
const key = toolCallCacheKey(toolName, input);
|
|
3828
3845
|
const count = (repeatedToolCalls.get(key) ?? 0) + 1;
|
|
@@ -4167,6 +4184,7 @@ export async function runAgentLoop(opts) {
|
|
|
4167
4184
|
completedSideEffect: true,
|
|
4168
4185
|
});
|
|
4169
4186
|
recordToolResult(result, false);
|
|
4187
|
+
noteToolCallSucceeded(actionEntry);
|
|
4170
4188
|
return {
|
|
4171
4189
|
type: "tool-result",
|
|
4172
4190
|
toolCallId: toolCall.id,
|
|
@@ -4220,6 +4238,7 @@ export async function runAgentLoop(opts) {
|
|
|
4220
4238
|
...(actionEntry.chatUI ? { chatUI: actionEntry.chatUI } : {}),
|
|
4221
4239
|
});
|
|
4222
4240
|
recordToolResult(result, false);
|
|
4241
|
+
noteToolCallSucceeded(actionEntry);
|
|
4223
4242
|
return {
|
|
4224
4243
|
type: "tool-result",
|
|
4225
4244
|
toolCallId: toolCall.id,
|
|
@@ -4645,6 +4664,7 @@ export async function runAgentLoop(opts) {
|
|
|
4645
4664
|
});
|
|
4646
4665
|
recordToolResult(result, isError);
|
|
4647
4666
|
if (!isError) {
|
|
4667
|
+
noteToolCallSucceeded(actionEntry);
|
|
4648
4668
|
if (cacheKey) {
|
|
4649
4669
|
readOnlyToolResultCache.set(cacheKey, result);
|
|
4650
4670
|
}
|
|
@@ -4715,7 +4735,46 @@ export async function runAgentLoop(opts) {
|
|
|
4715
4735
|
parallelBatchKind = null;
|
|
4716
4736
|
toolResultParts.push(...(await Promise.all(batch.map(runToolCall))));
|
|
4717
4737
|
};
|
|
4738
|
+
// An `endsTurn` action already handed control to the user, so the rest of
|
|
4739
|
+
// this assistant message belongs to a turn that is over. Report those calls
|
|
4740
|
+
// as not executed rather than running them: a second `ask-question` would
|
|
4741
|
+
// overwrite the first one's card before anyone could answer it.
|
|
4742
|
+
const skipToolCallAfterYield = (toolCall) => {
|
|
4743
|
+
const result = `Not executed: ${toolCall.name} was called after an action that ends the turn. ` +
|
|
4744
|
+
`The turn is paused for the user's answer — call it again on a later turn if still needed.`;
|
|
4745
|
+
send({
|
|
4746
|
+
type: "tool_start",
|
|
4747
|
+
id: toolCall.id,
|
|
4748
|
+
tool: toolCall.name,
|
|
4749
|
+
input: toolCall.input,
|
|
4750
|
+
});
|
|
4751
|
+
send({
|
|
4752
|
+
type: "tool_done",
|
|
4753
|
+
id: toolCall.id,
|
|
4754
|
+
tool: toolCall.name,
|
|
4755
|
+
input: toolCall.input,
|
|
4756
|
+
result,
|
|
4757
|
+
completedSideEffect: false,
|
|
4758
|
+
});
|
|
4759
|
+
toolResultHistory.push({
|
|
4760
|
+
name: toolCall.name,
|
|
4761
|
+
content: result,
|
|
4762
|
+
isError: false,
|
|
4763
|
+
});
|
|
4764
|
+
return {
|
|
4765
|
+
type: "tool-result",
|
|
4766
|
+
toolCallId: toolCall.id,
|
|
4767
|
+
toolName: toolCall.name,
|
|
4768
|
+
toolInput: JSON.stringify(toolCall.input ?? {}),
|
|
4769
|
+
content: result,
|
|
4770
|
+
};
|
|
4771
|
+
};
|
|
4718
4772
|
for (const toolCall of toolCallParts) {
|
|
4773
|
+
if (turnYieldedToUser) {
|
|
4774
|
+
await flushParallelBatch();
|
|
4775
|
+
toolResultParts.push(skipToolCallAfterYield(toolCall));
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4719
4778
|
const batchKind = getParallelBatchKind(toolCall);
|
|
4720
4779
|
if (batchKind) {
|
|
4721
4780
|
if (parallelBatchKind && parallelBatchKind !== batchKind) {
|
|
@@ -4829,6 +4888,13 @@ export async function runAgentLoop(opts) {
|
|
|
4829
4888
|
message: terminalActionStop.message,
|
|
4830
4889
|
});
|
|
4831
4890
|
}
|
|
4891
|
+
else if (terminalActionStop?.errorCode === "awaiting-user-input") {
|
|
4892
|
+
reportOutcome({
|
|
4893
|
+
state: "input_required",
|
|
4894
|
+
code: "awaiting_user_input",
|
|
4895
|
+
message: terminalActionStop.message,
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4832
4898
|
else if (terminalActionStop) {
|
|
4833
4899
|
reportOutcome({
|
|
4834
4900
|
state: "failed",
|
|
@@ -4083,11 +4083,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4083
4083
|
// GuidedQuestionPayload to application_state under "guided-questions". The
|
|
4084
4084
|
// hook polls that key, and on submit/skip composes the answer as a normal
|
|
4085
4085
|
// user turn (via the shared sendToAgentChat) and clears the persisted key so
|
|
4086
|
-
// the question does not reappear.
|
|
4086
|
+
// the question does not reappear. The key is per browser tab, so `threadId`
|
|
4087
|
+
// is what keeps a pending question in the chat that asked it.
|
|
4087
4088
|
const { questions: guidedQuestions, title: guidedQuestionsTitle, description: guidedQuestionsDescription, skipLabel: guidedQuestionsSkipLabel, submitLabel: guidedQuestionsSubmitLabel, handleSubmit: handleGuidedQuestionsSubmit, handleSkip: handleGuidedQuestionsSkip, } = useGuidedQuestionFlow({
|
|
4088
4089
|
stateKey: "guided-questions",
|
|
4089
4090
|
queryKey: ["guided-questions"],
|
|
4090
4091
|
...(browserTabId ? { browserTabId } : {}),
|
|
4092
|
+
...(threadId ? { threadId } : {}),
|
|
4091
4093
|
});
|
|
4092
4094
|
const hasComposerAccessoryAboveStack = Boolean(composerError ||
|
|
4093
4095
|
showComposerSlot ||
|
|
@@ -46,6 +46,13 @@ export interface GuidedQuestionPayload {
|
|
|
46
46
|
* promise with the answer instead of forwarding it to the agent chat.
|
|
47
47
|
*/
|
|
48
48
|
clientResolveId?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Chat thread that asked. Set by the agent's `ask-question` tool. A payload
|
|
51
|
+
* carrying this only renders in that conversation; one without it (app code
|
|
52
|
+
* calling {@link askUserQuestion}, deterministic writes) is not thread-bound
|
|
53
|
+
* and renders wherever the flow is mounted.
|
|
54
|
+
*/
|
|
55
|
+
threadId?: string;
|
|
49
56
|
}
|
|
50
57
|
export declare function isOtherGuidedAnswer(value: unknown): value is string;
|
|
51
58
|
export declare function getOtherGuidedAnswerText(value: unknown): string;
|
|
@@ -137,6 +144,13 @@ export interface UseGuidedQuestionFlowOptions {
|
|
|
137
144
|
* (which it almost always does — see `sessionBrowserTabId`).
|
|
138
145
|
*/
|
|
139
146
|
browserTabId?: string;
|
|
147
|
+
/**
|
|
148
|
+
* The conversation this flow is mounted in. Agent-written payloads name the
|
|
149
|
+
* thread that asked, and a pending question belongs to that conversation
|
|
150
|
+
* only — the application-state key is per browser tab, so without this the
|
|
151
|
+
* same card follows the user into every other chat in the tab.
|
|
152
|
+
*/
|
|
153
|
+
threadId?: string;
|
|
140
154
|
queryKey?: readonly unknown[];
|
|
141
155
|
refetchInterval?: number | false;
|
|
142
156
|
submitMessage?: string;
|
|
@@ -147,7 +161,7 @@ export interface UseGuidedQuestionFlowOptions {
|
|
|
147
161
|
}) => string;
|
|
148
162
|
buildSkipContext?: () => string;
|
|
149
163
|
}
|
|
150
|
-
export declare function useGuidedQuestionFlow({ enabled, stateKey, browserTabId, queryKey, refetchInterval, submitMessage, skipMessage, buildSubmitContext, buildSkipContext, }?: UseGuidedQuestionFlowOptions): {
|
|
164
|
+
export declare function useGuidedQuestionFlow({ enabled, stateKey, browserTabId, threadId, queryKey, refetchInterval, submitMessage, skipMessage, buildSubmitContext, buildSkipContext, }?: UseGuidedQuestionFlowOptions): {
|
|
151
165
|
payload: GuidedQuestionPayload | null;
|
|
152
166
|
questions: GuidedQuestion[] | null;
|
|
153
167
|
title: string | undefined;
|
|
@@ -406,7 +406,17 @@ function normalizeBrowserTabId(browserTabId) {
|
|
|
406
406
|
const trimmed = browserTabId.trim();
|
|
407
407
|
return SAFE_BROWSER_TAB_ID_RE.test(trimmed) ? trimmed : undefined;
|
|
408
408
|
}
|
|
409
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Whether a stored payload belongs to the conversation currently on screen.
|
|
411
|
+
* Payloads with no `threadId` are not thread-bound and always match.
|
|
412
|
+
*/
|
|
413
|
+
function payloadBelongsToThread(payload, threadId) {
|
|
414
|
+
const asker = typeof payload.threadId === "string" ? payload.threadId.trim() : "";
|
|
415
|
+
if (!asker)
|
|
416
|
+
return true;
|
|
417
|
+
return asker === (threadId ?? "").trim();
|
|
418
|
+
}
|
|
419
|
+
export function useGuidedQuestionFlow({ enabled = true, stateKey = "show-questions", browserTabId, threadId, queryKey = ["show-questions"], refetchInterval = false, submitMessage = "Here are my answers — go ahead.", skipMessage = "Skip the questions — decide for me.", buildSubmitContext, buildSkipContext, } = {}) {
|
|
410
420
|
const queryClient = useQueryClient();
|
|
411
421
|
const [payload, setPayload] = useState(null);
|
|
412
422
|
const normalizedBrowserTabId = useMemo(() => normalizeBrowserTabId(browserTabId), [browserTabId]);
|
|
@@ -458,7 +468,11 @@ export function useGuidedQuestionFlow({ enabled = true, stateKey = "show-questio
|
|
|
458
468
|
if (prev &&
|
|
459
469
|
guidedQuestionsFingerprint(prev.questions) ===
|
|
460
470
|
guidedQuestionsFingerprint(data.questions) &&
|
|
461
|
-
prev.clientResolveId === data.clientResolveId
|
|
471
|
+
prev.clientResolveId === data.clientResolveId &&
|
|
472
|
+
// Two chats can ask a word-for-word identical question. Keeping the
|
|
473
|
+
// old payload would bind the new one to the wrong thread, hiding it
|
|
474
|
+
// in the chat that asked and re-showing it in the one that didn't.
|
|
475
|
+
prev.threadId === data.threadId) {
|
|
462
476
|
return prev;
|
|
463
477
|
}
|
|
464
478
|
return data;
|
|
@@ -468,6 +482,10 @@ export function useGuidedQuestionFlow({ enabled = true, stateKey = "show-questio
|
|
|
468
482
|
setPayload(null);
|
|
469
483
|
}
|
|
470
484
|
}, [data]);
|
|
485
|
+
// A question asked in another conversation stays in application state — the
|
|
486
|
+
// user can still answer it by going back to that chat — but it must not
|
|
487
|
+
// render here.
|
|
488
|
+
const visiblePayload = payload && payloadBelongsToThread(payload, threadId) ? payload : null;
|
|
471
489
|
const clear = useCallback(() => {
|
|
472
490
|
setPayload(null);
|
|
473
491
|
queryClient.setQueryData(resolvedQueryKey, null);
|
|
@@ -481,15 +499,15 @@ export function useGuidedQuestionFlow({ enabled = true, stateKey = "show-questio
|
|
|
481
499
|
const handleSubmit = useCallback((answers) => {
|
|
482
500
|
// Client-initiated question (askUserQuestion): resolve the caller's
|
|
483
501
|
// promise with the answer instead of forwarding it to the agent chat.
|
|
484
|
-
const resolveId =
|
|
502
|
+
const resolveId = visiblePayload?.clientResolveId;
|
|
485
503
|
if (resolveId) {
|
|
486
|
-
const firstId =
|
|
504
|
+
const firstId = visiblePayload?.questions?.[0]?.id ?? "q1";
|
|
487
505
|
resolveClientQuestion(resolveId, extractSingleAnswer(answers, firstId));
|
|
488
506
|
clear();
|
|
489
507
|
return;
|
|
490
508
|
}
|
|
491
509
|
const formattedAnswers = formatGuidedAnswersForAgent(answers);
|
|
492
|
-
const resolvedSubmitMessage =
|
|
510
|
+
const resolvedSubmitMessage = visiblePayload?.submitMessage ?? submitMessage;
|
|
493
511
|
const context = buildSubmitContext?.({ answers, formattedAnswers }) ??
|
|
494
512
|
defaultGuidedSubmitContext(formattedAnswers);
|
|
495
513
|
sendToAgentChat({
|
|
@@ -498,28 +516,28 @@ export function useGuidedQuestionFlow({ enabled = true, stateKey = "show-questio
|
|
|
498
516
|
submit: true,
|
|
499
517
|
});
|
|
500
518
|
clear();
|
|
501
|
-
}, [buildSubmitContext, clear,
|
|
519
|
+
}, [buildSubmitContext, clear, visiblePayload, submitMessage]);
|
|
502
520
|
const handleSkip = useCallback(() => {
|
|
503
|
-
const resolveId =
|
|
521
|
+
const resolveId = visiblePayload?.clientResolveId;
|
|
504
522
|
if (resolveId) {
|
|
505
523
|
resolveClientQuestion(resolveId, null);
|
|
506
524
|
clear();
|
|
507
525
|
return;
|
|
508
526
|
}
|
|
509
527
|
sendToAgentChat({
|
|
510
|
-
message:
|
|
528
|
+
message: visiblePayload?.skipMessage ?? skipMessage,
|
|
511
529
|
context: buildSkipContext?.(),
|
|
512
530
|
submit: true,
|
|
513
531
|
});
|
|
514
532
|
clear();
|
|
515
|
-
}, [buildSkipContext, clear,
|
|
533
|
+
}, [buildSkipContext, clear, visiblePayload, skipMessage]);
|
|
516
534
|
return {
|
|
517
|
-
payload,
|
|
518
|
-
questions:
|
|
519
|
-
title:
|
|
520
|
-
description:
|
|
521
|
-
skipLabel:
|
|
522
|
-
submitLabel:
|
|
535
|
+
payload: visiblePayload,
|
|
536
|
+
questions: visiblePayload?.questions ?? null,
|
|
537
|
+
title: visiblePayload?.title,
|
|
538
|
+
description: visiblePayload?.description,
|
|
539
|
+
skipLabel: visiblePayload?.skipLabel,
|
|
540
|
+
submitLabel: visiblePayload?.submitLabel,
|
|
523
541
|
clear,
|
|
524
542
|
handleSubmit,
|
|
525
543
|
handleSkip,
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -1534,14 +1534,22 @@ function getQueuedA2AContinuationAgent(completedRun) {
|
|
|
1534
1534
|
}
|
|
1535
1535
|
function extractSlackInputRequest(completedRun) {
|
|
1536
1536
|
const events = completedRun.events.map((runEvent) => runEvent.event);
|
|
1537
|
-
const
|
|
1537
|
+
const delivered = events.find((event) => event.type === "tool_done" &&
|
|
1538
1538
|
event.tool === "ask-question" &&
|
|
1539
1539
|
String(event.result ?? "").startsWith("Asked the user a clarifying question and rendered it in the chat."));
|
|
1540
|
-
if (!
|
|
1540
|
+
if (!delivered)
|
|
1541
1541
|
return null;
|
|
1542
|
+
// Match the start to the delivered call by id. A turn can also contain
|
|
1543
|
+
// `ask-question` calls that never ran — the loop skips the rest of the
|
|
1544
|
+
// message once one of them ends the turn, but still emits their start events
|
|
1545
|
+
// — and scanning for the last start would project a question the user's chat
|
|
1546
|
+
// never showed.
|
|
1547
|
+
const deliveredId = delivered.type === "tool_done" ? delivered.id : "";
|
|
1542
1548
|
for (let index = events.length - 1; index >= 0; index--) {
|
|
1543
1549
|
const event = events[index];
|
|
1544
|
-
if (event.type !== "tool_start" ||
|
|
1550
|
+
if (event.type !== "tool_start" ||
|
|
1551
|
+
event.tool !== "ask-question" ||
|
|
1552
|
+
event.id !== deliveredId) {
|
|
1545
1553
|
continue;
|
|
1546
1554
|
}
|
|
1547
1555
|
const input = event.input;
|
|
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
137
137
|
inputSchema: {
|
|
138
138
|
type: string;
|
|
139
139
|
properties: {
|
|
140
|
-
count?: undefined;
|
|
141
140
|
query?: undefined;
|
|
142
141
|
minutes?: undefined;
|
|
143
142
|
limit?: undefined;
|
|
144
143
|
clientHint?: undefined;
|
|
145
144
|
timestamp?: undefined;
|
|
146
145
|
chapterId?: undefined;
|
|
146
|
+
count?: undefined;
|
|
147
147
|
startAt?: undefined;
|
|
148
148
|
endAt?: undefined;
|
|
149
149
|
reason?: undefined;
|
|
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
159
159
|
inputSchema: {
|
|
160
160
|
type: string;
|
|
161
161
|
properties: {
|
|
162
|
-
count?: undefined;
|
|
163
162
|
query: {
|
|
164
163
|
type: string;
|
|
165
164
|
description: string;
|
|
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
175
174
|
clientHint?: undefined;
|
|
176
175
|
timestamp?: undefined;
|
|
177
176
|
chapterId?: undefined;
|
|
177
|
+
count?: undefined;
|
|
178
178
|
startAt?: undefined;
|
|
179
179
|
endAt?: undefined;
|
|
180
180
|
reason?: undefined;
|
|
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
190
190
|
inputSchema: {
|
|
191
191
|
type: string;
|
|
192
192
|
properties: {
|
|
193
|
-
count?: undefined;
|
|
194
193
|
minutes: {
|
|
195
194
|
type: string;
|
|
196
195
|
description: string;
|
|
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
200
199
|
clientHint?: undefined;
|
|
201
200
|
timestamp?: undefined;
|
|
202
201
|
chapterId?: undefined;
|
|
202
|
+
count?: undefined;
|
|
203
203
|
startAt?: undefined;
|
|
204
204
|
endAt?: undefined;
|
|
205
205
|
reason?: undefined;
|
|
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
216
216
|
type: string;
|
|
217
217
|
required: string[];
|
|
218
218
|
properties: {
|
|
219
|
-
count?: undefined;
|
|
220
219
|
query: {
|
|
221
220
|
type: string;
|
|
222
221
|
description: string;
|
|
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
235
234
|
};
|
|
236
235
|
timestamp?: undefined;
|
|
237
236
|
chapterId?: undefined;
|
|
237
|
+
count?: undefined;
|
|
238
238
|
startAt?: undefined;
|
|
239
239
|
endAt?: undefined;
|
|
240
240
|
reason?: undefined;
|
|
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
250
250
|
type: string;
|
|
251
251
|
required: string[];
|
|
252
252
|
properties: {
|
|
253
|
-
count?: undefined;
|
|
254
253
|
query?: undefined;
|
|
255
254
|
minutes?: undefined;
|
|
256
255
|
limit?: undefined;
|
|
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
264
263
|
description: string;
|
|
265
264
|
};
|
|
266
265
|
chapterId?: undefined;
|
|
266
|
+
count?: undefined;
|
|
267
267
|
startAt?: undefined;
|
|
268
268
|
endAt?: undefined;
|
|
269
269
|
includeMicrophone?: undefined;
|
|
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
314
314
|
type: string;
|
|
315
315
|
required: string[];
|
|
316
316
|
properties: {
|
|
317
|
-
count?: undefined;
|
|
318
317
|
query?: undefined;
|
|
319
318
|
minutes?: undefined;
|
|
320
319
|
limit?: undefined;
|
|
321
320
|
clientHint?: undefined;
|
|
322
321
|
timestamp?: undefined;
|
|
323
322
|
chapterId?: undefined;
|
|
323
|
+
count?: undefined;
|
|
324
324
|
startAt: {
|
|
325
325
|
type: string;
|
|
326
326
|
description: string;
|
|
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
349
349
|
type: string;
|
|
350
350
|
required: string[];
|
|
351
351
|
properties: {
|
|
352
|
-
count?: undefined;
|
|
353
352
|
query?: undefined;
|
|
354
353
|
minutes?: undefined;
|
|
355
354
|
limit?: undefined;
|
|
356
355
|
clientHint?: undefined;
|
|
357
356
|
timestamp?: undefined;
|
|
358
357
|
chapterId?: undefined;
|
|
358
|
+
count?: undefined;
|
|
359
359
|
startAt?: undefined;
|
|
360
360
|
endAt?: undefined;
|
|
361
361
|
reason?: undefined;
|
|
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
16
16
|
ok?: undefined;
|
|
17
17
|
error?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
count?: undefined;
|
|
20
19
|
updated: number;
|
|
21
20
|
ok?: undefined;
|
|
22
21
|
error?: undefined;
|
|
23
|
-
} | {
|
|
24
22
|
count?: undefined;
|
|
23
|
+
} | {
|
|
25
24
|
updated?: undefined;
|
|
26
25
|
error: string;
|
|
27
26
|
ok?: undefined;
|
|
28
|
-
} | {
|
|
29
27
|
count?: undefined;
|
|
28
|
+
} | {
|
|
30
29
|
updated?: undefined;
|
|
31
30
|
ok: boolean;
|
|
32
31
|
error?: undefined;
|
|
32
|
+
count?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
|
-
ok?: undefined;
|
|
46
44
|
summary: import("./types.js").TraceSummary;
|
|
47
45
|
spans: import("./types.js").TraceSpan[];
|
|
48
46
|
id?: undefined;
|
|
49
|
-
} | {
|
|
50
47
|
error?: undefined;
|
|
51
48
|
ok?: undefined;
|
|
49
|
+
} | {
|
|
52
50
|
summary?: undefined;
|
|
53
51
|
spans?: undefined;
|
|
54
52
|
id: string;
|
|
55
|
-
|
|
53
|
+
error?: undefined;
|
|
56
54
|
ok?: undefined;
|
|
55
|
+
} | {
|
|
57
56
|
summary?: undefined;
|
|
58
57
|
spans?: undefined;
|
|
59
58
|
id?: undefined;
|
|
60
59
|
error: any;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
ok?: undefined;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -376,8 +376,12 @@ export function createUrlTools() {
|
|
|
376
376
|
},
|
|
377
377
|
},
|
|
378
378
|
"ask-question": {
|
|
379
|
+
// The turn is over once the question is on screen. Without this the loop
|
|
380
|
+
// asks the model for another step, and it keeps working (and re-asking)
|
|
381
|
+
// over an unanswered question no matter what the tool result says.
|
|
382
|
+
endsTurn: true,
|
|
379
383
|
tool: {
|
|
380
|
-
description: "Ask the user a multiple-choice clarifying question and render it inline in the chat. Use this ONLY when you are genuinely blocked on a decision you cannot resolve from context and a wrong guess would be costly — an ambiguous metric, date range, or grain; a real fork in approach. Present 2-5 concrete options and mark the most likely one recommended. Do NOT use it for confirmations, for things the user already specified, or to dodge easy work you could just do.
|
|
384
|
+
description: "Ask the user a multiple-choice clarifying question and render it inline in the chat. Use this ONLY when you are genuinely blocked on a decision you cannot resolve from context and a wrong guess would be costly — an ambiguous metric, date range, or grain; a real fork in approach. Present 2-5 concrete options and mark the most likely one recommended. Do NOT use it for confirmations, for things the user already specified, or to dodge easy work you could just do. Calling this ends the turn: one question per turn, and any other tool call you emit alongside it will not run.",
|
|
381
385
|
parameters: {
|
|
382
386
|
type: "object",
|
|
383
387
|
properties: {
|
|
@@ -408,9 +412,12 @@ export function createUrlTools() {
|
|
|
408
412
|
},
|
|
409
413
|
},
|
|
410
414
|
run: async (args) => {
|
|
415
|
+
// These must throw, not return an error string: `endsTurn` yields the
|
|
416
|
+
// turn on any non-error result, so a returned string would leave the
|
|
417
|
+
// run waiting on a question card that was never written.
|
|
411
418
|
const question = String(args?.question ?? "").trim();
|
|
412
419
|
if (!question)
|
|
413
|
-
|
|
420
|
+
throw new Error("'question' is required.");
|
|
414
421
|
const header = String(args?.header ?? "").trim();
|
|
415
422
|
const allowMultiple = String(args?.allowMultiple ?? "") === "true";
|
|
416
423
|
const allowFreeText = String(args?.allowFreeText ?? "true") !== "false";
|
|
@@ -419,10 +426,10 @@ export function createUrlTools() {
|
|
|
419
426
|
parsedOptions = JSON.parse(String(args?.options ?? "[]"));
|
|
420
427
|
}
|
|
421
428
|
catch {
|
|
422
|
-
|
|
429
|
+
throw new Error("'options' must be a JSON array of { label, value?, description?, recommended? }.");
|
|
423
430
|
}
|
|
424
431
|
if (!Array.isArray(parsedOptions) || parsedOptions.length === 0) {
|
|
425
|
-
|
|
432
|
+
throw new Error("'options' must be a non-empty JSON array of { label, value?, description?, recommended? }.");
|
|
426
433
|
}
|
|
427
434
|
const options = parsedOptions
|
|
428
435
|
.map((raw) => {
|
|
@@ -450,14 +457,26 @@ export function createUrlTools() {
|
|
|
450
457
|
})
|
|
451
458
|
.filter((opt) => opt !== null);
|
|
452
459
|
if (options.length === 0) {
|
|
453
|
-
|
|
460
|
+
throw new Error("'options' must contain at least one option with a label.");
|
|
454
461
|
}
|
|
455
462
|
// Shape must match the GuidedQuestionFlow renderer in
|
|
456
463
|
// client/guided-questions.tsx: a `text-options` question whose options
|
|
457
464
|
// carry `value`, with `multiSelect` for multi-pick and `allowOther` for
|
|
458
465
|
// free text. The renderer otherwise injects "Explore"/"Decide" options,
|
|
459
466
|
// which would be noise for a focused clarifying question, so disable them.
|
|
467
|
+
// The application-state key is scoped per browser tab, not per chat, so
|
|
468
|
+
// the payload has to name the thread that asked. The client hides a
|
|
469
|
+
// pending question in every other conversation instead of following the
|
|
470
|
+
// user from chat to chat.
|
|
471
|
+
// A request that carried no thread id falls back to the run id (see
|
|
472
|
+
// `onRunStart`), and a per-run value would never match the chat the user
|
|
473
|
+
// is looking at. Leave those surfaces unbound — they have one chat.
|
|
474
|
+
const askingRunCtx = getRequestRunContext();
|
|
475
|
+
const askingThreadId = askingRunCtx?.threadId && askingRunCtx.threadId !== askingRunCtx.runId
|
|
476
|
+
? askingRunCtx.threadId
|
|
477
|
+
: undefined;
|
|
460
478
|
const payload = {
|
|
479
|
+
...(askingThreadId ? { threadId: askingThreadId } : {}),
|
|
461
480
|
questions: [
|
|
462
481
|
{
|
|
463
482
|
id: "q1",
|
|
@@ -475,7 +494,9 @@ export function createUrlTools() {
|
|
|
475
494
|
};
|
|
476
495
|
const { writeAppState } = await import("../../application-state/script-helpers.js");
|
|
477
496
|
await writeAppState(appStateKeyForBrowserTab("guided-questions", getRequestRunContext()?.browserTabId), payload);
|
|
478
|
-
|
|
497
|
+
// The `startsWith` of this text is how integration surfaces recognize a
|
|
498
|
+
// delivered question (see `extractSlackInputRequest`). Keep the prefix.
|
|
499
|
+
return "Asked the user a clarifying question and rendered it in the chat. This turn is over — their answer arrives as a new message.";
|
|
479
500
|
},
|
|
480
501
|
},
|
|
481
502
|
};
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
token: string;
|
|
31
30
|
expiresAt: string;
|
|
32
31
|
ttlSeconds: number;
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.152.
|
|
3
|
+
"version": "0.152.1",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|