@agent-native/core 0.84.2 → 0.84.3
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/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +9 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +38 -8
- package/corpus/core/src/agent/thread-data-builder.ts +6 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +208 -9
- package/corpus/core/src/client/sharing/ShareButton.tsx +70 -21
- package/corpus/core/src/client/sse-event-processor.ts +15 -0
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +129 -100
- package/corpus/templates/design/changelog/2026-06-30-agent-chat-now-reports-saved-design-generations-clearly-when.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-apply-styles-now-appears-only-for-localhost-visual-edit-scre.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-now-make-export-and-agent-handoff-easier-to-no.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-share-general-access-menu-stays-open-when-choosing-organization.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +9 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +2 -2
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +31 -8
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +2 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +5 -0
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +179 -8
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +6 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +25 -11
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +4 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +13 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- af049a8: Improve agent-chat timeout recovery so completed tool actions end with a clear saved-result note instead of a generic connection failure, and bound repeated no-progress tool stalls.
|
|
8
|
+
- af049a8: Allow share panels to hide copyable link fields, omit the bottom Done action, and render compact host-provided footer actions.
|
|
9
|
+
- af049a8: Stack nested share popover menus above their parent panel so users can change visibility.
|
|
10
|
+
|
|
3
11
|
## 0.84.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.3",
|
|
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": {
|
|
@@ -3330,7 +3330,10 @@ export async function runAgentLoop(opts: {
|
|
|
3330
3330
|
}
|
|
3331
3331
|
|
|
3332
3332
|
const DEFAULT_TOOL_RESULT_CHARS = 50_000;
|
|
3333
|
-
|
|
3333
|
+
// Default action tools should not undercut durable/background runs. The
|
|
3334
|
+
// run-manager still aborts foreground hosted runs around 40s, while
|
|
3335
|
+
// background runs get nearly the full 15-minute function budget.
|
|
3336
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 12 * 60_000;
|
|
3334
3337
|
const toolTimeoutMs =
|
|
3335
3338
|
actionEntry.timeoutMs ??
|
|
3336
3339
|
opts.toolLimits?.timeoutMs ??
|
|
@@ -3798,7 +3801,11 @@ export async function runAgentLoop(opts: {
|
|
|
3798
3801
|
input: toolCall.input as Record<string, unknown>,
|
|
3799
3802
|
result,
|
|
3800
3803
|
...(isError ? { isError: true } : {}),
|
|
3801
|
-
...(isError
|
|
3804
|
+
...(isError
|
|
3805
|
+
? { completedSideEffect: false }
|
|
3806
|
+
: actionEntry.readOnly !== true
|
|
3807
|
+
? { completedSideEffect: true }
|
|
3808
|
+
: {}),
|
|
3802
3809
|
...(mcpApp ? { mcpApp } : {}),
|
|
3803
3810
|
...(actionEntry.chatUI ? { chatUI: actionEntry.chatUI } : {}),
|
|
3804
3811
|
});
|
|
@@ -79,6 +79,24 @@ async function appendToolCallJournalNote(
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
async function hasCompletedSideEffectToolCallInCurrentTurn(
|
|
83
|
+
threadId: string | undefined,
|
|
84
|
+
): Promise<boolean> {
|
|
85
|
+
if (!threadId) return false;
|
|
86
|
+
try {
|
|
87
|
+
const events = await getCurrentTurnEventsForThread(threadId);
|
|
88
|
+
if (events.length === 0) return false;
|
|
89
|
+
return events.some(
|
|
90
|
+
(event) =>
|
|
91
|
+
event.type === "tool_done" &&
|
|
92
|
+
event.completedSideEffect === true &&
|
|
93
|
+
event.isError !== true,
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
/**
|
|
83
101
|
* Cap on continuation iterations inside a single
|
|
84
102
|
* `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
|
|
@@ -102,12 +120,12 @@ export const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
|
|
|
102
120
|
* exhausts its in-invocation continuation budget without finishing. Generic and
|
|
103
121
|
* framework-level (not app-specific). Mirrors the `reliable-mutations` skill's
|
|
104
122
|
* "fail loud, retry as a single bulk action" guidance so the user understands
|
|
105
|
-
* the turn stopped before finishing
|
|
106
|
-
*
|
|
123
|
+
* the turn stopped before finishing without implying that earlier completed
|
|
124
|
+
* tool calls did not persist. */
|
|
107
125
|
export const RUN_BUDGET_EXHAUSTED_MESSAGE =
|
|
108
|
-
"I ran out of time before finishing this step
|
|
109
|
-
"I stopped rather than
|
|
110
|
-
"
|
|
126
|
+
"I ran out of time before finishing this step. " +
|
|
127
|
+
"I stopped rather than keep retrying silently. " +
|
|
128
|
+
"Check any completed tool cards above before retrying, ideally as one smaller follow-up.";
|
|
111
129
|
|
|
112
130
|
/**
|
|
113
131
|
* Internal entry point used by the agent-chat plugin's run handler. Wraps
|
|
@@ -189,7 +207,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
|
|
|
189
207
|
// Clear partial text the client received before the abort so the
|
|
190
208
|
// resumed model doesn't re-emit it and produce duplicated output.
|
|
191
209
|
lastAttemptWasUnfinishedContinuation = true;
|
|
192
|
-
|
|
210
|
+
if (
|
|
211
|
+
!(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
|
|
212
|
+
) {
|
|
213
|
+
opts.send({ type: "clear" });
|
|
214
|
+
}
|
|
193
215
|
appendAgentLoopContinuation(opts.messages, "run_timeout");
|
|
194
216
|
await appendToolCallJournalNote(opts.messages, opts.threadId);
|
|
195
217
|
continue;
|
|
@@ -209,7 +231,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
|
|
|
209
231
|
// the in-memory messages array, so the next attempt re-emits it).
|
|
210
232
|
if (!upstreamSignal.aborted && isResumableEngineError(err)) {
|
|
211
233
|
lastAttemptWasUnfinishedContinuation = true;
|
|
212
|
-
|
|
234
|
+
if (
|
|
235
|
+
!(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
|
|
236
|
+
) {
|
|
237
|
+
opts.send({ type: "clear" });
|
|
238
|
+
}
|
|
213
239
|
appendAgentLoopContinuation(
|
|
214
240
|
opts.messages,
|
|
215
241
|
continuationReasonForResumableError(err),
|
|
@@ -234,7 +260,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
|
|
|
234
260
|
if (!upstreamSignal.aborted && lastAttemptWasUnfinishedContinuation) {
|
|
235
261
|
// Discard any partial text already streamed for the unfinished attempt so
|
|
236
262
|
// the terminal message stands alone instead of trailing a half sentence.
|
|
237
|
-
|
|
263
|
+
// Preserve completed tool cards: they are the user's only durable proof
|
|
264
|
+
// that a side effect landed before the final assistant note timed out.
|
|
265
|
+
if (!(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))) {
|
|
266
|
+
opts.send({ type: "clear" });
|
|
267
|
+
}
|
|
238
268
|
opts.send({
|
|
239
269
|
type: "error",
|
|
240
270
|
error: RUN_BUDGET_EXHAUSTED_MESSAGE,
|
|
@@ -18,6 +18,8 @@ interface ContentPart {
|
|
|
18
18
|
argsText?: string;
|
|
19
19
|
args?: Record<string, string>;
|
|
20
20
|
result?: string;
|
|
21
|
+
isError?: boolean;
|
|
22
|
+
completedSideEffect?: boolean;
|
|
21
23
|
mcpApp?: AgentMcpAppPayload;
|
|
22
24
|
chatUI?: ActionChatUIConfig;
|
|
23
25
|
}
|
|
@@ -159,6 +161,10 @@ export function buildAssistantMessage(
|
|
|
159
161
|
part.result === undefined
|
|
160
162
|
) {
|
|
161
163
|
part.result = event.result ?? "";
|
|
164
|
+
if (event.isError !== undefined) part.isError = event.isError;
|
|
165
|
+
if (event.completedSideEffect !== undefined) {
|
|
166
|
+
part.completedSideEffect = event.completedSideEffect;
|
|
167
|
+
}
|
|
162
168
|
if (event.mcpApp) part.mcpApp = event.mcpApp;
|
|
163
169
|
if (event.chatUI) part.chatUI = event.chatUI;
|
|
164
170
|
break;
|
|
@@ -697,6 +697,41 @@ function hasContinuationProgress(content: ContentPart[]): boolean {
|
|
|
697
697
|
);
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
function lastCompletedSideEffectTool(
|
|
701
|
+
content: ContentPart[],
|
|
702
|
+
): Extract<ContentPart, { type: "tool-call" }> | undefined {
|
|
703
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
704
|
+
const part = content[i];
|
|
705
|
+
if (
|
|
706
|
+
part.type === "tool-call" &&
|
|
707
|
+
part.activity !== true &&
|
|
708
|
+
part.result !== undefined &&
|
|
709
|
+
part.isError !== true &&
|
|
710
|
+
part.completedSideEffect === true
|
|
711
|
+
) {
|
|
712
|
+
return part;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function humanizeActionName(toolName: string): string {
|
|
719
|
+
return toolName
|
|
720
|
+
.replace(/^agent:/, "")
|
|
721
|
+
.replace(/[-_]+/g, " ")
|
|
722
|
+
.trim();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function completedToolTimeoutMessage(toolName: string): string {
|
|
726
|
+
if (toolName === "generate-design" || toolName === "update-design") {
|
|
727
|
+
return "The design was saved, but the assistant timed out before sending its final note. You can open the generated design from the completed tool card above.";
|
|
728
|
+
}
|
|
729
|
+
if (toolName === "present-design-variants") {
|
|
730
|
+
return "The design variants were saved, but the assistant timed out before sending its final note. You can review the completed variants from the tool card above.";
|
|
731
|
+
}
|
|
732
|
+
return `The ${humanizeActionName(toolName)} action completed, but the assistant timed out before sending its final response. The saved result is in the completed tool card above.`;
|
|
733
|
+
}
|
|
734
|
+
|
|
700
735
|
/**
|
|
701
736
|
* Signature of the *unique* sentence-like segments in a continuation's newly
|
|
702
737
|
* streamed text, used to detect a degenerate repetition loop. A stuck model
|
|
@@ -785,6 +820,8 @@ function toolContinuationKey(
|
|
|
785
820
|
stableJson(part.args),
|
|
786
821
|
part.result === undefined ? "pending" : "done",
|
|
787
822
|
part.result ?? "",
|
|
823
|
+
part.isError === true ? "error" : "",
|
|
824
|
+
part.completedSideEffect === true ? "side-effect" : "",
|
|
788
825
|
part.activity === true ? "activity" : "tool",
|
|
789
826
|
part.mcpApp ? "mcp-app" : "",
|
|
790
827
|
].join("\u0000");
|
|
@@ -851,6 +888,8 @@ function incrementalActionGuidance(tool: string): string | undefined {
|
|
|
851
888
|
case "generate-design":
|
|
852
889
|
case "update-design":
|
|
853
890
|
return "persist a minimal first version (fewer files) with `generate-design`, then refine individual files with `edit-design` search/replace instead of resending everything";
|
|
891
|
+
case "present-design-variants":
|
|
892
|
+
return "save compact but complete variant screens with `present-design-variants` first, keeping each HTML direction focused enough to finish, then refine the chosen direction with `generate-design` or `edit-design`";
|
|
854
893
|
case "create-visual-plan":
|
|
855
894
|
case "create-ui-plan":
|
|
856
895
|
case "create-plan-design":
|
|
@@ -1349,6 +1388,7 @@ export function createAgentChatAdapter(
|
|
|
1349
1388
|
// its own budget rather than inheriting the prior payload's.
|
|
1350
1389
|
let lastInFlightToolSignature: string | undefined;
|
|
1351
1390
|
let repeatedInFlightToolCount = 0;
|
|
1391
|
+
let recoveryGaveUpOnInFlightTool = false;
|
|
1352
1392
|
const MAX_REPEATED_INFLIGHT_TOOL_STALLS = 3;
|
|
1353
1393
|
const continuationHistoryFragments: string[] = [];
|
|
1354
1394
|
const structuredContinuationFragments: AgentChatStructuredMessage[] = [];
|
|
@@ -1390,6 +1430,9 @@ export function createAgentChatAdapter(
|
|
|
1390
1430
|
};
|
|
1391
1431
|
|
|
1392
1432
|
const exhaustedRecoveryMessage = (reason?: string): string => {
|
|
1433
|
+
if (recoveryGaveUpOnInFlightTool) {
|
|
1434
|
+
return "The agent got stuck waiting for the same tool to finish, so I stopped the automatic retries. The tool did not report a completed result.";
|
|
1435
|
+
}
|
|
1393
1436
|
if (recoveryGaveUpOnRepetition) {
|
|
1394
1437
|
return "The agent got stuck repeating the same response without finishing, so I stopped the automatic retries. This often happens when it tries to re-type a large pasted file into one action — starting a new chat, or asking for a smaller first step, usually gets it unstuck.";
|
|
1395
1438
|
}
|
|
@@ -1674,6 +1717,83 @@ export function createAgentChatAdapter(
|
|
|
1674
1717
|
return false;
|
|
1675
1718
|
};
|
|
1676
1719
|
|
|
1720
|
+
const reconnectBackgroundContinuationForRunTimeout =
|
|
1721
|
+
async function* (): AsyncGenerator<
|
|
1722
|
+
ChatModelRunResult,
|
|
1723
|
+
boolean,
|
|
1724
|
+
unknown
|
|
1725
|
+
> {
|
|
1726
|
+
if (!threadId || !runId) return false;
|
|
1727
|
+
const interruptedRunId = runId;
|
|
1728
|
+
const interruptedLastSeq = lastSeq;
|
|
1729
|
+
let lastActiveRunError: unknown = null;
|
|
1730
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1731
|
+
if (attempt > 0) {
|
|
1732
|
+
await delay(500, abortSignal);
|
|
1733
|
+
}
|
|
1734
|
+
if (abortSignal.aborted) return true;
|
|
1735
|
+
try {
|
|
1736
|
+
const activeRes = await fetch(
|
|
1737
|
+
`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
|
|
1738
|
+
{ signal: abortSignal },
|
|
1739
|
+
);
|
|
1740
|
+
if (!activeRes.ok) {
|
|
1741
|
+
if (activeRes.status === 404) return false;
|
|
1742
|
+
lastActiveRunError = new Error(
|
|
1743
|
+
`Active run lookup failed: ${activeRes.status}`,
|
|
1744
|
+
);
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
const active = await activeRes.json();
|
|
1748
|
+
if (!active?.active || !active.runId) return false;
|
|
1749
|
+
const activeRunId = String(active.runId);
|
|
1750
|
+
const dispatchMode =
|
|
1751
|
+
typeof active.dispatchMode === "string"
|
|
1752
|
+
? active.dispatchMode
|
|
1753
|
+
: "";
|
|
1754
|
+
if (activeRunId === interruptedRunId) {
|
|
1755
|
+
if (dispatchMode.startsWith("background")) continue;
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
const activeTurnId =
|
|
1759
|
+
typeof active.turnId === "string" ? active.turnId : "";
|
|
1760
|
+
const activeStatus =
|
|
1761
|
+
typeof active.status === "string" ? active.status : "";
|
|
1762
|
+
if (!dispatchMode.startsWith("background")) return false;
|
|
1763
|
+
if (activeTurnId && activeTurnId !== turnId) return false;
|
|
1764
|
+
if (activeStatus !== "running" && activeStatus !== "starting") {
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
runId = activeRunId;
|
|
1768
|
+
if (!attemptedRunIds.includes(activeRunId)) {
|
|
1769
|
+
attemptedRunIds.push(activeRunId);
|
|
1770
|
+
}
|
|
1771
|
+
lastSeq = -1;
|
|
1772
|
+
setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
|
|
1773
|
+
const reconnected = yield* reconnectCurrentRun();
|
|
1774
|
+
if (reconnected) return true;
|
|
1775
|
+
} catch (activeErr: unknown) {
|
|
1776
|
+
if (
|
|
1777
|
+
activeErr instanceof Error &&
|
|
1778
|
+
activeErr.name === "AbortError"
|
|
1779
|
+
) {
|
|
1780
|
+
clearActiveRun();
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
lastActiveRunError = activeErr;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (lastActiveRunError) {
|
|
1787
|
+
captureChatClientError(
|
|
1788
|
+
lastActiveRunError,
|
|
1789
|
+
"reconnect-background-continuation-failed",
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
runId = interruptedRunId;
|
|
1793
|
+
lastSeq = interruptedLastSeq;
|
|
1794
|
+
return false;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1677
1797
|
const visibleContentForContinuation = (): ContentPart[] => {
|
|
1678
1798
|
return contentAfterContinuationPrefix(
|
|
1679
1799
|
content,
|
|
@@ -1683,7 +1803,11 @@ export function createAgentChatAdapter(
|
|
|
1683
1803
|
|
|
1684
1804
|
const prepareAutoContinuation = (
|
|
1685
1805
|
signal: AgentAutoContinueSignal,
|
|
1686
|
-
): {
|
|
1806
|
+
): {
|
|
1807
|
+
ok: boolean;
|
|
1808
|
+
resetVisibleContent: boolean;
|
|
1809
|
+
completedToolName?: string;
|
|
1810
|
+
} => {
|
|
1687
1811
|
lastAutoContinueReason = signal.reason;
|
|
1688
1812
|
if (signal.errorInfo) {
|
|
1689
1813
|
lastRecoverableRunError = signal.errorInfo;
|
|
@@ -1703,6 +1827,7 @@ export function createAgentChatAdapter(
|
|
|
1703
1827
|
// before tool_start; treating it as progress caused silent retry
|
|
1704
1828
|
// loops when the LLM timed out while assembling a large tool input.
|
|
1705
1829
|
const hasInFlightTool = hasInFlightToolCall(visibleContent);
|
|
1830
|
+
const completedSideEffectTool = lastCompletedSideEffectTool(content);
|
|
1706
1831
|
// Either real output or an actively-running tool counts as progress
|
|
1707
1832
|
// for the stalled/empty caps.
|
|
1708
1833
|
const madeProgress = madeContentProgress || hasInFlightTool;
|
|
@@ -1717,11 +1842,11 @@ export function createAgentChatAdapter(
|
|
|
1717
1842
|
// MAX_REPEATED_INFLIGHT_TOOL_STALLS consecutive stream_ended events,
|
|
1718
1843
|
// bail with a clear message.
|
|
1719
1844
|
//
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1845
|
+
// Count broken streams (connection drop / reconnect failed) and
|
|
1846
|
+
// no_progress stalls (the client aborts a stream that stayed open but
|
|
1847
|
+
// stopped producing events). Do NOT count run_timeout: with an
|
|
1848
|
+
// in-flight tool that means the server is still actively executing a
|
|
1849
|
+
// slow action and reconnection may recover the result.
|
|
1725
1850
|
const currentInFlightToolPart = visibleContent.find(
|
|
1726
1851
|
(p): p is Extract<ContentPart, { type: "tool-call" }> =>
|
|
1727
1852
|
p.type === "tool-call" &&
|
|
@@ -1732,8 +1857,9 @@ export function createAgentChatAdapter(
|
|
|
1732
1857
|
const currentInFlightToolSignature = currentInFlightToolPart
|
|
1733
1858
|
? inFlightToolInputSignature(currentInFlightToolPart)
|
|
1734
1859
|
: undefined;
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1860
|
+
const isBrokenInFlightTool =
|
|
1861
|
+
signal.reason === "stream_ended" || signal.reason === "no_progress";
|
|
1862
|
+
if (currentInFlightToolName && isBrokenInFlightTool) {
|
|
1737
1863
|
if (
|
|
1738
1864
|
currentInFlightToolName === lastInFlightToolName &&
|
|
1739
1865
|
currentInFlightToolSignature === lastInFlightToolSignature
|
|
@@ -1772,13 +1898,31 @@ export function createAgentChatAdapter(
|
|
|
1772
1898
|
emptyTransientContinuationAttempts = 0;
|
|
1773
1899
|
} else {
|
|
1774
1900
|
totalTransientContinuationAttempts += 1;
|
|
1901
|
+
// If a mutating action already completed, do not turn a missing
|
|
1902
|
+
// closing sentence into a scary connection failure. Give the model
|
|
1903
|
+
// one continuation opportunity (the completed tool itself counts
|
|
1904
|
+
// as progress on the first timeout); if the follow-up produces no
|
|
1905
|
+
// new content, stop locally with a clear "saved, final note timed
|
|
1906
|
+
// out" message.
|
|
1907
|
+
if (
|
|
1908
|
+
completedSideEffectTool &&
|
|
1909
|
+
!hasInFlightToolCall(content) &&
|
|
1910
|
+
!madeContentProgress &&
|
|
1911
|
+
!hasInFlightTool
|
|
1912
|
+
) {
|
|
1913
|
+
return {
|
|
1914
|
+
ok: false,
|
|
1915
|
+
resetVisibleContent: false,
|
|
1916
|
+
completedToolName: completedSideEffectTool.toolName,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1775
1919
|
// Bail when the same write tool is stuck in-flight across too many
|
|
1776
1920
|
// consecutive continuations. Checked before the text-repeat guard
|
|
1777
1921
|
// because hasInFlightTool=true would mask the repeat as progress.
|
|
1778
1922
|
if (
|
|
1779
1923
|
repeatedInFlightToolCount >= MAX_REPEATED_INFLIGHT_TOOL_STALLS
|
|
1780
1924
|
) {
|
|
1781
|
-
|
|
1925
|
+
recoveryGaveUpOnInFlightTool = true;
|
|
1782
1926
|
return { ok: false, resetVisibleContent: false };
|
|
1783
1927
|
}
|
|
1784
1928
|
// Bail fast on a non-advancing repetition loop, well before the
|
|
@@ -2175,6 +2319,11 @@ export function createAgentChatAdapter(
|
|
|
2175
2319
|
if (err.reason === "no_progress") {
|
|
2176
2320
|
await abortCurrentRun();
|
|
2177
2321
|
}
|
|
2322
|
+
if (err.reason === "run_timeout" && !err.errorInfo) {
|
|
2323
|
+
const reconnected =
|
|
2324
|
+
yield* reconnectBackgroundContinuationForRunTimeout();
|
|
2325
|
+
if (reconnected) return;
|
|
2326
|
+
}
|
|
2178
2327
|
if (err.reason === "stream_ended") {
|
|
2179
2328
|
const reconnected = yield* reconnectCurrentRun();
|
|
2180
2329
|
if (reconnected) return;
|
|
@@ -2183,6 +2332,31 @@ export function createAgentChatAdapter(
|
|
|
2183
2332
|
}
|
|
2184
2333
|
const continuation = prepareAutoContinuation(err);
|
|
2185
2334
|
if (!continuation.ok) {
|
|
2335
|
+
if (continuation.completedToolName) {
|
|
2336
|
+
const message = completedToolTimeoutMessage(
|
|
2337
|
+
continuation.completedToolName,
|
|
2338
|
+
);
|
|
2339
|
+
content.push({ type: "text", text: message });
|
|
2340
|
+
yield {
|
|
2341
|
+
content: [...content],
|
|
2342
|
+
status: {
|
|
2343
|
+
type: "complete" as const,
|
|
2344
|
+
reason: "stop" as const,
|
|
2345
|
+
},
|
|
2346
|
+
metadata: {
|
|
2347
|
+
custom: {
|
|
2348
|
+
...(runId ? { runId } : {}),
|
|
2349
|
+
runWarning: {
|
|
2350
|
+
message,
|
|
2351
|
+
errorCode: "final_response_timeout_after_tool",
|
|
2352
|
+
recoverable: true,
|
|
2353
|
+
},
|
|
2354
|
+
},
|
|
2355
|
+
},
|
|
2356
|
+
};
|
|
2357
|
+
clearActiveRun();
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2186
2360
|
const preservedError =
|
|
2187
2361
|
err.errorInfo ?? lastRecoverableRunError ?? null;
|
|
2188
2362
|
const message =
|
|
@@ -2355,6 +2529,31 @@ export function createAgentChatAdapter(
|
|
|
2355
2529
|
new AgentAutoContinueSignal({ reason: "stream_ended" }),
|
|
2356
2530
|
);
|
|
2357
2531
|
if (!continuation.ok) {
|
|
2532
|
+
if (continuation.completedToolName) {
|
|
2533
|
+
const message = completedToolTimeoutMessage(
|
|
2534
|
+
continuation.completedToolName,
|
|
2535
|
+
);
|
|
2536
|
+
content.push({ type: "text", text: message });
|
|
2537
|
+
yield {
|
|
2538
|
+
content: [...content],
|
|
2539
|
+
status: {
|
|
2540
|
+
type: "complete" as const,
|
|
2541
|
+
reason: "stop" as const,
|
|
2542
|
+
},
|
|
2543
|
+
metadata: {
|
|
2544
|
+
custom: {
|
|
2545
|
+
...(runId ? { runId } : {}),
|
|
2546
|
+
runWarning: {
|
|
2547
|
+
message,
|
|
2548
|
+
errorCode: "final_response_timeout_after_tool",
|
|
2549
|
+
recoverable: true,
|
|
2550
|
+
},
|
|
2551
|
+
},
|
|
2552
|
+
},
|
|
2553
|
+
};
|
|
2554
|
+
clearActiveRun();
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2358
2557
|
const message = exhaustedRecoveryMessage("stream_ended");
|
|
2359
2558
|
captureChatClientError(err, "recovery-exhausted");
|
|
2360
2559
|
const runError = {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { useQueryClient } from "@tanstack/react-query";
|
|
16
16
|
import {
|
|
17
17
|
useCallback,
|
|
18
|
+
type ComponentPropsWithoutRef,
|
|
18
19
|
useEffect,
|
|
19
20
|
useId,
|
|
20
21
|
useMemo,
|
|
@@ -69,6 +70,10 @@ export interface ShareButtonProps {
|
|
|
69
70
|
/** Where to render share links in the popover. Defaults to the bottom,
|
|
70
71
|
* matching the historical Google-Docs-style share dialog. */
|
|
71
72
|
shareUrlPlacement?: "top" | "bottom";
|
|
73
|
+
/** Whether to render copyable share URL fields. Defaults to true. */
|
|
74
|
+
showShareLinks?: boolean;
|
|
75
|
+
/** Whether to render the bottom Done button. Defaults to true. */
|
|
76
|
+
showDoneButton?: boolean;
|
|
72
77
|
/** Optional placeholder shown in the share-URL slot when `shareUrl` is
|
|
73
78
|
* undefined. Use this to explain *why* there's no link yet (e.g. "Publish
|
|
74
79
|
* this form to get a public response link") instead of leaving the slot
|
|
@@ -99,6 +104,8 @@ export interface ShareButtonProps {
|
|
|
99
104
|
generalAccessLabel?: ReactNode;
|
|
100
105
|
/** Optional note rendered between general access and the copyable link. */
|
|
101
106
|
accessNote?: ReactNode;
|
|
107
|
+
/** Optional host-rendered footer for compact app-specific share actions. */
|
|
108
|
+
shareFooterContent?: ReactNode;
|
|
102
109
|
/** Optional Notion-style organization access control. When present, the
|
|
103
110
|
* share panel exposes a "Hide in search" switch under Advanced for org
|
|
104
111
|
* visibility. */
|
|
@@ -171,6 +178,8 @@ const BUTTON_GHOST_ICON = cn(
|
|
|
171
178
|
);
|
|
172
179
|
const SHARE_POPOVER_SURFACE =
|
|
173
180
|
"border border-border bg-popover text-popover-foreground";
|
|
181
|
+
const SHARE_NESTED_OVERLAY_ATTR = "data-agent-native-share-overlay";
|
|
182
|
+
const SHARE_NESTED_OVERLAY_Z = "z-[100020]";
|
|
174
183
|
const MEMBER_SUGGESTION_LIMIT = 25;
|
|
175
184
|
const MEMBER_SEARCH_DEBOUNCE_MS = 140;
|
|
176
185
|
|
|
@@ -219,6 +228,28 @@ const ROLE_OPTIONS: Array<{ value: Role; label: string; description: string }> =
|
|
|
219
228
|
},
|
|
220
229
|
];
|
|
221
230
|
|
|
231
|
+
type SharePopoverInteractOutsideEvent = Parameters<
|
|
232
|
+
NonNullable<
|
|
233
|
+
ComponentPropsWithoutRef<typeof PopoverContent>["onInteractOutside"]
|
|
234
|
+
>
|
|
235
|
+
>[0];
|
|
236
|
+
|
|
237
|
+
function isShareNestedOverlayTarget(target: EventTarget | null): boolean {
|
|
238
|
+
return (
|
|
239
|
+
target instanceof Element &&
|
|
240
|
+
target.closest(`[${SHARE_NESTED_OVERLAY_ATTR}]`) !== null
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleSharePopoverInteractOutside(
|
|
245
|
+
event: SharePopoverInteractOutsideEvent,
|
|
246
|
+
) {
|
|
247
|
+
const originalTarget = event.detail.originalEvent.target;
|
|
248
|
+
if (isShareNestedOverlayTarget(originalTarget)) {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
/**
|
|
223
254
|
* Framework share control. Renders a shadcn-outline-styled trigger that
|
|
224
255
|
* opens a Google-Docs-style popover anchored beneath it. Uses Tailwind
|
|
@@ -344,12 +375,14 @@ export function ShareButton(props: ShareButtonProps) {
|
|
|
344
375
|
<PopoverContent
|
|
345
376
|
align="end"
|
|
346
377
|
sideOffset={6}
|
|
378
|
+
data-agent-native-share-overlay=""
|
|
347
379
|
className={cn(
|
|
348
380
|
"z-[2000] w-[min(460px,92vw)] rounded-lg p-4 shadow-lg",
|
|
349
381
|
SHARE_POPOVER_SURFACE,
|
|
350
382
|
props.popoverClassName,
|
|
351
383
|
)}
|
|
352
384
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
385
|
+
onInteractOutside={handleSharePopoverInteractOutside}
|
|
353
386
|
>
|
|
354
387
|
<SharePanel
|
|
355
388
|
{...props}
|
|
@@ -622,9 +655,11 @@ function SharePanel(
|
|
|
622
655
|
</>
|
|
623
656
|
);
|
|
624
657
|
const showShareLinks =
|
|
625
|
-
|
|
626
|
-
Boolean(props.
|
|
627
|
-
|
|
658
|
+
(props.showShareLinks ?? true) &&
|
|
659
|
+
(Boolean(props.shareUrl) ||
|
|
660
|
+
Boolean(props.shareUrlPlaceholder) ||
|
|
661
|
+
Boolean(props.secondaryShareUrl));
|
|
662
|
+
const showDoneButton = props.showDoneButton ?? true;
|
|
628
663
|
const shareUrlPlacement = props.shareUrlPlacement ?? "bottom";
|
|
629
664
|
const extraTabs = props.shareTabs?.tabs ?? [];
|
|
630
665
|
const hasTabs = extraTabs.length > 0;
|
|
@@ -821,11 +856,13 @@ function SharePanel(
|
|
|
821
856
|
<div className="mb-4 h-7 rounded-md bg-muted animate-pulse" />
|
|
822
857
|
<div className="mb-2 text-sm font-semibold">{generalAccessLabel}</div>
|
|
823
858
|
<div className="mb-4 h-9 rounded-md bg-muted animate-pulse" />
|
|
824
|
-
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
859
|
+
{showDoneButton ? (
|
|
860
|
+
<div className="mt-2 flex justify-end">
|
|
861
|
+
<button type="button" onClick={onClose} className={BUTTON_PRIMARY_SM}>
|
|
862
|
+
Done
|
|
863
|
+
</button>
|
|
864
|
+
</div>
|
|
865
|
+
) : null}
|
|
829
866
|
</div>
|
|
830
867
|
) : (
|
|
831
868
|
<div>
|
|
@@ -990,15 +1027,19 @@ function SharePanel(
|
|
|
990
1027
|
|
|
991
1028
|
{showShareLinks && shareUrlPlacement === "bottom" ? shareLinks : null}
|
|
992
1029
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1030
|
+
{props.shareFooterContent}
|
|
1031
|
+
|
|
1032
|
+
{showDoneButton ? (
|
|
1033
|
+
<div className="mt-2 flex justify-end">
|
|
1034
|
+
<button
|
|
1035
|
+
type="button"
|
|
1036
|
+
onClick={handleDone}
|
|
1037
|
+
className={BUTTON_PRIMARY_SM}
|
|
1038
|
+
>
|
|
1039
|
+
Done
|
|
1040
|
+
</button>
|
|
1041
|
+
</div>
|
|
1042
|
+
) : null}
|
|
1002
1043
|
</div>
|
|
1003
1044
|
);
|
|
1004
1045
|
|
|
@@ -1076,8 +1117,13 @@ function AdvancedAccessPopover({
|
|
|
1076
1117
|
<PopoverContent
|
|
1077
1118
|
align="start"
|
|
1078
1119
|
sideOffset={6}
|
|
1120
|
+
data-agent-native-share-overlay=""
|
|
1079
1121
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
1080
|
-
className={cn(
|
|
1122
|
+
className={cn(
|
|
1123
|
+
SHARE_NESTED_OVERLAY_Z,
|
|
1124
|
+
"w-72 p-3 shadow-lg",
|
|
1125
|
+
SHARE_POPOVER_SURFACE,
|
|
1126
|
+
)}
|
|
1081
1127
|
>
|
|
1082
1128
|
<div className="space-y-3">
|
|
1083
1129
|
<div>
|
|
@@ -1291,9 +1337,11 @@ function MemberAutocomplete({
|
|
|
1291
1337
|
<PopoverContent
|
|
1292
1338
|
align="start"
|
|
1293
1339
|
sideOffset={4}
|
|
1340
|
+
data-agent-native-share-overlay=""
|
|
1294
1341
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
1295
1342
|
className={cn(
|
|
1296
|
-
|
|
1343
|
+
SHARE_NESTED_OVERLAY_Z,
|
|
1344
|
+
"w-[var(--radix-popper-anchor-width)] min-w-[18rem] rounded-md p-1 shadow-lg",
|
|
1297
1345
|
SHARE_POPOVER_SURFACE,
|
|
1298
1346
|
)}
|
|
1299
1347
|
>
|
|
@@ -1441,8 +1489,7 @@ function CopyLinkField({
|
|
|
1441
1489
|
// Radix Select wrappers styled like shadcn Select (no native <select> anywhere)
|
|
1442
1490
|
// ---------------------------------------------------------------------------
|
|
1443
1491
|
|
|
1444
|
-
const selectContentClass =
|
|
1445
|
-
"z-[2100] min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0";
|
|
1492
|
+
const selectContentClass = `${SHARE_NESTED_OVERLAY_Z} min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`;
|
|
1446
1493
|
const selectItemClass =
|
|
1447
1494
|
"relative flex w-full cursor-pointer select-none items-start gap-2 rounded-sm py-2 ps-8 pe-3 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
|
|
1448
1495
|
|
|
@@ -1517,6 +1564,7 @@ function RoleSelect(props: {
|
|
|
1517
1564
|
</Select.Trigger>
|
|
1518
1565
|
<Select.Portal>
|
|
1519
1566
|
<Select.Content
|
|
1567
|
+
data-agent-native-share-overlay=""
|
|
1520
1568
|
className={selectContentClass}
|
|
1521
1569
|
position="popper"
|
|
1522
1570
|
sideOffset={4}
|
|
@@ -1563,6 +1611,7 @@ function VisibilitySelect(props: {
|
|
|
1563
1611
|
</Select.Trigger>
|
|
1564
1612
|
<Select.Portal>
|
|
1565
1613
|
<Select.Content
|
|
1614
|
+
data-agent-native-share-overlay=""
|
|
1566
1615
|
className={selectContentClass}
|
|
1567
1616
|
position="popper"
|
|
1568
1617
|
sideOffset={4}
|