@lotics/app-sdk 0.61.0 → 0.62.0
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/AGENTS.md +1 -1
- package/dist/src/agent_stream.d.ts +52 -2
- package/dist/src/agent_stream.js +47 -2
- package/dist/src/hooks.d.ts +12 -9
- package/dist/src/hooks.js +12 -9
- package/docs/ai.md +6 -4
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -21,7 +21,7 @@ signature; open the file.**
|
|
|
21
21
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
22
22
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions`, `useViewer`, `useComments`, and the `@lotics/ui` components they feed. |
|
|
23
23
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
24
|
-
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** — images/PDFs are perceived natively, Word/Excel/CSV/text are materialized into the run; no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`** (`run_app_query` / `run_app_workflow`); the raw record read/write tools are rejected
|
|
24
|
+
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** — images/PDFs are perceived natively, Word/Excel/CSV/text are materialized into the run; no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`** (`run_app_query` / `run_app_workflow`); the raw record read/write tools are rejected., a leg's `AgentRunLanding` (`settled`/`parked`/`failed`/`aborted` — never a bare `undefined`) |
|
|
25
25
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
26
26
|
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
27
27
|
|
|
@@ -104,8 +104,6 @@ export interface PendingChoice {
|
|
|
104
104
|
toolCallId: string;
|
|
105
105
|
questions: ChoiceQuestion[];
|
|
106
106
|
}
|
|
107
|
-
/** The run's pending `ask_user_choice`, parsed for rendering (a `ClarifyWizard`
|
|
108
|
-
* maps 1:1) — non-null exactly while the run is parked awaiting the answer. */
|
|
109
107
|
export declare function pendingInteractiveCall(state: AgentRunState): PendingChoice | null;
|
|
110
108
|
/**
|
|
111
109
|
* Fold the user's picks into the tool's output shape — `answers` aligns to
|
|
@@ -147,4 +145,56 @@ export declare function parseSseChunks(buffer: string): {
|
|
|
147
145
|
chunks: Chunk[];
|
|
148
146
|
rest: string;
|
|
149
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* How one run LEG ended — the whole answer, in the resolved value.
|
|
150
|
+
*
|
|
151
|
+
* A leg used to resolve `TOutput | undefined`, and `undefined` meant three
|
|
152
|
+
* different things: the run parked on a question, the run died, or the client
|
|
153
|
+
* stopped listening. Telling them apart needed the hook's state, which has not
|
|
154
|
+
* committed yet when the promise resolves — so every consumer raced, and none
|
|
155
|
+
* could win. Minh Tín's app lost live `awaiting_input` runs to exactly that: the
|
|
156
|
+
* dialog closed over a healthy, answerable row and the run expired on its TTL.
|
|
157
|
+
*
|
|
158
|
+
* Discriminating here makes the race unrepresentable rather than guarded. A run
|
|
159
|
+
* FAILURE is data (`failed`) — the caller needs no try/catch for it.
|
|
160
|
+
*
|
|
161
|
+
* The promise still REJECTS for the two cases that are not run outcomes at all:
|
|
162
|
+
* API misuse (`answerChoice` with nothing pending), and a refused answer (400
|
|
163
|
+
* invalid / 409 raced cancel), where the run stays PARKED and answerable — a
|
|
164
|
+
* landing there would report `parked` and swallow the reason the answer bounced.
|
|
165
|
+
*/
|
|
166
|
+
export type AgentRunLanding<TOutput> =
|
|
167
|
+
/** The leg finished. `output` is the structured result (absent for a free-text
|
|
168
|
+
* agent, where `text` is the result). */
|
|
169
|
+
{
|
|
170
|
+
kind: "settled";
|
|
171
|
+
output?: TOutput;
|
|
172
|
+
text: string;
|
|
173
|
+
}
|
|
174
|
+
/** Parked on a question. `pendingChoice` carries it and `answerChoice`
|
|
175
|
+
* continues — there is nothing for the caller to do here, which is the point:
|
|
176
|
+
* it is no longer indistinguishable from a dead run. */
|
|
177
|
+
| {
|
|
178
|
+
kind: "parked";
|
|
179
|
+
}
|
|
180
|
+
/** The run failed and its error is user-facing. */
|
|
181
|
+
| {
|
|
182
|
+
kind: "failed";
|
|
183
|
+
error: string;
|
|
184
|
+
}
|
|
185
|
+
/** The CLIENT stopped listening (`abort`, unmount, a newer run replacing this
|
|
186
|
+
* one). The run itself keeps executing server-side and lands in the session
|
|
187
|
+
* history — never surface this as a failure. */
|
|
188
|
+
| {
|
|
189
|
+
kind: "aborted";
|
|
190
|
+
};
|
|
191
|
+
/** The client stopped listening; the run itself continues server-side. Shared so
|
|
192
|
+
* the four exit points cannot drift, and so the literal keeps its type. */
|
|
193
|
+
export declare const ABORTED: AgentRunLanding<never>;
|
|
194
|
+
/** All prose parts concatenated — a free-text agent's actual result. Shared so
|
|
195
|
+
* the hook's `text` and a landing's can never disagree about what was said. */
|
|
196
|
+
export declare function proseOf(parts: readonly AgentUIPart[]): string;
|
|
197
|
+
/** The landing a settled accumulator describes. One place decides, so `run` and
|
|
198
|
+
* `answerChoice` can never disagree about what an outcome was called. */
|
|
199
|
+
export declare function landingOf(state: AgentRunState): AgentRunLanding<unknown>;
|
|
150
200
|
export {};
|
package/dist/src/agent_stream.js
CHANGED
|
@@ -66,6 +66,15 @@ export function adoptSettledRun(state, settled) {
|
|
|
66
66
|
}
|
|
67
67
|
/** The run's pending `ask_user_choice`, parsed for rendering (a `ClarifyWizard`
|
|
68
68
|
* maps 1:1) — non-null exactly while the run is parked awaiting the answer. */
|
|
69
|
+
/** Untrusted model output — a non-JSON string is data, not an exception. */
|
|
70
|
+
function decodeJson(raw) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
export function pendingInteractiveCall(state) {
|
|
70
79
|
if (state.status !== "awaiting_input")
|
|
71
80
|
return null;
|
|
@@ -81,9 +90,16 @@ export function pendingInteractiveCall(state) {
|
|
|
81
90
|
return null;
|
|
82
91
|
}
|
|
83
92
|
function parseChoiceQuestions(input) {
|
|
84
|
-
|
|
93
|
+
// A model may hand the tool its arguments as a JSON STRING rather than an
|
|
94
|
+
// object. The SERVER accepts either and parks the run, so refusing the string
|
|
95
|
+
// here strands a live `awaiting_input` run: the question exists, the run is
|
|
96
|
+
// answerable, and the UI reports it as unrecoverable. Parse before validating
|
|
97
|
+
// — a string that isn't JSON falls through to the same empty result as any
|
|
98
|
+
// other malformed input, which `adoptGuarded` surfaces loudly and retryably.
|
|
99
|
+
const decoded = typeof input === "string" ? decodeJson(input) : input;
|
|
100
|
+
if (!decoded || typeof decoded !== "object")
|
|
85
101
|
return [];
|
|
86
|
-
const record =
|
|
102
|
+
const record = decoded;
|
|
87
103
|
if (!Array.isArray(record.questions))
|
|
88
104
|
return [];
|
|
89
105
|
const questions = [];
|
|
@@ -267,3 +283,32 @@ export function parseSseChunks(buffer) {
|
|
|
267
283
|
}
|
|
268
284
|
return { chunks, rest };
|
|
269
285
|
}
|
|
286
|
+
/** The client stopped listening; the run itself continues server-side. Shared so
|
|
287
|
+
* the four exit points cannot drift, and so the literal keeps its type. */
|
|
288
|
+
export const ABORTED = { kind: "aborted" };
|
|
289
|
+
/** All prose parts concatenated — a free-text agent's actual result. Shared so
|
|
290
|
+
* the hook's `text` and a landing's can never disagree about what was said. */
|
|
291
|
+
export function proseOf(parts) {
|
|
292
|
+
return parts.reduce((acc, p) => (p.type === "text" ? acc + p.text : acc), "");
|
|
293
|
+
}
|
|
294
|
+
/** The landing a settled accumulator describes. One place decides, so `run` and
|
|
295
|
+
* `answerChoice` can never disagree about what an outcome was called. */
|
|
296
|
+
export function landingOf(state) {
|
|
297
|
+
switch (state.status) {
|
|
298
|
+
case "awaiting_input":
|
|
299
|
+
return { kind: "parked" };
|
|
300
|
+
case "error":
|
|
301
|
+
return { kind: "failed", error: state.error ?? "The run failed." };
|
|
302
|
+
case "completed":
|
|
303
|
+
return { kind: "settled", output: state.output, text: proseOf(state.parts) };
|
|
304
|
+
case "streaming":
|
|
305
|
+
// A leg that is still streaming has not landed. Every internal caller has
|
|
306
|
+
// already resolved this state — by polling the persisted row — before
|
|
307
|
+
// asking, so this branch is unreachable there. It is named rather than
|
|
308
|
+
// left to a fallthrough because the fallthrough answered "settled", which
|
|
309
|
+
// would report a live run as finished and hand a consumer a partial
|
|
310
|
+
// `output` as if it were the result. Unconfirmed is the truthful answer,
|
|
311
|
+
// and it matches what the poll path says for the same situation.
|
|
312
|
+
return { kind: "failed", error: "The run has not finished — its result cannot be confirmed yet." };
|
|
313
|
+
}
|
|
314
|
+
}
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type ImageFidelity } from "./upload/optimize.js";
|
|
2
2
|
import { type AiContextValue } from "./rpc.js";
|
|
3
|
-
import { type AgentUIPart, type PendingChoice } from "./agent_stream.js";
|
|
3
|
+
import { type AgentUIPart, type PendingChoice, type AgentRunLanding } from "./agent_stream.js";
|
|
4
4
|
import type { AppWorkflows, AppWorkflowResults, AppQueries, AppAgents, AppAgentResults } from "./types.js";
|
|
5
5
|
import type { ResolvedMember } from "./members.js";
|
|
6
6
|
import type { ResolvedOption } from "./select.js";
|
|
7
|
-
export type { AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput } from "./agent_stream.js";
|
|
7
|
+
export type { AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, AgentRunLanding } from "./agent_stream.js";
|
|
8
8
|
export { buildChoiceOutput } from "./agent_stream.js";
|
|
9
9
|
/** Fields shared by every query hook's return value. */
|
|
10
10
|
interface QueryStateBase {
|
|
@@ -472,10 +472,12 @@ export interface AgentRunOptions {
|
|
|
472
472
|
}
|
|
473
473
|
/** The live state + controls returned by `useAgentRun`. */
|
|
474
474
|
export interface UseAgentRun<TInput, TOutput> {
|
|
475
|
-
/** Start a run — streams progress into this hook's state and resolves
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
|
|
475
|
+
/** Start a run — streams progress into this hook's state and resolves with how
|
|
476
|
+
* the leg ENDED (`settled` / `parked` / `failed` / `aborted`). Read `kind`
|
|
477
|
+
* rather than the hook's state: this value is a snapshot at settle, whereas
|
|
478
|
+
* the state has not committed yet when the promise resolves. SINGLE-FLIGHT —
|
|
479
|
+
* a second call while one is streaming joins the first (see `replace`). */
|
|
480
|
+
run: (input: TInput, opts: AgentRunOptions) => Promise<AgentRunLanding<TOutput>>;
|
|
479
481
|
/** Stop listening locally (no server effect) — the run keeps executing
|
|
480
482
|
* server-side and its result lands in the session history. Used on unmount. */
|
|
481
483
|
abort: () => void;
|
|
@@ -497,12 +499,13 @@ export interface UseAgentRun<TInput, TOutput> {
|
|
|
497
499
|
/** Answer the pending ask and CONTINUE the run — one `{value, custom}` per
|
|
498
500
|
* question, aligned by index (exactly what `ClarifyWizard`'s `onSubmit`
|
|
499
501
|
* yields). Streams the continuation into the same `parts` and resolves like
|
|
500
|
-
* `run`
|
|
501
|
-
* pending
|
|
502
|
+
* `run` — a landing, which may be `parked` again for a follow-up ask. Rejects
|
|
503
|
+
* when nothing is pending, or when the server refuses the answer (the run
|
|
504
|
+
* stays parked; show the error and keep the wizard open). */
|
|
502
505
|
answerChoice: (answers: {
|
|
503
506
|
value: string;
|
|
504
507
|
custom: boolean;
|
|
505
|
-
}[]) => Promise<TOutput
|
|
508
|
+
}[]) => Promise<AgentRunLanding<TOutput>>;
|
|
506
509
|
/** The agent's ANSWER prose (every `text` part concatenated), accumulating live —
|
|
507
510
|
* excludes thinking (`reasoning` is its own part). For a FREE-TEXT agent this IS
|
|
508
511
|
* the result; a structured agent's result is `output`. Derived from `parts`. */
|
package/dist/src/hooks.js
CHANGED
|
@@ -20,7 +20,7 @@ import { DEFAULT_IMAGE_FIDELITY } from "./upload/optimize.js";
|
|
|
20
20
|
import useSWR from "swr";
|
|
21
21
|
import useSWRInfinite from "swr/infinite";
|
|
22
22
|
import { rpc, rpcAgentRun, rpcAgentRunContinue, postHostNotification, subscribeHostRefetch, } from "./rpc.js";
|
|
23
|
-
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, adoptSettledRun, pendingInteractiveCall, buildChoiceOutput, applyInteractiveAnswer, } from "./agent_stream.js";
|
|
23
|
+
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, adoptSettledRun, pendingInteractiveCall, buildChoiceOutput, applyInteractiveAnswer, landingOf, proseOf, ABORTED, } from "./agent_stream.js";
|
|
24
24
|
import { getMockRows, hasMockFlag } from "./mock.js";
|
|
25
25
|
import { captureAppEvent } from "./analytics.js";
|
|
26
26
|
export { buildChoiceOutput } from "./agent_stream.js";
|
|
@@ -536,7 +536,7 @@ export function useAgentRun(alias) {
|
|
|
536
536
|
return handle.done
|
|
537
537
|
.then(async () => {
|
|
538
538
|
if (aborted)
|
|
539
|
-
return
|
|
539
|
+
return ABORTED;
|
|
540
540
|
// A clean stream end WITHOUT a `finish` frame is a truncation, not a
|
|
541
541
|
// completion — an edge can close a long SSE gracefully mid-run (seen
|
|
542
542
|
// in production on ~2-minute runs), swallowing the frames that carry
|
|
@@ -551,7 +551,7 @@ export function useAgentRun(alias) {
|
|
|
551
551
|
? await pollAgentRunToSettle(runId, () => aborted || !mountedRef.current)
|
|
552
552
|
: null;
|
|
553
553
|
if (aborted)
|
|
554
|
-
return
|
|
554
|
+
return ABORTED;
|
|
555
555
|
// No settled row after the full poll window means the run's status
|
|
556
556
|
// could NOT be confirmed (an orphan the server's reaper hasn't
|
|
557
557
|
// repaired yet, or no run id ever arrived) — that is an error, never
|
|
@@ -574,11 +574,11 @@ export function useAgentRun(alias) {
|
|
|
574
574
|
has_output: acc.output !== undefined,
|
|
575
575
|
});
|
|
576
576
|
}
|
|
577
|
-
return acc
|
|
577
|
+
return landingOf(acc);
|
|
578
578
|
})
|
|
579
579
|
.catch(async (err) => {
|
|
580
580
|
if (aborted)
|
|
581
|
-
return
|
|
581
|
+
return ABORTED;
|
|
582
582
|
// The continue request itself was rejected — the server never resumed
|
|
583
583
|
// the run (400 invalid answer, 409 raced cancel/expiry, network at
|
|
584
584
|
// connect). Restore the pre-answer parked state and surface the error;
|
|
@@ -594,7 +594,7 @@ export function useAgentRun(alias) {
|
|
|
594
594
|
if (runId) {
|
|
595
595
|
const settled = await pollAgentRunToSettle(runId, () => aborted || !mountedRef.current);
|
|
596
596
|
if (aborted)
|
|
597
|
-
return
|
|
597
|
+
return ABORTED;
|
|
598
598
|
if (settled) {
|
|
599
599
|
acc = adoptGuarded(acc, settled);
|
|
600
600
|
safeSetState(acc);
|
|
@@ -610,11 +610,14 @@ export function useAgentRun(alias) {
|
|
|
610
610
|
has_output: acc.output !== undefined,
|
|
611
611
|
});
|
|
612
612
|
if (settled)
|
|
613
|
-
return acc
|
|
613
|
+
return landingOf(acc);
|
|
614
614
|
}
|
|
615
|
+
// A run failure is DATA — the landing carries it, so no consumer needs
|
|
616
|
+
// try/catch to tell "the run died" from "the run parked". Only API
|
|
617
|
+
// MISUSE still rejects (answering when nothing is pending).
|
|
615
618
|
acc = { ...acc, status: "error", error: err.message };
|
|
616
619
|
safeSetState(acc);
|
|
617
|
-
|
|
620
|
+
return landingOf(acc);
|
|
618
621
|
});
|
|
619
622
|
}, [safeSetState]);
|
|
620
623
|
const run = useCallback((input, opts) => {
|
|
@@ -679,7 +682,7 @@ export function useAgentRun(alias) {
|
|
|
679
682
|
// `parts` is the source of truth; `text` (all prose concatenated) is the derived
|
|
680
683
|
// answer view.
|
|
681
684
|
const parts = state?.parts ?? EMPTY_PARTS;
|
|
682
|
-
const text = useMemo(() => parts
|
|
685
|
+
const text = useMemo(() => proseOf(parts), [parts]);
|
|
683
686
|
return {
|
|
684
687
|
run,
|
|
685
688
|
abort,
|
package/docs/ai.md
CHANGED
|
@@ -56,12 +56,12 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
56
56
|
|
|
57
57
|
| Member | Type | What it is |
|
|
58
58
|
|---|---|---|
|
|
59
|
-
| `run` | `(input, { sessionId, replace? }) => Promise<TOutput
|
|
59
|
+
| `run` | `(input, { sessionId, replace? }) => Promise<AgentRunLanding<TOutput>>` | Start a run. Streams progress into the hook's state and resolves with **how the leg ended** — `{kind:"settled", output?, text}`, `{kind:"parked"}`, `{kind:"failed", error}` or `{kind:"aborted"}`. Switch on `kind`; do NOT read the hook's state to tell them apart, because it has not committed when the promise resolves (that race closed dialogs over live, answerable runs). A run failure is DATA here, so no try/catch is needed for it. **Single-flight:** while a run is in flight, calling it again returns the in-flight run's promise — an accidental double-press joins the first run instead of billing a second one (each dedup emits the `app_agent_run_deduped` analytics event). Pass `replace: true` to deliberately abort-and-restart; the replaced run still executes and bills server-side |
|
|
60
60
|
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this |
|
|
61
61
|
| `abort` | `() => void` | Stop listening **locally only** — the run keeps executing server-side and its result is still persisted. This is the unmount path (the hook calls it automatically on unmount) |
|
|
62
62
|
| `status` | `"idle" \| "streaming" \| "awaiting_input" \| "completed" \| "error"` | Whole-run state. `awaiting_input` = the run is PARKED on a question the agent asked (see the ask-back section below). `abort`/`cancel` reset it to `"idle"` (and clear the partial transcript) |
|
|
63
63
|
| `pendingChoice` | `PendingChoice \| null` | The agent's pending question(s) — non-null exactly while `status` is `awaiting_input`. `questions` maps 1:1 onto `@lotics/ui`'s `ClarifyWizard` (`{question, options: {label, description}[], allow_custom}`) |
|
|
64
|
-
| `answerChoice` | `(answers: {value, custom}[]) => Promise<TOutput
|
|
64
|
+
| `answerChoice` | `(answers: {value, custom}[]) => Promise<AgentRunLanding<TOutput>>` | Answer the pending question(s) and CONTINUE the run — one entry per question, aligned by index (exactly what `ClarifyWizard`'s `onSubmit` yields). Streams the continuation into the same `parts`; resolves like `run`. Rejects when nothing is pending — and when the server refuses the answer, in which case the pending question is restored for a retry |
|
|
65
65
|
| `parts` | `AgentUIPart[]` | The ordered live transcript as **ai-sdk `UIMessage.parts`** — answer prose, thinking, and tool calls, in stream order. The single source of truth for the feed; hand it straight to `@lotics/ui` `AgentRun` |
|
|
66
66
|
| `text` | `string` | The agent's **answer prose** (every `text` part concatenated), accumulating live. Excludes thinking. For a free-text agent this IS the result |
|
|
67
67
|
| `output` | `TOutput \| undefined` | The structured result once the run completes. `undefined` when the run produced none |
|
|
@@ -125,8 +125,9 @@ const run = useAgentRun("importer");
|
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
The ask renders in the feed as a settled tool row once answered (the answer rides its
|
|
128
|
-
on-demand reveal). `run()`
|
|
129
|
-
|
|
128
|
+
on-demand reveal). `run()` resolves `{kind:"parked"}` when the run parks — nothing for you
|
|
129
|
+
to do there, the wizard renders off `pendingChoice` — and the continuation's landing (from
|
|
130
|
+
`answerChoice`) carries the final output, or `parked` again for a follow-up ask. The question is as
|
|
130
131
|
connection-decoupled as the run: a dropped stream can't lose it — the hook's recovery poll
|
|
131
132
|
rebuilds the pending question from the persisted run, so `awaiting_input` always yields an
|
|
132
133
|
answerable `pendingChoice` (the one unrecoverable corner surfaces a retryable `error`, never
|
|
@@ -382,3 +383,4 @@ The floors below are when each capability shipped in `@lotics/app-sdk`; an app p
|
|
|
382
383
|
| `askAi` | `@lotics/app-sdk` 0.45 |
|
|
383
384
|
| `useAiContext` (ambient-chat view state + auto query refetch on chat mutation) | `@lotics/app-sdk` 0.52 |
|
|
384
385
|
| The agent asks back (`pendingChoice`/`answerChoice`, `awaiting_input`) | `@lotics/app-sdk` 0.55 |
|
|
386
|
+
| A leg resolves an `AgentRunLanding` (`settled`/`parked`/`failed`/`aborted`) instead of `TOutput \| undefined` | `@lotics/app-sdk` 0.62 |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Runtime SDK for Lotics custom-code apps
|
|
3
|
+
"version": "0.62.0",
|
|
4
|
+
"description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|