@agent-native/core 0.68.0 → 0.68.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/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +22 -3
- package/corpus/core/src/agent/engine/types.ts +1 -0
- package/corpus/core/src/agent/production-agent.ts +2 -0
- package/corpus/core/src/agent/types.ts +1 -0
- package/corpus/core/src/client/AssistantChat.tsx +45 -6
- package/corpus/core/src/client/active-run-state.ts +16 -0
- package/corpus/core/src/client/guided-questions.tsx +73 -17
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/core/src/templates/workspace-root/.env.example +4 -0
- package/corpus/templates/design/actions/show-design-questions.ts +15 -1
- package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
- package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +21 -3
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/engine/types.d.ts +2 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +3 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +42 -7
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/active-run-state.d.ts +2 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +10 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/guided-questions.d.ts +2 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +52 -7
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/templates/workspace-root/.env.example +4 -0
- package/package.json +1 -1
- package/src/templates/workspace-root/.env.example +4 -0
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.68.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 48356d7: Forward Builder gateway heartbeat JSONL frames through the engine and agent SSE stream so long upstream silences (adaptive thinking, TTFT) do not trip the client no-progress timeout.
|
|
8
|
+
- 48356d7: Fix guided question selection UX: preserve answers across poll refreshes, pause polling while a form is open, show clearer selected-state affordances (including `aria-pressed` on option buttons), and stop duplicate Explore/Decide injection in Design question flows.
|
|
9
|
+
- 48356d7: Resume page-load chat reconnect from the last seen run event seq instead of replaying the full SSE history, preventing duplicated assistant turns after refresh.
|
|
10
|
+
|
|
3
11
|
## 0.68.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -58,6 +58,8 @@ export const BUILDER_SUPPORTED_MODELS = BUILDER_MODEL_CONFIG.supportedModels;
|
|
|
58
58
|
// (Netlify synchronous Functions are 60s) hard-kill the invocation.
|
|
59
59
|
const DEFAULT_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
|
|
60
60
|
const MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
|
|
61
|
+
/** Local ai-services has no serverless wall; allow longer streams in dev. */
|
|
62
|
+
const MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = 180_000;
|
|
61
63
|
const BUILDER_GATEWAY_NETWORK_ERROR_CODE = "builder_gateway_network_error";
|
|
62
64
|
|
|
63
65
|
export const BUILDER_DEFAULT_MODEL = BUILDER_MODEL_CONFIG.defaultModel;
|
|
@@ -533,6 +535,10 @@ async function* parseJsonlStream(
|
|
|
533
535
|
};
|
|
534
536
|
break;
|
|
535
537
|
|
|
538
|
+
case "heartbeat":
|
|
539
|
+
yield { type: "gateway-heartbeat" };
|
|
540
|
+
break;
|
|
541
|
+
|
|
536
542
|
case "tool-call": {
|
|
537
543
|
flushPending();
|
|
538
544
|
parts.push({
|
|
@@ -749,14 +755,27 @@ export function createBuilderEngine(
|
|
|
749
755
|
return new BuilderEngine();
|
|
750
756
|
}
|
|
751
757
|
|
|
758
|
+
function resolveMaxBuilderGatewayTimeoutMs(): number {
|
|
759
|
+
try {
|
|
760
|
+
const base = getBuilderGatewayBaseUrl();
|
|
761
|
+
if (/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/i.test(base)) {
|
|
762
|
+
return MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
763
|
+
}
|
|
764
|
+
} catch {
|
|
765
|
+
// ignore malformed override
|
|
766
|
+
}
|
|
767
|
+
return MAX_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
768
|
+
}
|
|
769
|
+
|
|
752
770
|
function getBuilderGatewayTimeoutMs(): number {
|
|
753
771
|
const raw = process.env.AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
754
|
-
|
|
772
|
+
const maxMs = resolveMaxBuilderGatewayTimeoutMs();
|
|
773
|
+
if (!raw) return maxMs;
|
|
755
774
|
const parsed = Number(raw);
|
|
756
775
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
757
|
-
return
|
|
776
|
+
return maxMs;
|
|
758
777
|
}
|
|
759
|
-
return Math.min(parsed,
|
|
778
|
+
return Math.min(parsed, maxMs);
|
|
760
779
|
}
|
|
761
780
|
|
|
762
781
|
function createGatewayAbortSignal(
|
|
@@ -141,6 +141,7 @@ export type EngineEvent =
|
|
|
141
141
|
| { type: "thinking-delta"; text: string; signature?: string }
|
|
142
142
|
| { type: "tool-input-start"; id?: string; name?: string }
|
|
143
143
|
| { type: "tool-input-delta"; id?: string; name?: string; text?: string }
|
|
144
|
+
| { type: "gateway-heartbeat" }
|
|
144
145
|
| { type: "tool-call"; id: string; name: string; input: unknown }
|
|
145
146
|
| {
|
|
146
147
|
type: "tool-call-error";
|
|
@@ -2573,6 +2573,8 @@ export async function runAgentLoop(opts: {
|
|
|
2573
2573
|
event.name ??
|
|
2574
2574
|
(event.id ? toolInputNames.get(event.id) : undefined);
|
|
2575
2575
|
sendToolInputActivity(toolName);
|
|
2576
|
+
} else if (event.type === "gateway-heartbeat") {
|
|
2577
|
+
send({ type: "stream_keepalive" });
|
|
2576
2578
|
} else if (event.type === "tool-call") {
|
|
2577
2579
|
// The authoritative tool-call blocks arrive in assistant-content.
|
|
2578
2580
|
} else if (event.type === "tool-call-error") {
|
|
@@ -169,6 +169,7 @@ export type AgentChatEvent =
|
|
|
169
169
|
| { type: "text"; text: string }
|
|
170
170
|
| { type: "thinking"; text: string }
|
|
171
171
|
| { type: "activity"; label: string; tool?: string }
|
|
172
|
+
| { type: "stream_keepalive" }
|
|
172
173
|
| { type: "tool_start"; tool: string; input: Record<string, string> }
|
|
173
174
|
| {
|
|
174
175
|
type: "tool_done";
|
|
@@ -50,7 +50,12 @@ import type {
|
|
|
50
50
|
ChatThreadSnapshot,
|
|
51
51
|
} from "./use-chat-threads.js";
|
|
52
52
|
import { useAgentEngineConfigured } from "./use-agent-engine-configured.js";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
getActiveRun,
|
|
55
|
+
resolveReconnectAfterSeq,
|
|
56
|
+
setActiveRun,
|
|
57
|
+
updateActiveRunSeq,
|
|
58
|
+
} from "./active-run-state.js";
|
|
54
59
|
import {
|
|
55
60
|
AgentAutoContinueSignal,
|
|
56
61
|
type ContentPart,
|
|
@@ -1247,6 +1252,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1247
1252
|
// When stop is clicked during reconnect, keep content visible (don't wipe it)
|
|
1248
1253
|
const [reconnectFrozen, setReconnectFrozen] = useState(false);
|
|
1249
1254
|
const reconnectRunIdRef = useRef<string | null>(null);
|
|
1255
|
+
const [reconnectAfterSeq, setReconnectAfterSeq] = useState(0);
|
|
1250
1256
|
const reconnectAbortRef = useRef<AbortController | null>(null);
|
|
1251
1257
|
// Nuclear stop: user clicked stop. Clears the stop button/indicator AND
|
|
1252
1258
|
// lets new submissions go through immediately — prevents the "stuck
|
|
@@ -1456,6 +1462,13 @@ const AssistantChatInner = forwardRef<
|
|
|
1456
1462
|
if (reconnectRunIdRef.current === runId) return true;
|
|
1457
1463
|
|
|
1458
1464
|
reconnectRunIdRef.current = runId;
|
|
1465
|
+
const afterSeq = resolveReconnectAfterSeq(threadId, runId);
|
|
1466
|
+
setReconnectAfterSeq(afterSeq);
|
|
1467
|
+
setActiveRun({
|
|
1468
|
+
threadId,
|
|
1469
|
+
runId,
|
|
1470
|
+
lastSeq: afterSeq > 0 ? afterSeq - 1 : -1,
|
|
1471
|
+
});
|
|
1459
1472
|
setIsReconnecting(true);
|
|
1460
1473
|
setReconnectFrozen(false);
|
|
1461
1474
|
setReconnectContent([]);
|
|
@@ -1498,9 +1511,16 @@ const AssistantChatInner = forwardRef<
|
|
|
1498
1511
|
const streamReconnect = async () => {
|
|
1499
1512
|
let noProgressDuringReconnect = false;
|
|
1500
1513
|
let latestContent: ContentPart[] = [];
|
|
1514
|
+
const threadPollInterval =
|
|
1515
|
+
afterSeq > 0
|
|
1516
|
+
? window.setInterval(() => {
|
|
1517
|
+
if (reconnectRunIdRef.current !== runId) return;
|
|
1518
|
+
void refreshThreadFromServer();
|
|
1519
|
+
}, 2000)
|
|
1520
|
+
: undefined;
|
|
1501
1521
|
try {
|
|
1502
1522
|
const sseRes = await fetch(
|
|
1503
|
-
`${apiUrl}/runs/${encodeURIComponent(runId)}/events?after
|
|
1523
|
+
`${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${afterSeq}`,
|
|
1504
1524
|
{ signal: abortCtrl.signal },
|
|
1505
1525
|
);
|
|
1506
1526
|
if (sseRes.ok && sseRes.body) {
|
|
@@ -1511,6 +1531,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1511
1531
|
let rafPending = false;
|
|
1512
1532
|
let latestSnapshot: ContentPart[] = [];
|
|
1513
1533
|
const scheduleUpdate = (snapshot: ContentPart[]) => {
|
|
1534
|
+
if (afterSeq > 0) return;
|
|
1514
1535
|
latestSnapshot = snapshot;
|
|
1515
1536
|
if (rafPending) return;
|
|
1516
1537
|
rafPending = true;
|
|
@@ -1526,8 +1547,11 @@ const AssistantChatInner = forwardRef<
|
|
|
1526
1547
|
toolCallCounter,
|
|
1527
1548
|
tabId,
|
|
1528
1549
|
scheduleUpdate,
|
|
1550
|
+
(seq) => updateActiveRunSeq(seq),
|
|
1529
1551
|
);
|
|
1530
|
-
|
|
1552
|
+
if (afterSeq === 0) {
|
|
1553
|
+
setReconnectContent([...content]);
|
|
1554
|
+
}
|
|
1531
1555
|
}
|
|
1532
1556
|
} catch (err) {
|
|
1533
1557
|
if (
|
|
@@ -1543,6 +1567,9 @@ const AssistantChatInner = forwardRef<
|
|
|
1543
1567
|
noProgressDuringReconnect = true;
|
|
1544
1568
|
}
|
|
1545
1569
|
} finally {
|
|
1570
|
+
if (threadPollInterval !== undefined) {
|
|
1571
|
+
window.clearInterval(threadPollInterval);
|
|
1572
|
+
}
|
|
1546
1573
|
clearInterval(watchdog);
|
|
1547
1574
|
clearTimeout(maxReconnectTimer);
|
|
1548
1575
|
}
|
|
@@ -1570,9 +1597,17 @@ const AssistantChatInner = forwardRef<
|
|
|
1570
1597
|
} catch {
|
|
1571
1598
|
// Best effort — the important part is unwinding the UI.
|
|
1572
1599
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1600
|
+
if (afterSeq > 0) {
|
|
1601
|
+
// Tail-resume only replays new events; never freeze that slice as a
|
|
1602
|
+
// complete assistant turn — the server thread is authoritative.
|
|
1603
|
+
await refreshThreadFromServer();
|
|
1604
|
+
setReconnectContent([]);
|
|
1605
|
+
setReconnectFrozen(false);
|
|
1606
|
+
} else {
|
|
1607
|
+
settleInterruptedToolCalls(latestContent);
|
|
1608
|
+
setReconnectContent([...latestContent]);
|
|
1609
|
+
setReconnectFrozen(latestContent.length > 0);
|
|
1610
|
+
}
|
|
1576
1611
|
setRunErrorInfo({
|
|
1577
1612
|
message:
|
|
1578
1613
|
"The previous agent run stopped producing visible progress while reconnecting, so it was stopped before it could keep looping.",
|
|
@@ -1584,6 +1619,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1584
1619
|
reconnectAbortRef.current = null;
|
|
1585
1620
|
setIsReconnecting(false);
|
|
1586
1621
|
reconnectRunIdRef.current = null;
|
|
1622
|
+
setReconnectAfterSeq(0);
|
|
1587
1623
|
window.dispatchEvent(
|
|
1588
1624
|
new CustomEvent("agentNative.chatRunning", {
|
|
1589
1625
|
detail: { isRunning: false, tabId: tabId || threadId },
|
|
@@ -1610,6 +1646,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1610
1646
|
reconnectAbortRef.current = null;
|
|
1611
1647
|
setIsReconnecting(false);
|
|
1612
1648
|
reconnectRunIdRef.current = null;
|
|
1649
|
+
setReconnectAfterSeq(0);
|
|
1613
1650
|
window.dispatchEvent(
|
|
1614
1651
|
new CustomEvent("agentNative.chatRunning", {
|
|
1615
1652
|
detail: { isRunning: false, tabId: tabId || threadId },
|
|
@@ -2360,6 +2397,7 @@ const AssistantChatInner = forwardRef<
|
|
|
2360
2397
|
reconnectAbortRef.current?.abort();
|
|
2361
2398
|
reconnectAbortRef.current = null;
|
|
2362
2399
|
reconnectRunIdRef.current = null;
|
|
2400
|
+
setReconnectAfterSeq(0);
|
|
2363
2401
|
setIsReconnecting(false);
|
|
2364
2402
|
setReconnectFrozen(reconnectContent.length > 0);
|
|
2365
2403
|
}
|
|
@@ -3159,6 +3197,7 @@ const AssistantChatInner = forwardRef<
|
|
|
3159
3197
|
/>
|
|
3160
3198
|
)}
|
|
3161
3199
|
{(isReconnecting || reconnectFrozen) &&
|
|
3200
|
+
reconnectAfterSeq === 0 &&
|
|
3162
3201
|
reconnectContent.length > 0 && (
|
|
3163
3202
|
<ReconnectStreamMessage content={reconnectContent} />
|
|
3164
3203
|
)}
|
|
@@ -35,3 +35,19 @@ export function clearActiveRun(): void {
|
|
|
35
35
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
36
36
|
} catch {}
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/** Resume reconnect SSE after the last seen event (0 = replay from the start). */
|
|
40
|
+
export function resolveReconnectAfterSeq(
|
|
41
|
+
threadId: string,
|
|
42
|
+
runId: string,
|
|
43
|
+
): number {
|
|
44
|
+
const stored = getActiveRun();
|
|
45
|
+
if (
|
|
46
|
+
stored?.threadId === threadId &&
|
|
47
|
+
stored?.runId === runId &&
|
|
48
|
+
Number.isFinite(stored.lastSeq)
|
|
49
|
+
) {
|
|
50
|
+
return stored.lastSeq + 1;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -357,6 +357,37 @@ function optionKey(option: GuidedQuestionOption): string {
|
|
|
357
357
|
return `${option.value.toLowerCase()}::${option.label.toLowerCase()}`;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/** Stable content hash so poll refreshes do not reset in-progress answers. */
|
|
361
|
+
export function guidedQuestionsFingerprint(
|
|
362
|
+
questions: GuidedQuestion[],
|
|
363
|
+
): string {
|
|
364
|
+
return JSON.stringify(
|
|
365
|
+
questions.map((question) => ({
|
|
366
|
+
id: question.id,
|
|
367
|
+
type: question.type,
|
|
368
|
+
header: question.header ?? null,
|
|
369
|
+
question: question.question,
|
|
370
|
+
description: question.description ?? null,
|
|
371
|
+
multiSelect: question.multiSelect ?? false,
|
|
372
|
+
required: question.required ?? false,
|
|
373
|
+
allowOther: question.allowOther ?? null,
|
|
374
|
+
includeExplore: question.includeExplore ?? null,
|
|
375
|
+
includeDecide: question.includeDecide ?? null,
|
|
376
|
+
min: question.min ?? null,
|
|
377
|
+
max: question.max ?? null,
|
|
378
|
+
step: question.step ?? null,
|
|
379
|
+
placeholder: question.placeholder ?? null,
|
|
380
|
+
options: (question.options ?? question.choices ?? []).map((option) => ({
|
|
381
|
+
label: option.label,
|
|
382
|
+
value: option.value,
|
|
383
|
+
color: option.color ?? null,
|
|
384
|
+
description: option.description ?? null,
|
|
385
|
+
recommended: option.recommended ?? false,
|
|
386
|
+
})),
|
|
387
|
+
})),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
360
391
|
function withDefaultOptions(question: GuidedQuestion): GuidedQuestionOption[] {
|
|
361
392
|
const base = question.options ?? question.choices ?? [];
|
|
362
393
|
const seen = new Set(base.map(optionKey));
|
|
@@ -403,10 +434,14 @@ export function GuidedQuestionFlow({
|
|
|
403
434
|
className,
|
|
404
435
|
}: GuidedQuestionFlowProps) {
|
|
405
436
|
const [answers, setAnswers] = useState<GuidedQuestionAnswers>({});
|
|
437
|
+
const questionsFingerprint = useMemo(
|
|
438
|
+
() => guidedQuestionsFingerprint(questions),
|
|
439
|
+
[questions],
|
|
440
|
+
);
|
|
406
441
|
|
|
407
442
|
useEffect(() => {
|
|
408
443
|
setAnswers({});
|
|
409
|
-
}, [
|
|
444
|
+
}, [questionsFingerprint]);
|
|
410
445
|
|
|
411
446
|
const setAnswer = useCallback((id: string, value: unknown) => {
|
|
412
447
|
setAnswers((prev) => ({ ...prev, [id]: value }));
|
|
@@ -663,23 +698,26 @@ function OptionButton({
|
|
|
663
698
|
<button
|
|
664
699
|
type="button"
|
|
665
700
|
onClick={onClick}
|
|
701
|
+
aria-pressed={selected}
|
|
666
702
|
className={cn(
|
|
667
703
|
"group flex min-h-[56px] cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors",
|
|
668
704
|
selected
|
|
669
|
-
? "border-primary bg-primary/10 text-primary"
|
|
705
|
+
? "border-primary bg-primary/10 text-foreground ring-2 ring-primary/25"
|
|
670
706
|
: "border-border bg-muted/30 text-muted-foreground hover:border-muted-foreground/50 hover:bg-muted/45 hover:text-foreground",
|
|
671
707
|
)}
|
|
672
708
|
>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
709
|
+
<span
|
|
710
|
+
className={cn(
|
|
711
|
+
"mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center border",
|
|
712
|
+
multiSelect ? "rounded-sm" : "rounded-full",
|
|
713
|
+
selected
|
|
714
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
715
|
+
: "border-border bg-background",
|
|
716
|
+
)}
|
|
717
|
+
aria-hidden
|
|
718
|
+
>
|
|
719
|
+
{selected && <IconCheck className="h-3 w-3" />}
|
|
720
|
+
</span>
|
|
683
721
|
<span className="min-w-0 flex-1">
|
|
684
722
|
<span className="flex flex-wrap items-center gap-1.5 text-sm font-medium leading-5">
|
|
685
723
|
{option.label}
|
|
@@ -940,6 +978,17 @@ export function useGuidedQuestionFlow({
|
|
|
940
978
|
[queryKey, normalizedBrowserTabId],
|
|
941
979
|
);
|
|
942
980
|
|
|
981
|
+
const resolvedRefetchInterval =
|
|
982
|
+
refetchInterval === false
|
|
983
|
+
? false
|
|
984
|
+
: (query: { state: { data?: GuidedQuestionPayload | null } }) => {
|
|
985
|
+
const activeQuestions = query.state.data?.questions;
|
|
986
|
+
if (Array.isArray(activeQuestions) && activeQuestions.length > 0) {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
return refetchInterval;
|
|
990
|
+
};
|
|
991
|
+
|
|
943
992
|
const { data } = useQuery({
|
|
944
993
|
queryKey: resolvedQueryKey,
|
|
945
994
|
queryFn: async () => {
|
|
@@ -951,9 +1000,7 @@ export function useGuidedQuestionFlow({
|
|
|
951
1000
|
try {
|
|
952
1001
|
const parsed = JSON.parse(text);
|
|
953
1002
|
if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
|
|
954
|
-
return
|
|
955
|
-
_ts: number;
|
|
956
|
-
};
|
|
1003
|
+
return parsed as GuidedQuestionPayload;
|
|
957
1004
|
}
|
|
958
1005
|
} catch {
|
|
959
1006
|
return null;
|
|
@@ -967,13 +1014,22 @@ export function useGuidedQuestionFlow({
|
|
|
967
1014
|
(await read(stateKey))
|
|
968
1015
|
);
|
|
969
1016
|
},
|
|
970
|
-
refetchInterval,
|
|
1017
|
+
refetchInterval: resolvedRefetchInterval,
|
|
971
1018
|
structuralSharing: false,
|
|
972
1019
|
});
|
|
973
1020
|
|
|
974
1021
|
useEffect(() => {
|
|
975
1022
|
if (Array.isArray(data?.questions) && data.questions.length > 0) {
|
|
976
|
-
setPayload(
|
|
1023
|
+
setPayload((prev) => {
|
|
1024
|
+
if (
|
|
1025
|
+
prev &&
|
|
1026
|
+
guidedQuestionsFingerprint(prev.questions) ===
|
|
1027
|
+
guidedQuestionsFingerprint(data.questions)
|
|
1028
|
+
) {
|
|
1029
|
+
return prev;
|
|
1030
|
+
}
|
|
1031
|
+
return data;
|
|
1032
|
+
});
|
|
977
1033
|
} else {
|
|
978
1034
|
setPayload(null);
|
|
979
1035
|
}
|
|
@@ -357,6 +357,10 @@ export function processEvent(
|
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
if (ev.type === "stream_keepalive") {
|
|
361
|
+
return { action: "continue" };
|
|
362
|
+
}
|
|
363
|
+
|
|
360
364
|
if (ev.type === "activity") {
|
|
361
365
|
const tool = ev.tool?.trim() || undefined;
|
|
362
366
|
const label = humanizeToolLabelText(ev.label ?? "Working", tool);
|
|
@@ -868,6 +872,7 @@ export async function readSSEStreamRaw(
|
|
|
868
872
|
toolCallCounter: { value: number },
|
|
869
873
|
tabId: string | undefined,
|
|
870
874
|
onUpdate: (content: ContentPart[]) => void,
|
|
875
|
+
onSeq?: (seq: number) => void,
|
|
871
876
|
): Promise<void> {
|
|
872
877
|
const reader = body.getReader();
|
|
873
878
|
const decoder = new TextDecoder();
|
|
@@ -907,6 +912,10 @@ export async function readSSEStreamRaw(
|
|
|
907
912
|
sawDataEvent = true;
|
|
908
913
|
lastMeaningfulEventAt = Date.now();
|
|
909
914
|
|
|
915
|
+
if (ev.seq !== undefined && onSeq) {
|
|
916
|
+
onSeq(ev.seq);
|
|
917
|
+
}
|
|
918
|
+
|
|
910
919
|
const { action, autoContinue } = processEvent(
|
|
911
920
|
ev,
|
|
912
921
|
content,
|
|
@@ -36,6 +36,10 @@ BUILDER_PRIVATE_KEY=
|
|
|
36
36
|
BUILDER_PUBLIC_KEY=
|
|
37
37
|
BUILDER_USER_ID=
|
|
38
38
|
|
|
39
|
+
# Local ai-services LLM gateway. Leave unset to use the hosted Builder gateway.
|
|
40
|
+
# ai-services listens on API_PORT (default 8080): /agent-native/gateway/v1/messages
|
|
41
|
+
BUILDER_GATEWAY_BASE_URL=
|
|
42
|
+
|
|
39
43
|
# Builder app creation / branching. In local dev these can live in .env.
|
|
40
44
|
# In production, configure deploy env vars instead; production app creation
|
|
41
45
|
# never writes credentials or project IDs to the filesystem.
|
|
@@ -47,6 +47,18 @@ const questionSchema = z.object({
|
|
|
47
47
|
includeDecide: z.boolean().optional(),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
function normalizeDesignQuestions(
|
|
51
|
+
questions: z.infer<typeof questionSchema>[],
|
|
52
|
+
): z.infer<typeof questionSchema>[] {
|
|
53
|
+
return questions.map((question) => ({
|
|
54
|
+
...question,
|
|
55
|
+
// The agent supplies Explore/Decide choices explicitly when needed.
|
|
56
|
+
// Default injection duplicates cards on every question in the form.
|
|
57
|
+
includeExplore: question.includeExplore ?? false,
|
|
58
|
+
includeDecide: question.includeDecide ?? false,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
export default defineAction({
|
|
51
63
|
description:
|
|
52
64
|
"Show a Claude Design-style question form in the Design editor before " +
|
|
@@ -95,6 +107,8 @@ export default defineAction({
|
|
|
95
107
|
}) => {
|
|
96
108
|
await assertAccess("design", designId, "editor");
|
|
97
109
|
|
|
110
|
+
const normalizedQuestions = normalizeDesignQuestions(questions);
|
|
111
|
+
|
|
98
112
|
await writeAppState(designQuestionsStateKey(designId), {
|
|
99
113
|
designId,
|
|
100
114
|
title: title ?? "Quick questions before I design",
|
|
@@ -103,7 +117,7 @@ export default defineAction({
|
|
|
103
117
|
"Pick what matters. Use Other for specifics, or let the agent decide.",
|
|
104
118
|
skipLabel: skipLabel ?? "Decide for me",
|
|
105
119
|
submitLabel: submitLabel ?? "Continue",
|
|
106
|
-
questions,
|
|
120
|
+
questions: normalizedQuestions,
|
|
107
121
|
});
|
|
108
122
|
|
|
109
123
|
return {
|
|
@@ -8,10 +8,16 @@ import {
|
|
|
8
8
|
// legitimately take several minutes, so avoid treating normal latency as
|
|
9
9
|
// failure.
|
|
10
10
|
const GENERATION_ORPHAN_TIMEOUT_MS = 30 * 60_000;
|
|
11
|
+
// Auto-continue briefly sets isRunning=false between gateway continuations.
|
|
12
|
+
// Debounce stop handling so we do not flash "generation complete" mid-turn.
|
|
13
|
+
const CHAT_STOP_DEBOUNCE_MS = 4_000;
|
|
11
14
|
|
|
12
15
|
interface UseAgentGeneratingOptions {
|
|
13
16
|
onComplete?: (tabId: string | null) => void;
|
|
14
17
|
onStale?: (tabId: string | null) => void;
|
|
18
|
+
/** When chat starts on a tab we did not open, adopt it if this returns true. */
|
|
19
|
+
shouldAdoptRunningTab?: () => boolean;
|
|
20
|
+
onAdoptRunningTab?: (tabId: string) => void;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -23,9 +29,17 @@ export function useAgentGenerating(options: UseAgentGeneratingOptions = {}) {
|
|
|
23
29
|
const [generating, setGenerating] = useState(false);
|
|
24
30
|
const activeTabIdRef = useRef<string | null>(null);
|
|
25
31
|
const timeoutRef = useRef<number | null>(null);
|
|
32
|
+
const stopDebounceRef = useRef<number | null>(null);
|
|
26
33
|
const callbacksRef = useRef(options);
|
|
27
34
|
callbacksRef.current = options;
|
|
28
35
|
|
|
36
|
+
const clearStopDebounce = useCallback(() => {
|
|
37
|
+
if (stopDebounceRef.current) {
|
|
38
|
+
window.clearTimeout(stopDebounceRef.current);
|
|
39
|
+
stopDebounceRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
29
43
|
const clearGenerationTimeout = useCallback(() => {
|
|
30
44
|
if (timeoutRef.current) {
|
|
31
45
|
window.clearTimeout(timeoutRef.current);
|
|
@@ -35,9 +49,10 @@ export function useAgentGenerating(options: UseAgentGeneratingOptions = {}) {
|
|
|
35
49
|
|
|
36
50
|
const reset = useCallback(() => {
|
|
37
51
|
clearGenerationTimeout();
|
|
52
|
+
clearStopDebounce();
|
|
38
53
|
activeTabIdRef.current = null;
|
|
39
54
|
setGenerating(false);
|
|
40
|
-
}, [clearGenerationTimeout]);
|
|
55
|
+
}, [clearGenerationTimeout, clearStopDebounce]);
|
|
41
56
|
|
|
42
57
|
const startGenerationTimeout = useCallback(
|
|
43
58
|
(tabId: string | null) => {
|
|
@@ -68,25 +83,49 @@ export function useAgentGenerating(options: UseAgentGeneratingOptions = {}) {
|
|
|
68
83
|
const eventTabId =
|
|
69
84
|
typeof detail.tabId === "string" ? detail.tabId : null;
|
|
70
85
|
|
|
71
|
-
if (!activeTabIdRef.current && detail.isRunning)
|
|
72
|
-
|
|
86
|
+
if (!activeTabIdRef.current && detail.isRunning) {
|
|
87
|
+
if (eventTabId && callbacksRef.current.shouldAdoptRunningTab?.()) {
|
|
88
|
+
activeTabIdRef.current = eventTabId;
|
|
89
|
+
callbacksRef.current.onAdoptRunningTab?.(eventTabId);
|
|
90
|
+
setGenerating(true);
|
|
91
|
+
startGenerationTimeout(eventTabId);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (eventTabId && eventTabId !== activeTabIdRef.current) {
|
|
97
|
+
if (!detail.isRunning && !activeTabIdRef.current) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
73
102
|
|
|
74
103
|
if (!detail.isRunning) {
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
clearStopDebounce();
|
|
105
|
+
const tabId = activeTabIdRef.current;
|
|
106
|
+
stopDebounceRef.current = window.setTimeout(() => {
|
|
107
|
+
stopDebounceRef.current = null;
|
|
108
|
+
if (activeTabIdRef.current !== tabId) return;
|
|
109
|
+
callbacksRef.current.onComplete?.(tabId);
|
|
110
|
+
reset();
|
|
111
|
+
}, CHAT_STOP_DEBOUNCE_MS);
|
|
77
112
|
return;
|
|
78
113
|
}
|
|
114
|
+
clearStopDebounce();
|
|
79
115
|
setGenerating(true);
|
|
80
116
|
startGenerationTimeout(activeTabIdRef.current);
|
|
81
117
|
}
|
|
82
118
|
};
|
|
83
119
|
window.addEventListener("agentNative.chatRunning", handler);
|
|
84
120
|
return () => window.removeEventListener("agentNative.chatRunning", handler);
|
|
85
|
-
}, [reset, startGenerationTimeout]);
|
|
121
|
+
}, [clearStopDebounce, reset, startGenerationTimeout]);
|
|
86
122
|
|
|
87
123
|
useEffect(() => {
|
|
88
|
-
return () =>
|
|
89
|
-
|
|
124
|
+
return () => {
|
|
125
|
+
clearGenerationTimeout();
|
|
126
|
+
clearStopDebounce();
|
|
127
|
+
};
|
|
128
|
+
}, [clearGenerationTimeout, clearStopDebounce]);
|
|
90
129
|
|
|
91
130
|
const submit = useCallback(
|
|
92
131
|
(
|
|
@@ -428,6 +428,12 @@ export default function DesignEditor() {
|
|
|
428
428
|
} = useAgentGenerating({
|
|
429
429
|
onComplete: handleGenerationComplete,
|
|
430
430
|
onStale: markGenerationStale,
|
|
431
|
+
shouldAdoptRunningTab: () =>
|
|
432
|
+
Boolean(id) && !generationOutputReadyRef.current,
|
|
433
|
+
onAdoptRunningTab: (tabId) => {
|
|
434
|
+
setGenerationChatTabId(tabId);
|
|
435
|
+
setHasPendingGeneration(true);
|
|
436
|
+
},
|
|
431
437
|
});
|
|
432
438
|
const handleQuestionFlowContinue = useCallback(
|
|
433
439
|
(runTabId: string) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder-engine.d.ts","sourceRoot":"","sources":["../../../src/agent/engine/builder-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAwBpB,eAAO,MAAM,oBAAoB,EAAE,kBAMlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,wOAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"builder-engine.d.ts","sourceRoot":"","sources":["../../../src/agent/engine/builder-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAwBpB,eAAO,MAAM,oBAAoB,EAAE,kBAMlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,wOAAuC,CAAC;AAW7E,eAAO,MAAM,qBAAqB,qBAAoC,CAAC;AA+qBvE,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACpC,WAAW,CAEb"}
|
|
@@ -33,6 +33,8 @@ export const BUILDER_SUPPORTED_MODELS = BUILDER_MODEL_CONFIG.supportedModels;
|
|
|
33
33
|
// (Netlify synchronous Functions are 60s) hard-kill the invocation.
|
|
34
34
|
const DEFAULT_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
|
|
35
35
|
const MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
|
|
36
|
+
/** Local ai-services has no serverless wall; allow longer streams in dev. */
|
|
37
|
+
const MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = 180_000;
|
|
36
38
|
const BUILDER_GATEWAY_NETWORK_ERROR_CODE = "builder_gateway_network_error";
|
|
37
39
|
export const BUILDER_DEFAULT_MODEL = BUILDER_MODEL_CONFIG.defaultModel;
|
|
38
40
|
/**
|
|
@@ -442,6 +444,9 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
|
|
|
442
444
|
: "",
|
|
443
445
|
};
|
|
444
446
|
break;
|
|
447
|
+
case "heartbeat":
|
|
448
|
+
yield { type: "gateway-heartbeat" };
|
|
449
|
+
break;
|
|
445
450
|
case "tool-call": {
|
|
446
451
|
flushPending();
|
|
447
452
|
parts.push({
|
|
@@ -640,15 +645,28 @@ function htmlToText(html) {
|
|
|
640
645
|
export function createBuilderEngine(_config = {}) {
|
|
641
646
|
return new BuilderEngine();
|
|
642
647
|
}
|
|
648
|
+
function resolveMaxBuilderGatewayTimeoutMs() {
|
|
649
|
+
try {
|
|
650
|
+
const base = getBuilderGatewayBaseUrl();
|
|
651
|
+
if (/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/i.test(base)) {
|
|
652
|
+
return MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// ignore malformed override
|
|
657
|
+
}
|
|
658
|
+
return MAX_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
659
|
+
}
|
|
643
660
|
function getBuilderGatewayTimeoutMs() {
|
|
644
661
|
const raw = process.env.AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS;
|
|
662
|
+
const maxMs = resolveMaxBuilderGatewayTimeoutMs();
|
|
645
663
|
if (!raw)
|
|
646
|
-
return
|
|
664
|
+
return maxMs;
|
|
647
665
|
const parsed = Number(raw);
|
|
648
666
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
649
|
-
return
|
|
667
|
+
return maxMs;
|
|
650
668
|
}
|
|
651
|
-
return Math.min(parsed,
|
|
669
|
+
return Math.min(parsed, maxMs);
|
|
652
670
|
}
|
|
653
671
|
function createGatewayAbortSignal(parentSignal, timeoutMs) {
|
|
654
672
|
const controller = new AbortController();
|