@agent-native/core 0.84.36 → 0.84.38
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 +12 -0
- package/corpus/core/docs/content/getting-started.mdx +3 -3
- package/corpus/core/docs/content/locales/ar-SA/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/de-DE/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/hi-IN/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/getting-started.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/getting-started.mdx +2 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +2 -0
- package/corpus/core/src/client/AssistantChat.tsx +27 -13
- package/corpus/core/src/client/RunStuckBanner.tsx +23 -3
- package/corpus/core/src/client/agent-chat-adapter.ts +20 -0
- package/corpus/core/src/client/sse-event-processor.ts +31 -6
- package/corpus/core/src/client/use-run-stuck-detection.ts +14 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +13 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +19 -13
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +16 -1
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +17 -2
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +11 -2
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +15 -10
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +4 -0
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +7 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +5 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/docs/content/getting-started.mdx +3 -3
- package/docs/content/locales/ar-SA/getting-started.mdx +2 -2
- package/docs/content/locales/de-DE/getting-started.mdx +2 -2
- package/docs/content/locales/es-ES/getting-started.mdx +2 -2
- package/docs/content/locales/fr-FR/getting-started.mdx +2 -2
- package/docs/content/locales/hi-IN/getting-started.mdx +2 -2
- package/docs/content/locales/ja-JP/getting-started.mdx +2 -2
- package/docs/content/locales/ko-KR/getting-started.mdx +2 -2
- package/docs/content/locales/pt-BR/getting-started.mdx +2 -2
- package/docs/content/locales/zh-CN/getting-started.mdx +2 -2
- package/docs/content/locales/zh-TW/getting-started.mdx +2 -2
- package/package.json +1 -1
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.38
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 203c1c3: Keep durable background chat runs attached during long action preparation instead of aborting on keepalive-only or zero-byte preparation activity.
|
|
8
|
+
|
|
9
|
+
## 0.84.37
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 885a0bd: Hide docs formatter guards from rendered content, refine assistant recovery status labels, and keep live background workers from being auto-retried as stuck.
|
|
14
|
+
|
|
3
15
|
## 0.84.36
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -59,7 +59,7 @@ An action is one typed operation your agent and UI can both call. Replace the
|
|
|
59
59
|
starter `hello` action with the first real operation in your domain. This example
|
|
60
60
|
analyzes form responses and returns a validated shape for a custom chat chart:
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
<!-- prettier-ignore -->
|
|
63
63
|
```ts
|
|
64
64
|
// actions/analyze-responses.ts
|
|
65
65
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -108,7 +108,7 @@ The action above declares `outputSchema` and a product-specific
|
|
|
108
108
|
into prose. Register a React component for that exact renderer id, for example
|
|
109
109
|
by importing `app/chat-renderers.tsx` once from `app/root.tsx`:
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
<!-- prettier-ignore -->
|
|
112
112
|
```tsx
|
|
113
113
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
114
114
|
|
|
@@ -209,7 +209,7 @@ separate REST route just for the browser if an action is the operation.
|
|
|
209
209
|
|
|
210
210
|
Now add a durable page for the data behind the chat result:
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
<!-- prettier-ignore -->
|
|
213
213
|
```tsx
|
|
214
214
|
// app/routes/response-insights.tsx
|
|
215
215
|
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
|
|
@@ -32,7 +32,7 @@ pnpm dev
|
|
|
32
32
|
|
|
33
33
|
Action هي عملية typed operation مشتركة بين agent و UI. في هذا المسار أنشئ `actions/analyze-responses.ts`: تحلل ردود النماذج وتعيد شكلاً موثقًا لرسم مخصص داخل الدردشة.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ pnpm action analyze-responses --formId demo
|
|
|
75
75
|
|
|
76
76
|
لأن action تعلن `outputSchema` و `chatUI.renderer` خاصًا بالمنتج، لا يحول transcript البيانات المنظمة إلى نص عادي. سجّل مكون React لذلك المعرّف نفسه عند بدء التطبيق، مثل استيراد `app/chat-renderers.tsx` مرة واحدة من `app/root.tsx`:
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ Das Template enthält durable chat threads, auth, live sync, ein `actions/`-Verz
|
|
|
32
32
|
|
|
33
33
|
Eine action ist eine typisierte Operation, die Agent und UI gemeinsam aufrufen. Für diesen Ablauf erstellen Sie `actions/analyze-responses.ts`: Sie analysiert Formularantworten und gibt eine validierte Form für ein eigenes Chat-Diagramm zurück.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ Oder im Browser den Agent fragen:
|
|
|
75
75
|
|
|
76
76
|
Weil die action `outputSchema` und eine produktspezifische `chatUI.renderer` deklariert, muss der Transcript strukturierte Daten nicht in Fließtext verwandeln. Registrieren Sie beim App-Start eine React-Komponente für genau diese Renderer-ID, zum Beispiel indem `app/chat-renderers.tsx` einmal aus `app/root.tsx` importiert wird:
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ El template incluye durable chat threads, auth, live sync, un directorio `action
|
|
|
32
32
|
|
|
33
33
|
Una action es una operación tipada que comparten el agent y la UI. Para este flujo, crea `actions/analyze-responses.ts`: analiza respuestas de formularios y devuelve una forma validada para un gráfico de chat propio.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ O pídeselo al agent en el navegador:
|
|
|
75
75
|
|
|
76
76
|
Como la action declara `outputSchema` y una `chatUI.renderer` específica del producto, el transcript no convierte los datos en texto plano. Registra un componente React para ese id exacto desde el arranque de la app, por ejemplo importando `app/chat-renderers.tsx` una vez desde `app/root.tsx`:
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ Le template inclut durable chat threads, auth, live sync, un dossier `actions/`,
|
|
|
32
32
|
|
|
33
33
|
Une action est une opération typée appelée par l'agent et par l'UI. Pour ce guide, créez `actions/analyze-responses.ts` : elle analyse des réponses de formulaire et renvoie une forme validée pour un graphique de chat personnalisé.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ Ou demandez à l'agent dans le navigateur :
|
|
|
75
75
|
|
|
76
76
|
Comme l'action déclare `outputSchema` et un `chatUI.renderer` propre au produit, le transcript ne transforme pas les données structurées en simple texte. Enregistrez un composant React pour cet id exact au démarrage de l'app, par exemple en important `app/chat-renderers.tsx` une fois depuis `app/root.tsx` :
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ pnpm dev
|
|
|
32
32
|
|
|
33
33
|
Action वह typed operation है जिसे agent और UI दोनों call करते हैं. इस flow के लिए `actions/analyze-responses.ts` बनाएं: यह form responses analyze करता है और custom chat chart के लिए validated shape लौटाता है.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ pnpm action analyze-responses --formId demo
|
|
|
75
75
|
|
|
76
76
|
क्योंकि action `outputSchema` और product-specific `chatUI.renderer` declare करता है, transcript structured data को plain text में नहीं बदलता. फिर app startup से उसी exact renderer id के लिए React component register करें, जैसे `app/root.tsx` से `app/chat-renderers.tsx` को एक बार import करके:
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ pnpm dev
|
|
|
32
32
|
|
|
33
33
|
action は agent と UI が共有する typed operation です。この流れでは `actions/analyze-responses.ts` を作成し、フォーム回答を分析してカスタムチャットグラフ用の検証済み形状を返します。
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ pnpm action analyze-responses --formId demo
|
|
|
75
75
|
|
|
76
76
|
action が `outputSchema` とプロダクト固有の `chatUI.renderer` を宣言しているため、チャットは構造化データを長い文章に潰しません。次に、app の起動時にその正確な renderer id に対応する React コンポーネントを登録します。たとえば `app/root.tsx` から `app/chat-renderers.tsx` を一度 import します。
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ pnpm dev
|
|
|
32
32
|
|
|
33
33
|
action은 agent와 UI가 함께 호출하는 typed operation입니다. 이 흐름에서는 `actions/analyze-responses.ts`를 만들고, 폼 응답을 분석해 custom chat chart를 위한 검증된 형태를 반환합니다.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ pnpm action analyze-responses --formId demo
|
|
|
75
75
|
|
|
76
76
|
action이 `outputSchema`와 제품별 `chatUI.renderer`를 선언하므로 transcript가 구조화된 데이터를 긴 텍스트로 바꾸지 않습니다. 그런 다음 app startup에서 정확히 같은 renderer id에 대한 React component를 등록합니다. 예를 들어 `app/root.tsx`에서 `app/chat-renderers.tsx`를 한 번 import합니다.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -32,7 +32,7 @@ O template inclui durable chat threads, auth, live sync, diretório `actions/`,
|
|
|
32
32
|
|
|
33
33
|
Uma action é uma operação tipada que o agent e a UI chamam juntos. Para este fluxo, crie `actions/analyze-responses.ts`: ela analisa respostas de formulários e retorna um formato validado para um gráfico de chat customizado.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<!-- prettier-ignore -->
|
|
36
36
|
```ts
|
|
37
37
|
// actions/analyze-responses.ts
|
|
38
38
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -75,7 +75,7 @@ Ou peça ao agent no navegador:
|
|
|
75
75
|
|
|
76
76
|
Como a action declara `outputSchema` e uma `chatUI.renderer` específica do produto, o transcript não transforma dados estruturados em texto corrido. Registre um componente React para esse id exato no startup da app, por exemplo importando `app/chat-renderers.tsx` uma vez a partir de `app/root.tsx`:
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
79
|
```tsx
|
|
80
80
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
81
81
|
|
|
@@ -42,7 +42,7 @@ npx @agent-native/core@latest create my-app
|
|
|
42
42
|
Action 是 agent 和 UI 共同调用的一次 typed operation。可以把入门示例换成
|
|
43
43
|
`actions/analyze-responses.ts`:它分析表单反馈,并返回自定义聊天图表的已验证数据形状。
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
<!-- prettier-ignore -->
|
|
46
46
|
```ts
|
|
47
47
|
// actions/analyze-responses.ts
|
|
48
48
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -85,7 +85,7 @@ pnpm action analyze-responses --formId demo
|
|
|
85
85
|
|
|
86
86
|
因为 action 声明了 `outputSchema` 和产品专属的 `chatUI.renderer`,聊天记录不需要把结构化数据压成一段文字。然后在 app 启动时为这个完全相同的 renderer id 注册 React 组件,例如从 `app/root.tsx` 导入一次 `app/chat-renderers.tsx`:
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
<!-- prettier-ignore -->
|
|
89
89
|
```tsx
|
|
90
90
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
91
91
|
|
|
@@ -40,7 +40,7 @@ npx @agent-native/core@latest create my-app
|
|
|
40
40
|
|
|
41
41
|
Action 是 agent 與 UI 共同呼叫的一個 typed operation。可將入門範例換成 `actions/analyze-responses.ts`:它分析表單回覆並返回自訂聊天圖表的已驗證資料形狀。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
<!-- prettier-ignore -->
|
|
44
44
|
```ts
|
|
45
45
|
// actions/analyze-responses.ts
|
|
46
46
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -83,7 +83,7 @@ pnpm action analyze-responses --formId demo
|
|
|
83
83
|
|
|
84
84
|
因為 action 宣告了 `outputSchema` 與產品專屬的 `chatUI.renderer`,聊天紀錄不必把結構化資料壓成文字。然後在 app 啟動時為這個完全相同的 renderer id 註冊 React 元件,例如從 `app/root.tsx` 匯入一次 `app/chat-renderers.tsx`:
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
<!-- prettier-ignore -->
|
|
87
87
|
```tsx
|
|
88
88
|
import { registerActionChatRenderer, type ToolRendererProps,} from "@agent-native/core/client/chat";
|
|
89
89
|
|
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.38",
|
|
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": {
|
|
@@ -5106,6 +5106,7 @@ export function createProductionAgentHandler(
|
|
|
5106
5106
|
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
5107
5107
|
setResponseHeader(event, "Connection", "keep-alive");
|
|
5108
5108
|
setResponseHeader(event, "X-Run-Id", runId);
|
|
5109
|
+
setResponseHeader(event, "X-Dispatch-Mode", "background");
|
|
5109
5110
|
return stream;
|
|
5110
5111
|
}
|
|
5111
5112
|
// A background worker owns this run but we cannot subscribe — surface an
|
|
@@ -5912,6 +5913,7 @@ export function createProductionAgentHandler(
|
|
|
5912
5913
|
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
5913
5914
|
setResponseHeader(event, "Connection", "keep-alive");
|
|
5914
5915
|
setResponseHeader(event, "X-Run-Id", runId);
|
|
5916
|
+
setResponseHeader(event, "X-Dispatch-Mode", "foreground");
|
|
5915
5917
|
|
|
5916
5918
|
return stream;
|
|
5917
5919
|
});
|
|
@@ -662,6 +662,23 @@ export function resolveAssistantChatRunningState({
|
|
|
662
662
|
};
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
export function resolveAssistantChatRunningStatusLabel({
|
|
666
|
+
runningActivityLabel,
|
|
667
|
+
isAutoResuming,
|
|
668
|
+
isReconnecting,
|
|
669
|
+
hasReconnectContent,
|
|
670
|
+
}: {
|
|
671
|
+
runningActivityLabel: string | null | undefined;
|
|
672
|
+
isAutoResuming: boolean;
|
|
673
|
+
isReconnecting: boolean;
|
|
674
|
+
hasReconnectContent: boolean;
|
|
675
|
+
}): string {
|
|
676
|
+
if (runningActivityLabel) return runningActivityLabel;
|
|
677
|
+
if (isAutoResuming) return "Resuming";
|
|
678
|
+
if (isReconnecting && hasReconnectContent) return "Continuing";
|
|
679
|
+
return "Thinking";
|
|
680
|
+
}
|
|
681
|
+
|
|
665
682
|
type QueuedMessage = {
|
|
666
683
|
id: string;
|
|
667
684
|
text: string;
|
|
@@ -1497,18 +1514,15 @@ const AssistantChatInner = forwardRef<
|
|
|
1497
1514
|
hasActiveServerRun,
|
|
1498
1515
|
});
|
|
1499
1516
|
const textStreaming = showRunningInUI || externalStreaming;
|
|
1500
|
-
// A revealed activity label wins; otherwise
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
:
|
|
1508
|
-
|
|
1509
|
-
: isReconnecting && reconnectContent.length > 0
|
|
1510
|
-
? "Reconnecting"
|
|
1511
|
-
: "Thinking";
|
|
1517
|
+
// A revealed activity label wins; otherwise keep recovery states calm and
|
|
1518
|
+
// product-facing. Reconnect is transport machinery, so normal replay reads as
|
|
1519
|
+
// "Continuing" instead of exposing "Reconnecting" mid-chat.
|
|
1520
|
+
const runningStatusLabel = resolveAssistantChatRunningStatusLabel({
|
|
1521
|
+
runningActivityLabel,
|
|
1522
|
+
isAutoResuming,
|
|
1523
|
+
isReconnecting,
|
|
1524
|
+
hasReconnectContent: reconnectContent.length > 0,
|
|
1525
|
+
});
|
|
1512
1526
|
const reconnectActivityContent = useMemo(
|
|
1513
1527
|
() =>
|
|
1514
1528
|
(isReconnecting || reconnectFrozen) && reconnectContent.length === 0
|
|
@@ -1952,7 +1966,7 @@ const AssistantChatInner = forwardRef<
|
|
|
1952
1966
|
}
|
|
1953
1967
|
setRunErrorInfo({
|
|
1954
1968
|
message:
|
|
1955
|
-
"The previous agent run stopped producing visible progress
|
|
1969
|
+
"The previous agent run stopped producing visible progress during recovery, so it was stopped before it could keep looping.",
|
|
1956
1970
|
errorCode: "reconnect_no_progress",
|
|
1957
1971
|
recoverable: true,
|
|
1958
1972
|
runId,
|
|
@@ -55,6 +55,7 @@ export interface RunStuckBannerProps {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const AUTO_RETRY_CLAIM_TTL_MS = 5 * 60 * 1000;
|
|
58
|
+
const BACKGROUND_WORKER_FRESH_HEARTBEAT_MS = 30_000;
|
|
58
59
|
|
|
59
60
|
type BusyState = { type: "none" } | { type: "cancel" | "retry"; runId: string };
|
|
60
61
|
|
|
@@ -78,6 +79,16 @@ function autoRetryClaimKey(threadId: string, runId: string) {
|
|
|
78
79
|
return `agent-native:stuck-auto-retry:${threadId}:${runId}`;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function isFreshBackgroundWorker(state: RunStuckState): boolean {
|
|
83
|
+
return Boolean(
|
|
84
|
+
state.status === "running" &&
|
|
85
|
+
state.dispatchMode === "background-processing" &&
|
|
86
|
+
state.heartbeatSinceMs != null &&
|
|
87
|
+
state.heartbeatSinceMs >= 0 &&
|
|
88
|
+
state.heartbeatSinceMs < BACKGROUND_WORKER_FRESH_HEARTBEAT_MS,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
function markAutoRetryClaim(key: string, ownerId: string) {
|
|
82
93
|
if (typeof window === "undefined") return true;
|
|
83
94
|
const now = Date.now();
|
|
@@ -151,6 +162,7 @@ export function RunStuckBanner({
|
|
|
151
162
|
generatedOwnerIdRef.current = createAutoRetryOwnerId();
|
|
152
163
|
}
|
|
153
164
|
const ownerId = autoRetryOwnerId ?? generatedOwnerIdRef.current;
|
|
165
|
+
const backgroundWorkerStillAlive = isFreshBackgroundWorker(state);
|
|
154
166
|
|
|
155
167
|
const lastReportedRef = useRef<{
|
|
156
168
|
isStuck: boolean;
|
|
@@ -190,6 +202,7 @@ export function RunStuckBanner({
|
|
|
190
202
|
useEffect(() => {
|
|
191
203
|
if (
|
|
192
204
|
!autoRetry ||
|
|
205
|
+
backgroundWorkerStillAlive ||
|
|
193
206
|
!state.isStuck ||
|
|
194
207
|
!state.runId ||
|
|
195
208
|
busy.type !== "none" ||
|
|
@@ -223,6 +236,7 @@ export function RunStuckBanner({
|
|
|
223
236
|
}, [
|
|
224
237
|
abortRun,
|
|
225
238
|
autoRetry,
|
|
239
|
+
backgroundWorkerStillAlive,
|
|
226
240
|
busy,
|
|
227
241
|
onRetry,
|
|
228
242
|
ownerId,
|
|
@@ -280,11 +294,17 @@ export function RunStuckBanner({
|
|
|
280
294
|
/>
|
|
281
295
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
282
296
|
<div className="leading-snug">
|
|
283
|
-
<span className="font-medium">
|
|
297
|
+
<span className="font-medium">
|
|
298
|
+
{backgroundWorkerStillAlive
|
|
299
|
+
? "The agent is still working."
|
|
300
|
+
: "This chat looks stuck."}
|
|
301
|
+
</span>{" "}
|
|
284
302
|
<span className="text-muted-foreground">
|
|
285
303
|
No progress
|
|
286
|
-
{stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}.
|
|
287
|
-
|
|
304
|
+
{stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}.{" "}
|
|
305
|
+
{backgroundWorkerStillAlive
|
|
306
|
+
? "The background worker is still alive; large updates can take a few minutes."
|
|
307
|
+
: "The agent may have hit a server timeout or lost its connection."}
|
|
288
308
|
{autoRetry && busyType === "retry"
|
|
289
309
|
? " Retrying automatically now."
|
|
290
310
|
: ""}
|
|
@@ -1418,6 +1418,7 @@ export function createAgentChatAdapter(
|
|
|
1418
1418
|
const turnId = generateTurnId();
|
|
1419
1419
|
let runId: string | null = null;
|
|
1420
1420
|
let lastSeq = -1;
|
|
1421
|
+
let currentRunDispatchMode: string | null = null;
|
|
1421
1422
|
let currentMessageText = normalizeMentions(
|
|
1422
1423
|
recoveryMessageText.trim() || userMessageText,
|
|
1423
1424
|
);
|
|
@@ -1579,6 +1580,17 @@ export function createAgentChatAdapter(
|
|
|
1579
1580
|
}
|
|
1580
1581
|
};
|
|
1581
1582
|
|
|
1583
|
+
const updateCurrentRunDispatchMode = (value: unknown) => {
|
|
1584
|
+
if (typeof value !== "string") return;
|
|
1585
|
+
const mode = value.trim();
|
|
1586
|
+
if (mode) currentRunDispatchMode = mode;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
const currentSSEOptions = () => ({
|
|
1590
|
+
durableBackgroundRun:
|
|
1591
|
+
currentRunDispatchMode?.startsWith("background") === true,
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1582
1594
|
const captureChatClientError = (
|
|
1583
1595
|
error: unknown,
|
|
1584
1596
|
phase: string,
|
|
@@ -1687,6 +1699,9 @@ export function createAgentChatAdapter(
|
|
|
1687
1699
|
reconnectErrorCaptured = true;
|
|
1688
1700
|
break;
|
|
1689
1701
|
}
|
|
1702
|
+
updateCurrentRunDispatchMode(
|
|
1703
|
+
reconnectRes.headers.get("X-Dispatch-Mode"),
|
|
1704
|
+
);
|
|
1690
1705
|
|
|
1691
1706
|
for await (const result of readSSEStream(
|
|
1692
1707
|
reconnectRes.body,
|
|
@@ -1698,6 +1713,7 @@ export function createAgentChatAdapter(
|
|
|
1698
1713
|
if (threadId) updateActiveRunSeq(seq);
|
|
1699
1714
|
},
|
|
1700
1715
|
runId,
|
|
1716
|
+
currentSSEOptions(),
|
|
1701
1717
|
)) {
|
|
1702
1718
|
yield withRequestModeMetadata(result);
|
|
1703
1719
|
}
|
|
@@ -1778,6 +1794,7 @@ export function createAgentChatAdapter(
|
|
|
1778
1794
|
}
|
|
1779
1795
|
const active = await activeRes.json();
|
|
1780
1796
|
if (active?.active && active.runId) {
|
|
1797
|
+
updateCurrentRunDispatchMode(active.dispatchMode);
|
|
1781
1798
|
const activeStatus =
|
|
1782
1799
|
typeof active.status === "string" ? active.status : "";
|
|
1783
1800
|
const activeTurnId =
|
|
@@ -1851,6 +1868,7 @@ export function createAgentChatAdapter(
|
|
|
1851
1868
|
typeof active.dispatchMode === "string"
|
|
1852
1869
|
? active.dispatchMode
|
|
1853
1870
|
: "";
|
|
1871
|
+
updateCurrentRunDispatchMode(dispatchMode);
|
|
1854
1872
|
if (activeRunId === interruptedRunId) {
|
|
1855
1873
|
if (dispatchMode.startsWith("background")) continue;
|
|
1856
1874
|
return false;
|
|
@@ -2429,6 +2447,7 @@ export function createAgentChatAdapter(
|
|
|
2429
2447
|
|
|
2430
2448
|
// Track the run ID for reconnection
|
|
2431
2449
|
runId = res.headers.get("X-Run-Id");
|
|
2450
|
+
updateCurrentRunDispatchMode(res.headers.get("X-Dispatch-Mode"));
|
|
2432
2451
|
if (runId && !attemptedRunIds.includes(runId)) {
|
|
2433
2452
|
attemptedRunIds.push(runId);
|
|
2434
2453
|
}
|
|
@@ -2448,6 +2467,7 @@ export function createAgentChatAdapter(
|
|
|
2448
2467
|
}
|
|
2449
2468
|
},
|
|
2450
2469
|
runId,
|
|
2470
|
+
currentSSEOptions(),
|
|
2451
2471
|
)) {
|
|
2452
2472
|
yield withRequestModeMetadata(result);
|
|
2453
2473
|
}
|
|
@@ -148,6 +148,16 @@ export class AgentAutoContinueSignal extends Error {
|
|
|
148
148
|
export const SSE_NO_PROGRESS_TIMEOUT_MS = 75_000;
|
|
149
149
|
export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
|
|
150
150
|
|
|
151
|
+
export interface SSEStreamOptions {
|
|
152
|
+
/**
|
|
153
|
+
* Durable background runs have their own server-side liveness budget and
|
|
154
|
+
* heartbeat. While one is active, keepalive-only periods and zero-byte
|
|
155
|
+
* action-preparation activity should keep the client attached instead of
|
|
156
|
+
* aborting and starting duplicate continuations.
|
|
157
|
+
*/
|
|
158
|
+
durableBackgroundRun?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
type ActivityTrailEntry = AgentActivityTrailEntry;
|
|
152
162
|
|
|
153
163
|
type PreparingActionEntry = {
|
|
@@ -193,9 +203,13 @@ function isPreparingActionActivity(ev: SSEEvent): boolean {
|
|
|
193
203
|
function isMeaningfulProgressEvent(
|
|
194
204
|
ev: SSEEvent,
|
|
195
205
|
actionPreparationProgress?: boolean,
|
|
206
|
+
options?: SSEStreamOptions,
|
|
196
207
|
): boolean {
|
|
197
|
-
if (ev.type === "stream_keepalive")
|
|
208
|
+
if (ev.type === "stream_keepalive") {
|
|
209
|
+
return options?.durableBackgroundRun === true;
|
|
210
|
+
}
|
|
198
211
|
if (ev.type === "activity" && isPreparingActionActivity(ev)) {
|
|
212
|
+
if (options?.durableBackgroundRun === true) return true;
|
|
199
213
|
return actionPreparationProgress === true;
|
|
200
214
|
}
|
|
201
215
|
return true;
|
|
@@ -345,7 +359,12 @@ function updatePreparingActionState(
|
|
|
345
359
|
return undefined;
|
|
346
360
|
}
|
|
347
361
|
|
|
348
|
-
function hasStalledPreparingAction(
|
|
362
|
+
function hasStalledPreparingAction(
|
|
363
|
+
state: PreparingActionState,
|
|
364
|
+
now: number,
|
|
365
|
+
options?: SSEStreamOptions,
|
|
366
|
+
) {
|
|
367
|
+
if (options?.durableBackgroundRun === true) return false;
|
|
349
368
|
// Fire only when a tool input has gone SILENT — no further streaming deltas
|
|
350
369
|
// for the whole window — never merely because a large input has been
|
|
351
370
|
// streaming for a long time. `lastProgressAt` advances on every delta
|
|
@@ -1193,6 +1212,7 @@ export async function* readSSEStream(
|
|
|
1193
1212
|
tabId: string | undefined,
|
|
1194
1213
|
onSeq?: (seq: number) => void,
|
|
1195
1214
|
runId?: string | null,
|
|
1215
|
+
options?: SSEStreamOptions,
|
|
1196
1216
|
): AsyncGenerator<ChatModelRunResult> {
|
|
1197
1217
|
const reader = body.getReader();
|
|
1198
1218
|
const decoder = new TextDecoder();
|
|
@@ -1278,7 +1298,7 @@ export async function* readSSEStream(
|
|
|
1278
1298
|
ev,
|
|
1279
1299
|
now,
|
|
1280
1300
|
);
|
|
1281
|
-
if (isMeaningfulProgressEvent(ev, actionPreparationProgress)) {
|
|
1301
|
+
if (isMeaningfulProgressEvent(ev, actionPreparationProgress, options)) {
|
|
1282
1302
|
sawProgressEvent = true;
|
|
1283
1303
|
lastMeaningfulEventAt = now;
|
|
1284
1304
|
}
|
|
@@ -1320,7 +1340,9 @@ export async function* readSSEStream(
|
|
|
1320
1340
|
);
|
|
1321
1341
|
|
|
1322
1342
|
if (result) yield withStreamMetadata(result);
|
|
1323
|
-
if (
|
|
1343
|
+
if (
|
|
1344
|
+
hasStalledPreparingAction(preparingActionState, Date.now(), options)
|
|
1345
|
+
) {
|
|
1324
1346
|
throw new AgentAutoContinueSignal({
|
|
1325
1347
|
reason: "no_progress",
|
|
1326
1348
|
activityTrail: [...activityTrail],
|
|
@@ -1384,6 +1406,7 @@ export async function readSSEStreamRaw(
|
|
|
1384
1406
|
tabId: string | undefined,
|
|
1385
1407
|
onUpdate: (content: ContentPart[]) => void,
|
|
1386
1408
|
onSeq?: (seq: number) => void,
|
|
1409
|
+
options?: SSEStreamOptions,
|
|
1387
1410
|
): Promise<void> {
|
|
1388
1411
|
const reader = body.getReader();
|
|
1389
1412
|
const decoder = new TextDecoder();
|
|
@@ -1444,7 +1467,7 @@ export async function readSSEStreamRaw(
|
|
|
1444
1467
|
ev,
|
|
1445
1468
|
now,
|
|
1446
1469
|
);
|
|
1447
|
-
if (isMeaningfulProgressEvent(ev, actionPreparationProgress)) {
|
|
1470
|
+
if (isMeaningfulProgressEvent(ev, actionPreparationProgress, options)) {
|
|
1448
1471
|
sawProgressEvent = true;
|
|
1449
1472
|
lastMeaningfulEventAt = now;
|
|
1450
1473
|
}
|
|
@@ -1502,7 +1525,9 @@ export async function readSSEStreamRaw(
|
|
|
1502
1525
|
: { reason: "stream_ended", activityTrail: [...activityTrail] },
|
|
1503
1526
|
);
|
|
1504
1527
|
}
|
|
1505
|
-
if (
|
|
1528
|
+
if (
|
|
1529
|
+
hasStalledPreparingAction(preparingActionState, Date.now(), options)
|
|
1530
|
+
) {
|
|
1506
1531
|
onUpdate(contentSnapshot(content));
|
|
1507
1532
|
throw new AgentAutoContinueSignal({
|
|
1508
1533
|
reason: "no_progress",
|
|
@@ -24,6 +24,10 @@ export interface RunStuckState {
|
|
|
24
24
|
stuckSinceMs: number | null;
|
|
25
25
|
/** Server timestamp (ms) of the last process-alive heartbeat. */
|
|
26
26
|
heartbeatAt: number | null;
|
|
27
|
+
/** Milliseconds since `heartbeatAt`, computed against the server clock. */
|
|
28
|
+
heartbeatSinceMs: number | null;
|
|
29
|
+
/** How the run was dispatched, e.g. foreground or background-processing. */
|
|
30
|
+
dispatchMode: string | null;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface UseRunStuckDetectionOptions {
|
|
@@ -51,6 +55,7 @@ interface ActiveRunResponse {
|
|
|
51
55
|
status?: string;
|
|
52
56
|
heartbeatAt: number | null;
|
|
53
57
|
lastProgressAt?: number | null;
|
|
58
|
+
dispatchMode?: string | null;
|
|
54
59
|
/** Server clock at response time, used to compute elapsed server-relative. */
|
|
55
60
|
serverNow?: number;
|
|
56
61
|
}
|
|
@@ -62,6 +67,8 @@ const EMPTY_STATE: RunStuckState = {
|
|
|
62
67
|
lastProgressAt: null,
|
|
63
68
|
stuckSinceMs: null,
|
|
64
69
|
heartbeatAt: null,
|
|
70
|
+
heartbeatSinceMs: null,
|
|
71
|
+
dispatchMode: null,
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
export function useRunStuckDetection({
|
|
@@ -102,6 +109,9 @@ export function useRunStuckDetection({
|
|
|
102
109
|
const nowMs = data.serverNow ?? Date.now();
|
|
103
110
|
const stuckSinceMs =
|
|
104
111
|
lastProgressAt != null ? nowMs - lastProgressAt : null;
|
|
112
|
+
const heartbeatAt = data.heartbeatAt ?? null;
|
|
113
|
+
const heartbeatSinceMs =
|
|
114
|
+
heartbeatAt != null ? nowMs - heartbeatAt : null;
|
|
105
115
|
const isStuck = Boolean(
|
|
106
116
|
data.active &&
|
|
107
117
|
data.status === "running" &&
|
|
@@ -114,7 +124,10 @@ export function useRunStuckDetection({
|
|
|
114
124
|
status: data.status ?? null,
|
|
115
125
|
lastProgressAt,
|
|
116
126
|
stuckSinceMs,
|
|
117
|
-
heartbeatAt
|
|
127
|
+
heartbeatAt,
|
|
128
|
+
heartbeatSinceMs,
|
|
129
|
+
dispatchMode:
|
|
130
|
+
typeof data.dispatchMode === "string" ? data.dispatchMode : null,
|
|
118
131
|
});
|
|
119
132
|
// Back off polling when nothing is in flight — there's no point
|
|
120
133
|
// hammering the endpoint while the chat is idle. We still poll
|
|
@@ -7278,8 +7278,11 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7278
7278
|
let reason = "user";
|
|
7279
7279
|
try {
|
|
7280
7280
|
const body = await readBody(event);
|
|
7281
|
-
if (
|
|
7282
|
-
reason
|
|
7281
|
+
if (
|
|
7282
|
+
typeof body?.reason === "string" &&
|
|
7283
|
+
/^[a-z0-9_-]{1,64}$/i.test(body.reason)
|
|
7284
|
+
) {
|
|
7285
|
+
reason = body.reason;
|
|
7283
7286
|
}
|
|
7284
7287
|
} catch {
|
|
7285
7288
|
// Empty/invalid body — keep the default user abort reason.
|
|
@@ -7343,6 +7346,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7343
7346
|
setResponseStatus(event, 404);
|
|
7344
7347
|
return { error: "Run not found" };
|
|
7345
7348
|
}
|
|
7349
|
+
const runClaim = await readBackgroundRunClaim(runId).catch(
|
|
7350
|
+
() => null,
|
|
7351
|
+
);
|
|
7346
7352
|
const query = getQuery(event);
|
|
7347
7353
|
const after = parseInt(String(query.after ?? "0"), 10) || 0;
|
|
7348
7354
|
|
|
@@ -7355,6 +7361,11 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7355
7361
|
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
7356
7362
|
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
7357
7363
|
setResponseHeader(event, "Connection", "keep-alive");
|
|
7364
|
+
setResponseHeader(
|
|
7365
|
+
event,
|
|
7366
|
+
"X-Dispatch-Mode",
|
|
7367
|
+
runClaim?.dispatchMode ?? "foreground",
|
|
7368
|
+
);
|
|
7358
7369
|
return stream;
|
|
7359
7370
|
}
|
|
7360
7371
|
|