@lotics/app-sdk 0.100.1 → 0.101.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/AGENTS.md +32 -47
- package/dist/agent_stream.d.ts +131 -0
- package/dist/ask_ai.d.ts +27 -0
- package/dist/attachments.d.ts +58 -0
- package/dist/chunk-ARV5FAU5.js +1132 -0
- package/dist/comments.d.ts +89 -0
- package/dist/error_report.d.ts +9 -0
- package/dist/folder_pick.d.ts +8 -0
- package/dist/geolocation.d.ts +42 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/{src/index.d.ts → index.d.ts} +13 -22
- package/dist/index.js +31331 -0
- package/dist/index.js.LEGAL.txt +11 -0
- package/dist/members.d.ts +32 -0
- package/dist/mock.d.ts +37 -0
- package/dist/mount.d.ts +19 -0
- package/dist/new_record.d.ts +37 -0
- package/dist/open_app.d.ts +12 -0
- package/dist/open_external.d.ts +10 -0
- package/dist/overlay.d.ts +25 -0
- package/dist/queries.d.ts +231 -0
- package/dist/recording.d.ts +47 -0
- package/dist/recording_state.d.ts +43 -0
- package/dist/rename_file.d.ts +13 -0
- package/dist/router.d.ts +10 -0
- package/dist/router.js +97 -0
- package/dist/row.d.ts +87 -0
- package/dist/rpc.d.ts +114 -0
- package/dist/select.d.ts +24 -0
- package/dist/shared_types.d.ts +8 -0
- package/dist/store.d.ts +43 -0
- package/dist/types.d.ts +36 -0
- package/dist/upload/optimize.d.ts +30 -0
- package/dist/upload/pipeline.d.ts +36 -0
- package/dist/upload/transport.d.ts +19 -0
- package/dist/url_params.d.ts +55 -0
- package/dist/use_recents.d.ts +15 -0
- package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
- package/dist/viewer.d.ts +41 -0
- package/dist/written.d.ts +79 -0
- package/docs/ai.md +74 -133
- package/docs/data_fetching.md +209 -290
- package/docs/files.md +61 -51
- package/docs/members_and_options.md +92 -62
- package/docs/mutations.md +136 -205
- package/docs/navigation_and_state.md +26 -35
- package/docs/queries.md +144 -207
- package/docs/recipes.md +21 -45
- package/docs/runtime.md +74 -137
- package/docs/security.md +8 -11
- package/docs/workflows.md +189 -174
- package/package.json +27 -28
- package/dist/src/agent_stream.d.ts +0 -200
- package/dist/src/agent_stream.js +0 -314
- package/dist/src/ask_ai.d.ts +0 -40
- package/dist/src/ask_ai.js +0 -35
- package/dist/src/attachments.d.ts +0 -68
- package/dist/src/attachments.js +0 -93
- package/dist/src/comments.d.ts +0 -127
- package/dist/src/comments.js +0 -192
- package/dist/src/download.js +0 -54
- package/dist/src/geolocation.d.ts +0 -64
- package/dist/src/geolocation.js +0 -96
- package/dist/src/hooks.d.ts +0 -781
- package/dist/src/hooks.js +0 -860
- package/dist/src/index.js +0 -34
- package/dist/src/members.d.ts +0 -105
- package/dist/src/members.js +0 -62
- package/dist/src/mock.d.ts +0 -118
- package/dist/src/mock.js +0 -124
- package/dist/src/mount.d.ts +0 -47
- package/dist/src/mount.js +0 -34
- package/dist/src/new_record.d.ts +0 -74
- package/dist/src/new_record.js +0 -117
- package/dist/src/open_app.d.ts +0 -15
- package/dist/src/open_app.js +0 -18
- package/dist/src/open_external.d.ts +0 -16
- package/dist/src/open_external.js +0 -19
- package/dist/src/recording.d.ts +0 -59
- package/dist/src/recording.js +0 -30
- package/dist/src/recording_state.d.ts +0 -59
- package/dist/src/recording_state.js +0 -94
- package/dist/src/router.d.ts +0 -17
- package/dist/src/router.js +0 -144
- package/dist/src/row.d.ts +0 -159
- package/dist/src/row.js +0 -254
- package/dist/src/rpc.d.ts +0 -207
- package/dist/src/rpc.js +0 -904
- package/dist/src/select.d.ts +0 -48
- package/dist/src/select.js +0 -40
- package/dist/src/types.d.ts +0 -115
- package/dist/src/types.js +0 -1
- package/dist/src/upload/optimize.d.ts +0 -54
- package/dist/src/upload/optimize.js +0 -207
- package/dist/src/upload/pipeline.d.ts +0 -55
- package/dist/src/upload/pipeline.js +0 -52
- package/dist/src/upload/transport.d.ts +0 -42
- package/dist/src/upload/transport.js +0 -128
- package/dist/src/url_params.d.ts +0 -93
- package/dist/src/url_params.js +0 -215
- package/dist/src/use_optimistic.d.ts +0 -27
- package/dist/src/use_optimistic.js +0 -27
- package/dist/src/use_recents.d.ts +0 -19
- package/dist/src/use_recents.js +0 -71
- package/dist/src/use_url_state.js +0 -73
- package/dist/src/viewer.d.ts +0 -26
- package/dist/src/viewer.js +0 -47
- /package/dist/{src/download.d.ts → download.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,57 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.101.1",
|
|
4
|
+
"description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"types": "./dist/
|
|
9
|
-
"default": "./dist/
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"./router": {
|
|
12
|
-
"types": "./dist/
|
|
13
|
-
"default": "./dist/
|
|
12
|
+
"types": "./dist/router.d.ts",
|
|
13
|
+
"default": "./dist/router.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"types": "./dist/
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
19
|
"AGENTS.md",
|
|
20
20
|
"docs"
|
|
21
21
|
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
22
25
|
"scripts": {
|
|
23
|
-
"build": "tsgo -p tsconfig.build.json",
|
|
24
|
-
"
|
|
26
|
+
"build": "tsgo -p tsconfig.build.json && node scripts/build_sdk.mjs",
|
|
27
|
+
"lint": "oxlint",
|
|
25
28
|
"test": "vitest run",
|
|
26
|
-
"
|
|
29
|
+
"typecheck": "tsgo --noEmit",
|
|
30
|
+
"prepack": "npm run build"
|
|
27
31
|
},
|
|
28
32
|
"dependencies": {
|
|
29
|
-
"ai": "^7.0.30"
|
|
30
|
-
"swr": "^2.4.1"
|
|
33
|
+
"ai": "^7.0.30"
|
|
31
34
|
},
|
|
32
35
|
"peerDependencies": {
|
|
33
36
|
"react": "^19.2.0",
|
|
34
37
|
"react-dom": "^19.2.0",
|
|
35
38
|
"react-router": "^7.0.0 || ^8.0.0"
|
|
36
39
|
},
|
|
37
|
-
"peerDependenciesMeta": {
|
|
38
|
-
"react-router": {
|
|
39
|
-
"optional": true
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
40
|
"devDependencies": {
|
|
41
|
+
"@lotics/shared": "*",
|
|
42
|
+
"@testing-library/react": "^16.3.0",
|
|
43
43
|
"@types/react": "^19.2.17",
|
|
44
|
-
"@types/react-dom": "
|
|
45
|
-
"react": "
|
|
46
|
-
"
|
|
47
|
-
"
|
|
44
|
+
"@types/react-dom": "~19.2.2",
|
|
45
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
46
|
+
"esbuild": "^0.28.1",
|
|
47
|
+
"jsdom": "^27.0.0",
|
|
48
|
+
"react": "^19.2.0",
|
|
49
|
+
"react-dom": "^19.2.0",
|
|
50
|
+
"react-router": "^7.18.2",
|
|
51
|
+
"typescript": "~6.0.3",
|
|
52
|
+
"vite": "^7.2.4"
|
|
48
53
|
},
|
|
49
|
-
"keywords": [
|
|
50
|
-
"lotics",
|
|
51
|
-
"app-sdk",
|
|
52
|
-
"react",
|
|
53
|
-
"iframe"
|
|
54
|
-
],
|
|
55
54
|
"engines": {
|
|
56
55
|
"node": ">=18"
|
|
57
56
|
},
|
|
@@ -59,6 +58,6 @@
|
|
|
59
58
|
"repository": {
|
|
60
59
|
"type": "git",
|
|
61
60
|
"url": "https://github.com/lotics/lotics.git",
|
|
62
|
-
"directory": "packages/
|
|
61
|
+
"directory": "packages/app_sdk"
|
|
63
62
|
}
|
|
64
63
|
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a backend app-agent run stream into the app's agent-run model.
|
|
3
|
-
*
|
|
4
|
-
* The backend streams the AI-SDK "UI message" SSE protocol (the same wire format
|
|
5
|
-
* the chat uses) — `data: <json>\n\n` frames, each a typed chunk, terminated by
|
|
6
|
-
* `data: [DONE]`. We fold the chunk types that matter into an ordered `parts` array
|
|
7
|
-
* — the SAME ai-sdk `UIMessagePart[]` shape `@lotics/ui`'s `AgentRun` renders, so
|
|
8
|
-
* `<AgentRun parts={run.parts} />` needs no adapter. `ai` is a TYPE-ONLY import here
|
|
9
|
-
* (tool calls are hand-built as concrete `dynamic-tool` parts); NO `ai` runtime
|
|
10
|
-
* enters the sandboxed app bundle. Unknown chunk types are ignored, so a newer
|
|
11
|
-
* AI-SDK never breaks the SDK.
|
|
12
|
-
*
|
|
13
|
-
* An agent can be either kind: a STRUCTURED agent (declares `outputs`) emits its
|
|
14
|
-
* result as the input of the injected `submit_result` tool → `output` (NOT a part);
|
|
15
|
-
* a FREE-TEXT agent's result is its prose → a text part. `output` is NEVER the
|
|
16
|
-
* free-text (see `AgentRunState.output`). Pure + reducer-shaped so it is fully
|
|
17
|
-
* unit-testable against recorded frames — no live model needed.
|
|
18
|
-
*/
|
|
19
|
-
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
20
|
-
/** An ai-sdk message part — the render model, tool-set-agnostic. */
|
|
21
|
-
export type AgentUIPart = UIMessagePart<UIDataTypes, UITools>;
|
|
22
|
-
/**
|
|
23
|
-
* The MANDATORY interactive tools every app-agent run carries (mirrors the
|
|
24
|
-
* server's `INTERACTIVE_APP_TOOLS`). A call to one PARKS the run awaiting the
|
|
25
|
-
* user's answer: its part stays `input-available` through `finish`, the state
|
|
26
|
-
* reads `awaiting_input`, and `useAgentRun().answerChoice` continues the run.
|
|
27
|
-
*/
|
|
28
|
-
export declare const INTERACTIVE_TOOLS: ReadonlySet<string>;
|
|
29
|
-
export interface AgentRunState {
|
|
30
|
-
status: "streaming" | "awaiting_input" | "completed" | "error";
|
|
31
|
-
/** The ordered transcript as ai-sdk `parts` — prose interleaved with the tool
|
|
32
|
-
* calls (`dynamic-tool` parts), as it streamed. The single source of truth for
|
|
33
|
-
* the feed; `useAgentRun` derives `text` / `steps` from it. */
|
|
34
|
-
parts: AgentUIPart[];
|
|
35
|
-
/** The structured result — the `submit_result` tool's input — or `undefined`.
|
|
36
|
-
* NEVER the free-text answer: a free-text agent (or a structured one that
|
|
37
|
-
* finished without submitting) has no `output`; its answer is the transcript's
|
|
38
|
-
* text. Coercing text here would hand a consumer reading `output.<field>` a bare
|
|
39
|
-
* string, crashing it ("... is not iterable"). */
|
|
40
|
-
output?: unknown;
|
|
41
|
-
error?: string;
|
|
42
|
-
}
|
|
43
|
-
/** A settled run row, as the poll endpoint returns it. */
|
|
44
|
-
export interface SettledAgentRun {
|
|
45
|
-
status: string;
|
|
46
|
-
output?: unknown;
|
|
47
|
-
error_message?: string | null;
|
|
48
|
-
/** Present on a polled `awaiting_input` row: the pending ask derived from the
|
|
49
|
-
* server-side transcript, so a client that lost the stream (or reloaded) can
|
|
50
|
-
* rebuild the question without ever having received the part. */
|
|
51
|
-
pending_interactive?: {
|
|
52
|
-
tool_call_id: string;
|
|
53
|
-
tool_name: string;
|
|
54
|
-
input: unknown;
|
|
55
|
-
} | null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Fold a POLLED settled run row into the stream-accumulated state — the shared
|
|
59
|
-
* adoption step for BOTH recovery paths (a dropped stream, and a stream that
|
|
60
|
-
* ended cleanly WITHOUT a `finish` frame — an edge can close a long SSE
|
|
61
|
-
* gracefully mid-run, which looks like completion but is a truncation).
|
|
62
|
-
*
|
|
63
|
-
* The row is the source of truth for status and the STRUCTURED output. A
|
|
64
|
-
* free-text run's row `output` is its prose string — that never enters
|
|
65
|
-
* `state.output` (a structured consumer reads `output.<field>`; a stray string
|
|
66
|
-
* would crash it). The free-text answer stays in the transcript text.
|
|
67
|
-
*/
|
|
68
|
-
export declare function adoptSettledRun(state: AgentRunState, settled: SettledAgentRun): AgentRunState;
|
|
69
|
-
export type ChoiceOption = {
|
|
70
|
-
label: string;
|
|
71
|
-
description: string;
|
|
72
|
-
};
|
|
73
|
-
export type ChoiceQuestion = {
|
|
74
|
-
question: string;
|
|
75
|
-
options: ChoiceOption[];
|
|
76
|
-
allow_custom?: boolean;
|
|
77
|
-
};
|
|
78
|
-
export type AskUserChoiceAnswer = {
|
|
79
|
-
type: "option";
|
|
80
|
-
question_index: number;
|
|
81
|
-
question_number: number;
|
|
82
|
-
question_text: string;
|
|
83
|
-
option_index: number;
|
|
84
|
-
option_letter: string;
|
|
85
|
-
option_label: string;
|
|
86
|
-
option_description: string;
|
|
87
|
-
} | {
|
|
88
|
-
type: "custom";
|
|
89
|
-
question_index: number;
|
|
90
|
-
question_number: number;
|
|
91
|
-
question_text: string;
|
|
92
|
-
text: string;
|
|
93
|
-
} | {
|
|
94
|
-
type: "skipped";
|
|
95
|
-
question_index: number;
|
|
96
|
-
question_number: number;
|
|
97
|
-
question_text: string;
|
|
98
|
-
};
|
|
99
|
-
export type AskUserChoiceOutput = {
|
|
100
|
-
answers: AskUserChoiceAnswer[];
|
|
101
|
-
skipped_by_user?: boolean;
|
|
102
|
-
};
|
|
103
|
-
export interface PendingChoice {
|
|
104
|
-
toolCallId: string;
|
|
105
|
-
questions: ChoiceQuestion[];
|
|
106
|
-
}
|
|
107
|
-
export declare function pendingInteractiveCall(state: AgentRunState): PendingChoice | null;
|
|
108
|
-
/**
|
|
109
|
-
* Fold the user's picks into the tool's output shape — `answers` aligns to
|
|
110
|
-
* `questions` by index (the `ClarifyWizard` contract: `{ value, custom }` per
|
|
111
|
-
* question, value = the picked option's label or the free text). A missing or
|
|
112
|
-
* empty entry is a skipped question.
|
|
113
|
-
*/
|
|
114
|
-
export declare function buildChoiceOutput(questions: ChoiceQuestion[], answers: {
|
|
115
|
-
value: string;
|
|
116
|
-
custom: boolean;
|
|
117
|
-
}[]): AskUserChoiceOutput;
|
|
118
|
-
/** The user answered: settle the pending part (the answer rides its `output` for
|
|
119
|
-
* the feed's on-demand reveal) and put the run back into `streaming` for the
|
|
120
|
-
* continuation leg's chunks. */
|
|
121
|
-
export declare function applyInteractiveAnswer(state: AgentRunState, toolCallId: string, output: AskUserChoiceOutput): AgentRunState;
|
|
122
|
-
export declare function initialAgentRunState(): AgentRunState;
|
|
123
|
-
/** A parsed UI-message chunk — only the fields we read, all optional. */
|
|
124
|
-
interface Chunk {
|
|
125
|
-
type?: string;
|
|
126
|
-
delta?: string;
|
|
127
|
-
/** Partial tool-argument text, streamed while a tool call's input is generated
|
|
128
|
-
* (`tool-input-delta`) — distinct from `delta` (answer/thinking prose). */
|
|
129
|
-
inputTextDelta?: string;
|
|
130
|
-
toolName?: string;
|
|
131
|
-
toolCallId?: string;
|
|
132
|
-
input?: unknown;
|
|
133
|
-
output?: unknown;
|
|
134
|
-
errorText?: string;
|
|
135
|
-
finishReason?: string;
|
|
136
|
-
}
|
|
137
|
-
/** Fold one chunk into the run state. Returns a new state (immutable). */
|
|
138
|
-
export declare function reduceAgentChunk(state: AgentRunState, chunk: Chunk): AgentRunState;
|
|
139
|
-
/**
|
|
140
|
-
* Incremental SSE frame splitter. Feed it raw text as it arrives; it returns the
|
|
141
|
-
* complete chunks parsed so far and the leftover partial frame to carry forward.
|
|
142
|
-
* `[DONE]` is dropped (the `finish` chunk already settles the state).
|
|
143
|
-
*/
|
|
144
|
-
export declare function parseSseChunks(buffer: string): {
|
|
145
|
-
chunks: Chunk[];
|
|
146
|
-
rest: string;
|
|
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>;
|
|
200
|
-
export {};
|
package/dist/src/agent_stream.js
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a backend app-agent run stream into the app's agent-run model.
|
|
3
|
-
*
|
|
4
|
-
* The backend streams the AI-SDK "UI message" SSE protocol (the same wire format
|
|
5
|
-
* the chat uses) — `data: <json>\n\n` frames, each a typed chunk, terminated by
|
|
6
|
-
* `data: [DONE]`. We fold the chunk types that matter into an ordered `parts` array
|
|
7
|
-
* — the SAME ai-sdk `UIMessagePart[]` shape `@lotics/ui`'s `AgentRun` renders, so
|
|
8
|
-
* `<AgentRun parts={run.parts} />` needs no adapter. `ai` is a TYPE-ONLY import here
|
|
9
|
-
* (tool calls are hand-built as concrete `dynamic-tool` parts); NO `ai` runtime
|
|
10
|
-
* enters the sandboxed app bundle. Unknown chunk types are ignored, so a newer
|
|
11
|
-
* AI-SDK never breaks the SDK.
|
|
12
|
-
*
|
|
13
|
-
* An agent can be either kind: a STRUCTURED agent (declares `outputs`) emits its
|
|
14
|
-
* result as the input of the injected `submit_result` tool → `output` (NOT a part);
|
|
15
|
-
* a FREE-TEXT agent's result is its prose → a text part. `output` is NEVER the
|
|
16
|
-
* free-text (see `AgentRunState.output`). Pure + reducer-shaped so it is fully
|
|
17
|
-
* unit-testable against recorded frames — no live model needed.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* The MANDATORY interactive tools every app-agent run carries (mirrors the
|
|
21
|
-
* server's `INTERACTIVE_APP_TOOLS`). A call to one PARKS the run awaiting the
|
|
22
|
-
* user's answer: its part stays `input-available` through `finish`, the state
|
|
23
|
-
* reads `awaiting_input`, and `useAgentRun().answerChoice` continues the run.
|
|
24
|
-
*/
|
|
25
|
-
export const INTERACTIVE_TOOLS = new Set(["ask_user_choice"]);
|
|
26
|
-
/** The tool the backend injects to carry a typed structured result. */
|
|
27
|
-
const SUBMIT_TOOL = "submit_result";
|
|
28
|
-
/**
|
|
29
|
-
* Fold a POLLED settled run row into the stream-accumulated state — the shared
|
|
30
|
-
* adoption step for BOTH recovery paths (a dropped stream, and a stream that
|
|
31
|
-
* ended cleanly WITHOUT a `finish` frame — an edge can close a long SSE
|
|
32
|
-
* gracefully mid-run, which looks like completion but is a truncation).
|
|
33
|
-
*
|
|
34
|
-
* The row is the source of truth for status and the STRUCTURED output. A
|
|
35
|
-
* free-text run's row `output` is its prose string — that never enters
|
|
36
|
-
* `state.output` (a structured consumer reads `output.<field>`; a stray string
|
|
37
|
-
* would crash it). The free-text answer stays in the transcript text.
|
|
38
|
-
*/
|
|
39
|
-
export function adoptSettledRun(state, settled) {
|
|
40
|
-
if (settled.status === "completed") {
|
|
41
|
-
const structured = settled.output !== null && typeof settled.output === "object" ? settled.output : undefined;
|
|
42
|
-
return { ...state, status: "completed", output: structured ?? state.output };
|
|
43
|
-
}
|
|
44
|
-
// A PARKED row is a live, resumable state — never coerce it into an error.
|
|
45
|
-
// The invariant the hook layer relies on: `awaiting_input` always yields an
|
|
46
|
-
// answerable pending part. When the stream died before the ask part arrived,
|
|
47
|
-
// rebuild it from the row's derived `pending_interactive`.
|
|
48
|
-
if (settled.status === "awaiting_input") {
|
|
49
|
-
const next = { ...state, status: "awaiting_input" };
|
|
50
|
-
if (pendingInteractiveCall(next))
|
|
51
|
-
return next;
|
|
52
|
-
const p = settled.pending_interactive;
|
|
53
|
-
const alreadyPresent = p && state.parts.some((part) => part.type === "dynamic-tool" && part.toolCallId === p.tool_call_id);
|
|
54
|
-
if (p && INTERACTIVE_TOOLS.has(p.tool_name) && !alreadyPresent) {
|
|
55
|
-
return {
|
|
56
|
-
...next,
|
|
57
|
-
parts: [
|
|
58
|
-
...state.parts,
|
|
59
|
-
{ type: "dynamic-tool", toolName: p.tool_name, toolCallId: p.tool_call_id, state: "input-available", input: p.input },
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
return next;
|
|
64
|
-
}
|
|
65
|
-
return { ...state, status: "error", error: settled.error_message ?? "The run was stopped." };
|
|
66
|
-
}
|
|
67
|
-
/** The run's pending `ask_user_choice`, parsed for rendering (a `ClarifyWizard`
|
|
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
|
-
}
|
|
78
|
-
export function pendingInteractiveCall(state) {
|
|
79
|
-
if (state.status !== "awaiting_input")
|
|
80
|
-
return null;
|
|
81
|
-
for (let i = state.parts.length - 1; i >= 0; i--) {
|
|
82
|
-
const p = state.parts[i];
|
|
83
|
-
if (p.type !== "dynamic-tool" || !INTERACTIVE_TOOLS.has(p.toolName))
|
|
84
|
-
continue;
|
|
85
|
-
if (p.state !== "input-available" && p.state !== "input-streaming")
|
|
86
|
-
continue;
|
|
87
|
-
const questions = parseChoiceQuestions(p.input);
|
|
88
|
-
return questions.length > 0 ? { toolCallId: p.toolCallId, questions } : null;
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
function parseChoiceQuestions(input) {
|
|
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")
|
|
101
|
-
return [];
|
|
102
|
-
const record = decoded;
|
|
103
|
-
if (!Array.isArray(record.questions))
|
|
104
|
-
return [];
|
|
105
|
-
const questions = [];
|
|
106
|
-
for (const raw of record.questions) {
|
|
107
|
-
if (!raw || typeof raw !== "object")
|
|
108
|
-
continue;
|
|
109
|
-
const q = raw;
|
|
110
|
-
if (typeof q.question !== "string" || !Array.isArray(q.options))
|
|
111
|
-
continue;
|
|
112
|
-
const options = [];
|
|
113
|
-
for (const o of q.options) {
|
|
114
|
-
if (o && typeof o === "object" && typeof o.label === "string") {
|
|
115
|
-
const opt = o;
|
|
116
|
-
options.push({ label: opt.label, description: typeof opt.description === "string" ? opt.description : "" });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
questions.push({ question: q.question, options, allow_custom: q.allow_custom === true });
|
|
120
|
-
}
|
|
121
|
-
return questions;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Fold the user's picks into the tool's output shape — `answers` aligns to
|
|
125
|
-
* `questions` by index (the `ClarifyWizard` contract: `{ value, custom }` per
|
|
126
|
-
* question, value = the picked option's label or the free text). A missing or
|
|
127
|
-
* empty entry is a skipped question.
|
|
128
|
-
*/
|
|
129
|
-
export function buildChoiceOutput(questions, answers) {
|
|
130
|
-
const toLetter = (i) => String.fromCharCode("A".charCodeAt(0) + i);
|
|
131
|
-
const built = questions.map((question, index) => {
|
|
132
|
-
const questionNumber = index + 1;
|
|
133
|
-
const answer = answers[index];
|
|
134
|
-
const value = answer?.value.trim() ?? "";
|
|
135
|
-
if (!answer || value.length === 0) {
|
|
136
|
-
return { type: "skipped", question_index: index, question_number: questionNumber, question_text: question.question };
|
|
137
|
-
}
|
|
138
|
-
const optionIndex = answer.custom ? -1 : question.options.findIndex((o) => o.label === value);
|
|
139
|
-
if (optionIndex >= 0) {
|
|
140
|
-
const option = question.options[optionIndex];
|
|
141
|
-
return {
|
|
142
|
-
type: "option",
|
|
143
|
-
question_index: index,
|
|
144
|
-
question_number: questionNumber,
|
|
145
|
-
question_text: question.question,
|
|
146
|
-
option_index: optionIndex,
|
|
147
|
-
option_letter: toLetter(optionIndex),
|
|
148
|
-
option_label: option.label,
|
|
149
|
-
option_description: option.description,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
return { type: "custom", question_index: index, question_number: questionNumber, question_text: question.question, text: value };
|
|
153
|
-
});
|
|
154
|
-
const answeredCount = built.filter((a) => a.type !== "skipped").length;
|
|
155
|
-
return { answers: built, skipped_by_user: answeredCount === 0 };
|
|
156
|
-
}
|
|
157
|
-
/** The user answered: settle the pending part (the answer rides its `output` for
|
|
158
|
-
* the feed's on-demand reveal) and put the run back into `streaming` for the
|
|
159
|
-
* continuation leg's chunks. */
|
|
160
|
-
export function applyInteractiveAnswer(state, toolCallId, output) {
|
|
161
|
-
const parts = state.parts.map((p) => p.type === "dynamic-tool" && p.toolCallId === toolCallId
|
|
162
|
-
? { type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-available", input: p.input, output }
|
|
163
|
-
: p);
|
|
164
|
-
return { ...state, status: "streaming", parts };
|
|
165
|
-
}
|
|
166
|
-
export function initialAgentRunState() {
|
|
167
|
-
return { status: "streaming", parts: [] };
|
|
168
|
-
}
|
|
169
|
-
/** Grow the trailing prose part of the given kind, or open a new one (after a tool
|
|
170
|
-
* ran, or when the kind flips text↔reasoning) — so the transcript interleaves
|
|
171
|
-
* answer prose, thinking, and tools in the order they streamed. */
|
|
172
|
-
function appendProse(parts, kind, piece) {
|
|
173
|
-
const last = parts[parts.length - 1];
|
|
174
|
-
if (last && last.type === kind) {
|
|
175
|
-
return [...parts.slice(0, -1), { ...last, text: last.text + piece }];
|
|
176
|
-
}
|
|
177
|
-
const opened = kind === "text" ? { type: "text", text: piece, state: "streaming" } : { type: "reasoning", text: piece, state: "streaming" };
|
|
178
|
-
return [...parts, opened];
|
|
179
|
-
}
|
|
180
|
-
/** Apply an update to the `dynamic-tool` part with the given call id (by identity),
|
|
181
|
-
* leaving the rest untouched. A missing id (older frame) or no match is a no-op. */
|
|
182
|
-
function updateTool(parts, toolCallId, fn) {
|
|
183
|
-
if (!toolCallId)
|
|
184
|
-
return parts;
|
|
185
|
-
return parts.map((p) => (p.type === "dynamic-tool" && p.toolCallId === toolCallId ? fn(p) : p));
|
|
186
|
-
}
|
|
187
|
-
/** Fold one chunk into the run state. Returns a new state (immutable). */
|
|
188
|
-
export function reduceAgentChunk(state, chunk) {
|
|
189
|
-
switch (chunk.type) {
|
|
190
|
-
case "text-delta": {
|
|
191
|
-
const piece = chunk.delta ?? "";
|
|
192
|
-
return piece ? { ...state, parts: appendProse(state.parts, "text", piece) } : state;
|
|
193
|
-
}
|
|
194
|
-
case "reasoning-delta": {
|
|
195
|
-
// Thinking is its OWN part (not folded into the answer prose) so the UI can
|
|
196
|
-
// keep it collapsed / revealed-on-demand rather than inline in the answer.
|
|
197
|
-
const piece = chunk.delta ?? "";
|
|
198
|
-
return piece ? { ...state, parts: appendProse(state.parts, "reasoning", piece) } : state;
|
|
199
|
-
}
|
|
200
|
-
case "tool-input-start":
|
|
201
|
-
case "tool-input-available": {
|
|
202
|
-
const name = chunk.toolName ?? "tool";
|
|
203
|
-
if (name === SUBMIT_TOOL) {
|
|
204
|
-
// The agent emitted its structured result — routed to `output`, not a part.
|
|
205
|
-
return { ...state, output: chunk.input };
|
|
206
|
-
}
|
|
207
|
-
const id = chunk.toolCallId ?? `${name}-${state.parts.length}`;
|
|
208
|
-
// Upsert by call id: the tool part opens "input-available" (a running step in
|
|
209
|
-
// the feed) the moment the call fires and settles when its output arrives
|
|
210
|
-
// (below), so the feed shows a live tool, not an instantly-done one.
|
|
211
|
-
const existing = state.parts.some((p) => p.type === "dynamic-tool" && p.toolCallId === id);
|
|
212
|
-
if (existing) {
|
|
213
|
-
return { ...state, parts: updateTool(state.parts, id, (p) => ({ type: "dynamic-tool", toolName: p.toolName || name, toolCallId: id, state: "input-available", input: chunk.input ?? p.input })) };
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
...state,
|
|
217
|
-
parts: [...state.parts, { type: "dynamic-tool", toolName: name, toolCallId: id, state: "input-available", input: chunk.input }],
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
case "tool-input-delta":
|
|
221
|
-
// A tool's arguments streaming in. The ai-parts model carries no live size, so
|
|
222
|
-
// this is a no-op — the part already reads as running.
|
|
223
|
-
return state;
|
|
224
|
-
case "tool-output-available":
|
|
225
|
-
return { ...state, parts: updateTool(state.parts, chunk.toolCallId, (p) => ({ type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-available", input: p.input, output: chunk.output })) };
|
|
226
|
-
case "tool-output-error":
|
|
227
|
-
return { ...state, parts: updateTool(state.parts, chunk.toolCallId, (p) => ({ type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-error", input: p.input, errorText: chunk.errorText ?? "The tool failed." })) };
|
|
228
|
-
case "error":
|
|
229
|
-
return { ...state, status: "error", error: chunk.errorText ?? "The run failed." };
|
|
230
|
-
case "abort":
|
|
231
|
-
return { ...state, status: state.status === "streaming" ? "error" : state.status, error: state.error ?? "The run was stopped." };
|
|
232
|
-
case "finish": {
|
|
233
|
-
// Settle the status, and settle anything still streaming so nothing reads as
|
|
234
|
-
// in-progress on a completed run: a tool with no output frame → output-available
|
|
235
|
-
// (its output never came); a text/reasoning part still "streaming" → "done" (the
|
|
236
|
-
// stream's `text-end`/`reasoning-end` markers aren't tracked per-part). `output`
|
|
237
|
-
// is whatever `submit_result` set (else undefined) — NEVER the accumulated text:
|
|
238
|
-
// a structured-output consumer reads `output.<field>`, so a stray free-text
|
|
239
|
-
// string there would crash it. The free-text answer is always in a text part.
|
|
240
|
-
// ONE exemption: a pending INTERACTIVE call (`ask_user_choice`) is the run
|
|
241
|
-
// PARKING, not finishing — its part stays `input-available` and the state
|
|
242
|
-
// reads `awaiting_input` so the app renders the question and continues.
|
|
243
|
-
const awaiting = state.parts.some((p) => p.type === "dynamic-tool" && INTERACTIVE_TOOLS.has(p.toolName) && (p.state === "input-available" || p.state === "input-streaming"));
|
|
244
|
-
const parts = state.parts.map((p) => {
|
|
245
|
-
if (p.type === "dynamic-tool" &&
|
|
246
|
-
(p.state === "input-available" || p.state === "input-streaming") &&
|
|
247
|
-
!INTERACTIVE_TOOLS.has(p.toolName)) {
|
|
248
|
-
return { type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-available", input: p.input, output: undefined };
|
|
249
|
-
}
|
|
250
|
-
if ((p.type === "text" || p.type === "reasoning") && p.state === "streaming") {
|
|
251
|
-
return { ...p, state: "done" };
|
|
252
|
-
}
|
|
253
|
-
return p;
|
|
254
|
-
});
|
|
255
|
-
const status = state.status === "error" ? "error" : awaiting ? "awaiting_input" : "completed";
|
|
256
|
-
return { ...state, status, parts };
|
|
257
|
-
}
|
|
258
|
-
default:
|
|
259
|
-
return state;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Incremental SSE frame splitter. Feed it raw text as it arrives; it returns the
|
|
264
|
-
* complete chunks parsed so far and the leftover partial frame to carry forward.
|
|
265
|
-
* `[DONE]` is dropped (the `finish` chunk already settles the state).
|
|
266
|
-
*/
|
|
267
|
-
export function parseSseChunks(buffer) {
|
|
268
|
-
const chunks = [];
|
|
269
|
-
const frames = buffer.split("\n\n");
|
|
270
|
-
const rest = frames.pop() ?? ""; // last element is the incomplete frame
|
|
271
|
-
for (const frame of frames) {
|
|
272
|
-
for (const line of frame.split("\n")) {
|
|
273
|
-
const trimmed = line.startsWith("data:") ? line.slice(5).trim() : "";
|
|
274
|
-
if (!trimmed || trimmed === "[DONE]")
|
|
275
|
-
continue;
|
|
276
|
-
try {
|
|
277
|
-
chunks.push(JSON.parse(trimmed));
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
// a non-JSON data line — skip it, never throw on the stream
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return { chunks, rest };
|
|
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/ask_ai.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface AskAiArgs {
|
|
2
|
-
/** Prefills the chat composer — the user sees, edits, and sends it
|
|
3
|
-
* themselves. Nothing runs until they do. */
|
|
4
|
-
prompt?: string;
|
|
5
|
-
/** Files to attach to the chat (by id, from `readFiles(cell)`). The host
|
|
6
|
-
* resolves each id itself and opens the file preview beside the chat. */
|
|
7
|
-
file_ids?: string[];
|
|
8
|
-
/** Records the chat should know about (ids from query rows). The host
|
|
9
|
-
* resolves them to table context — the agent can then read and act on
|
|
10
|
-
* them under the signed-in user's authority. */
|
|
11
|
-
record_ids?: string[];
|
|
12
|
-
/** Free-text grounding for the agent ("Shipment SGN-2481, customer …").
|
|
13
|
-
* The host stamps the app's identity alongside it automatically. */
|
|
14
|
-
context?: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Hand off to the Lotics chat agent — opens the messenger on a FRESH chat
|
|
18
|
-
* seeded with the given files, records, and prompt.
|
|
19
|
-
*
|
|
20
|
-
* Use this for dialogue-shaped work: iterating on a document ("edit this
|
|
21
|
-
* invoice"), drafting from record context, open-ended questions. For
|
|
22
|
-
* structured judgment that commits back into the app's own data
|
|
23
|
-
* (extract/check/match/rank), use `useAgentRun` with a review surface
|
|
24
|
-
* instead — the outcome of `askAi` lands in chat, not in your workflows.
|
|
25
|
-
*
|
|
26
|
-
* The user stays in control: the prompt is only prefilled, attachments are
|
|
27
|
-
* visible in the composer, and nothing is sent until they press send.
|
|
28
|
-
*
|
|
29
|
-
* ```tsx
|
|
30
|
-
* import { askAi } from "@lotics/app-sdk";
|
|
31
|
-
* await askAi({
|
|
32
|
-
* file_ids: [file.id],
|
|
33
|
-
* record_ids: [row.id],
|
|
34
|
-
* prompt: "Update the header of this invoice to match our letterhead.",
|
|
35
|
-
* });
|
|
36
|
-
* ```
|
|
37
|
-
*
|
|
38
|
-
* Only available embedded in Lotics — rejects in standalone/dev mode.
|
|
39
|
-
*/
|
|
40
|
-
export declare function askAi(args: AskAiArgs): Promise<void>;
|