@lotics/app-sdk 0.52.0 → 0.52.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/dist/src/agent_stream.d.ts +18 -0
- package/dist/src/agent_stream.js +18 -0
- package/dist/src/hooks.js +25 -11
- package/docs/ai.md +7 -2
- package/package.json +1 -1
|
@@ -68,6 +68,24 @@ export interface AgentRunState {
|
|
|
68
68
|
output?: unknown;
|
|
69
69
|
error?: string;
|
|
70
70
|
}
|
|
71
|
+
/** A settled run row, as the poll endpoint returns it. */
|
|
72
|
+
export interface SettledAgentRun {
|
|
73
|
+
status: string;
|
|
74
|
+
output?: unknown;
|
|
75
|
+
error_message?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fold a POLLED settled run row into the stream-accumulated state — the shared
|
|
79
|
+
* adoption step for BOTH recovery paths (a dropped stream, and a stream that
|
|
80
|
+
* ended cleanly WITHOUT a `finish` frame — an edge can close a long SSE
|
|
81
|
+
* gracefully mid-run, which looks like completion but is a truncation).
|
|
82
|
+
*
|
|
83
|
+
* The row is the source of truth for status and the STRUCTURED output. A
|
|
84
|
+
* free-text run's row `output` is its prose string — that never enters
|
|
85
|
+
* `state.output` (a structured consumer reads `output.<field>`; a stray string
|
|
86
|
+
* would crash it). The free-text answer stays in the transcript text.
|
|
87
|
+
*/
|
|
88
|
+
export declare function adoptSettledRun(state: AgentRunState, settled: SettledAgentRun): AgentRunState;
|
|
71
89
|
export declare function initialAgentRunState(): AgentRunState;
|
|
72
90
|
/** A parsed UI-message chunk — only the fields we read, all optional. */
|
|
73
91
|
interface Chunk {
|
package/dist/src/agent_stream.js
CHANGED
|
@@ -20,6 +20,24 @@
|
|
|
20
20
|
*/
|
|
21
21
|
/** The tool the backend injects to carry a typed structured result. */
|
|
22
22
|
const SUBMIT_TOOL = "submit_result";
|
|
23
|
+
/**
|
|
24
|
+
* Fold a POLLED settled run row into the stream-accumulated state — the shared
|
|
25
|
+
* adoption step for BOTH recovery paths (a dropped stream, and a stream that
|
|
26
|
+
* ended cleanly WITHOUT a `finish` frame — an edge can close a long SSE
|
|
27
|
+
* gracefully mid-run, which looks like completion but is a truncation).
|
|
28
|
+
*
|
|
29
|
+
* The row is the source of truth for status and the STRUCTURED output. A
|
|
30
|
+
* free-text run's row `output` is its prose string — that never enters
|
|
31
|
+
* `state.output` (a structured consumer reads `output.<field>`; a stray string
|
|
32
|
+
* would crash it). The free-text answer stays in the transcript text.
|
|
33
|
+
*/
|
|
34
|
+
export function adoptSettledRun(state, settled) {
|
|
35
|
+
if (settled.status === "completed") {
|
|
36
|
+
const structured = settled.output !== null && typeof settled.output === "object" ? settled.output : undefined;
|
|
37
|
+
return { ...state, status: "completed", output: structured ?? state.output };
|
|
38
|
+
}
|
|
39
|
+
return { ...state, status: "error", error: settled.error_message ?? "The run was stopped." };
|
|
40
|
+
}
|
|
23
41
|
export function initialAgentRunState() {
|
|
24
42
|
return { status: "streaming", items: [] };
|
|
25
43
|
}
|
package/dist/src/hooks.js
CHANGED
|
@@ -19,7 +19,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
19
19
|
import useSWR from "swr";
|
|
20
20
|
import useSWRInfinite from "swr/infinite";
|
|
21
21
|
import { rpc, rpcAgentRun, postHostNotification, subscribeHostRefetch, } from "./rpc.js";
|
|
22
|
-
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, } from "./agent_stream.js";
|
|
22
|
+
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, adoptSettledRun, } from "./agent_stream.js";
|
|
23
23
|
import { getMockRows, hasMockFlag } from "./mock.js";
|
|
24
24
|
import { captureAppEvent } from "./analytics.js";
|
|
25
25
|
export function useWorkflow(alias) {
|
|
@@ -487,16 +487,33 @@ export function useAgentRun(alias) {
|
|
|
487
487
|
},
|
|
488
488
|
};
|
|
489
489
|
return handle.done
|
|
490
|
-
.then(() => {
|
|
490
|
+
.then(async () => {
|
|
491
491
|
if (aborted)
|
|
492
492
|
return undefined;
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
493
|
+
// A clean stream end WITHOUT a `finish` frame is a truncation, not a
|
|
494
|
+
// completion — an edge can close a long SSE gracefully mid-run (seen
|
|
495
|
+
// in production on ~2-minute runs), swallowing the frames that carry
|
|
496
|
+
// the structured result. The run is decoupled and settles server-side
|
|
497
|
+
// regardless, so the row is the source of truth: poll it, exactly
|
|
498
|
+
// like the dropped-with-error path below. (2026-07-18: four NOXH
|
|
499
|
+
// extractions completed server-side while every client showed
|
|
500
|
+
// failure through this hole.)
|
|
497
501
|
if (acc.status === "streaming") {
|
|
498
|
-
|
|
502
|
+
const runId = runIdRef.current;
|
|
503
|
+
const settled = runId
|
|
504
|
+
? await pollAgentRunToSettle(runId, () => aborted || !mountedRef.current)
|
|
505
|
+
: null;
|
|
506
|
+
if (aborted)
|
|
507
|
+
return undefined;
|
|
508
|
+
acc = settled ? adoptSettledRun(acc, settled) : { ...acc, status: "completed" };
|
|
499
509
|
safeSetState(acc);
|
|
510
|
+
// Fleet visibility: how often edges cut agent SSE streams. One
|
|
511
|
+
// event per truncation, with whether the row rescued the result.
|
|
512
|
+
captureAppEvent("app_agent_stream_truncated", {
|
|
513
|
+
recovered: settled != null,
|
|
514
|
+
settled_status: settled?.status ?? null,
|
|
515
|
+
has_output: acc.output !== undefined,
|
|
516
|
+
});
|
|
500
517
|
}
|
|
501
518
|
return acc.output;
|
|
502
519
|
})
|
|
@@ -510,10 +527,7 @@ export function useAgentRun(alias) {
|
|
|
510
527
|
if (runId) {
|
|
511
528
|
const settled = await pollAgentRunToSettle(runId, () => aborted || !mountedRef.current);
|
|
512
529
|
if (settled && !aborted) {
|
|
513
|
-
acc =
|
|
514
|
-
settled.status === "completed"
|
|
515
|
-
? { ...acc, status: "completed", output: settled.output ?? acc.output }
|
|
516
|
-
: { ...acc, status: "error", error: settled.error_message ?? "The run was stopped." };
|
|
530
|
+
acc = adoptSettledRun(acc, settled);
|
|
517
531
|
safeSetState(acc);
|
|
518
532
|
return acc.output;
|
|
519
533
|
}
|
package/docs/ai.md
CHANGED
|
@@ -121,9 +121,14 @@ Both settle the in-flight `run()` promise cleanly with `undefined` — a stop is
|
|
|
121
121
|
|
|
122
122
|
### Runs survive dropped connections
|
|
123
123
|
|
|
124
|
-
The run's lifetime is decoupled from the stream: the server drives it to completion and persists the result even if the connection drops. The hook reads the run id from the stream's start
|
|
124
|
+
The run's lifetime is decoupled from the stream: the server drives it to completion and persists the result even if the connection drops. The hook reads the run id from the stream's start and recovers through the **persisted run row** — the source of truth — in BOTH failure shapes:
|
|
125
125
|
|
|
126
|
-
**
|
|
126
|
+
- **The connection drops with an error** → the hook polls the persisted run to completion (every 2.5 s, bounded at 11 minutes) and resolves with the settled result instead of surfacing a network error.
|
|
127
|
+
- **The stream ends cleanly WITHOUT a `finish` frame** — an edge/proxy can close a long SSE gracefully mid-run, which looks like completion but swallowed the trailing frames (including the structured result). The hook detects the missing `finish` and polls the row the same way. Each such truncation emits the `app_agent_stream_truncated` analytics event (with whether the row rescued the result), so edge-cut frequency is visible fleet-wide.
|
|
128
|
+
|
|
129
|
+
A run still going when the poll deadline passes (the hard cap is 20 min) is **not** recovered on the client: the poll gives up and the drop surfaces as an error, though the result is still persisted server-side. Only when no run id was ever received (the run never started) does the failure reject before any polling.
|
|
130
|
+
|
|
131
|
+
On recovery, `output` adopts the row's output **only when it is an object** (a structured result) — the "`output` is never a stray string" rule holds on every path. A **free-text** run recovered from truncation keeps only the streamed prefix in `text`; read the full settled answer via `useAgentRuns(sessionId)` if you need it.
|
|
127
132
|
|
|
128
133
|
### Sessions
|
|
129
134
|
|