@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/docs/ai.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AI in apps
|
|
2
2
|
|
|
3
|
-
An app has two AI surfaces, and they answer different questions. **`useAgentRun(alias)`** runs an agent *declared on the app* — a streaming, tool-looping run whose result lands back **in the app** (a typed structured output, or free-text prose) for the app to review and commit through its own [workflows](./mutations.md). **`askAi(args)`** is a *handoff* — it opens the Lotics chat messenger seeded with files, records, and a prefilled prompt, and the outcome lands **in chat**, under the signed-in member's control.
|
|
3
|
+
An app has two AI surfaces, and they answer different questions. **`useAgentRun(alias)`** runs an agent *declared on the app* — a streaming, tool-looping run whose result lands back **in the app** (a typed structured output, or free-text prose) for the app to review and commit through its own [workflows](./mutations.md). **`askAi(args)`** is a *handoff* — it opens the Lotics chat messenger seeded with files, records, and a prefilled prompt, and the outcome lands **in chat**, under the signed-in member's control. **`useAiContext(slot, context)`** feeds the member's *ambient* chat agent a snapshot of what the current screen is showing. The authority model agent runs execute under is [security](./security.md). Exact signatures: `dist/hooks.d.ts` (`useAgentRun`, `useAgentRuns`, `useAiContext`), `dist/agent_stream.d.ts` (`AgentRunState`, `AgentUIPart`), `dist/ask_ai.d.ts` (`AskAiArgs`).
|
|
4
4
|
|
|
5
5
|
## Choosing the surface — the fields-vs-file razor
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ Don't run a structured extraction through `askAi` (the result is stranded in a c
|
|
|
13
13
|
|
|
14
14
|
## Declared agents — what `useAgentRun` runs
|
|
15
15
|
|
|
16
|
-
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `remove_app_agent`) — the
|
|
16
|
+
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `remove_app_agent`) — the one way an agent is bound or changed, each write a new version of the app; a deploy never touches it, and `get_app_agent` reads one back. Invoking an alias that isn't bound fails with a "no agent alias" error naming `set_app_agent`, and a deploy warns about any alias the code calls that is not bound.
|
|
17
17
|
|
|
18
18
|
A declaration carries:
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ A declaration carries:
|
|
|
26
26
|
| `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
|
|
27
27
|
| `model_tier` | Optional model tier — `haiku` \| `sonnet` \| `opus`. Omit (preferred) to follow the platform default tier, resolved at run time. A tier names capability, not a version, so the agent tracks model generations with no rewrite. Pin only a deliberate, tested choice |
|
|
28
28
|
| `effort_level` | Optional reasoning depth for adaptive-thinking tiers. Requires an explicit `model_tier` pin — effort is tuned per tier |
|
|
29
|
-
| `prefix_cache_ttl` | Optional prompt-cache window for the agent's stable prefix (tools + system). **Omit it** — the default
|
|
29
|
+
| `prefix_cache_ttl` | Optional prompt-cache window for the agent's stable prefix (tools + system). **Omit it** — the default `"5m"` is right for essentially every agent. `"1h"` writes at 2x the input rate (against 1.25x) to buy only the five-minute-to-one-hour band; set it only with measured cadence showing runs land there, such as a scheduled sweep |
|
|
30
30
|
| `inputs` | Optional typed input schema for one run — the same vocabulary as workflow inputs (`text`, `number`, `file`, `member`, `record_link`, `select`, …). The server validates every run payload against it. **Every field defaults to `required: true`**, exactly as `outputs` does — the two extend the same base — so an input the caller may legitimately omit needs `"required": false`, or the run is rejected before the agent sees it |
|
|
31
31
|
| `outputs` | Optional typed output schema — the declaration vocabulary, including the two ways a `select` names its option set, is [mutations](./mutations.md#structured-results-return-data). Declared → **structured agent** (the run must emit a matching result); omitted → **free-text agent** (the answer is the final prose). **Every field defaults to `required: true`** (the same base schema as `inputs` above) — mark `"required": false` on anything the source may legitimately not carry. It matters most for `number`, which has no blank: text can answer `""`, but a required number leaves only a wrong value or a rejected submission |
|
|
32
32
|
|
|
@@ -34,11 +34,11 @@ A declaration carries:
|
|
|
34
34
|
|
|
35
35
|
**Input validation and tenant bounds.** Run inputs get the same server-side enforcement as workflow inputs: `record_link` ids must live in the declared table, `member` ids in the declared group, `file` ids in the app's workspace (see [the caller boundary](./security.md)). The run executes under the **app owner's** authority in the app's own workspace.
|
|
36
36
|
|
|
37
|
-
**Record data is reached only through declared aliases.** Every tool that reads or writes record VALUES by table id is rejected in `tool_names` — there is no `query_records`, `get_record`, `create_records`, or `update_records` for an agent. The agent reads with `run_app_query(alias, params)` and writes with `run_app_workflow(alias, inputs)`, both refused for any alias outside `query_aliases` / `workflow_aliases`.
|
|
37
|
+
**Record data is reached only through declared aliases.** Every tool that reads or writes record VALUES by table id is rejected in `tool_names` — there is no `query_records`, `get_record`, `create_records`, or `update_records` for an agent. The agent reads with `run_app_query(alias, params)` and writes with `run_app_workflow(alias, inputs)`, both refused for any alias outside `query_aliases` / `workflow_aliases`. A query template fixes its own tables, joins, filters AND projection, so it bounds which **rows and columns** the agent sees; writes travel the app's declared mutation path, so workflow validation and table hooks apply. `is_current_member` inside a declared query resolves to the **invoking** member, so a self-scoped query scopes to the person using the agent, exactly as it does in the app's UI.
|
|
38
38
|
|
|
39
39
|
So an agent that reads or writes records needs a named query or workflow for each thing it touches — the same declaration the app's own UI uses. Declare only what that agent needs; a second agent in the same app can declare a narrower set.
|
|
40
40
|
|
|
41
|
-
**Typing.**
|
|
41
|
+
**Typing.** `.lotics/app_agents.d.ts`, generated from the app's live bindings whenever a sandbox session opens on the app, augments the SDK's `AppAgents` (alias → input shape) and `AppAgentResults` (alias → declared output shape) interfaces — `useAgentRun("recognize")` then types both `run(input)` and `output`. An alias it does not carry — every alias, in a project on your own machine — falls back to `Record<string, unknown>` input / `unknown` output.
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
@@ -57,8 +57,8 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
57
57
|
|
|
58
58
|
| Member | Type | What it is |
|
|
59
59
|
|---|---|---|
|
|
60
|
-
| `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
|
|
61
|
-
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this
|
|
60
|
+
| `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. A run failure is DATA here, never a rejection. **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. Pass `replace: true` to deliberately abort-and-restart; the replaced run still executes and bills server-side |
|
|
61
|
+
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this; the platform stamps `app_agent_runs.cancel_requested_at` |
|
|
62
62
|
| `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) |
|
|
63
63
|
| `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) |
|
|
64
64
|
| `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}`) |
|
|
@@ -95,11 +95,11 @@ The terminal `submit_result` call is captured into `output`, **not** rendered as
|
|
|
95
95
|
|
|
96
96
|
*Before it starts*: the request is refused (most often a 402 when the workspace is out of AI credits — "Credit quota exceeded. Upgrade your plan for continued AI access." — or a 403/404). You get that response's `message` alone; the rest of the body (`error`, `plan_id`, `credits_used`, …) never reaches the string, and a non-JSON body or any 5xx is replaced by a status-derived message.
|
|
97
97
|
|
|
98
|
-
*After it starts*: the run dies mid-flight, and the message is written **in the triggering member's language** (the workspace default
|
|
98
|
+
*After it starts*: the run dies mid-flight, and the message is written **in the triggering member's language** (the workspace default for an anonymous visitor). An error the platform wrote surfaces as-is, so running out of credits **during** a run still says to upgrade. A provider or tool failure becomes one of two sentences: the request itself was rejected (an attachment that is corrupt or too large — retrying unchanged cannot help), or it broke transiently and is worth another run.
|
|
99
99
|
|
|
100
|
-
Two
|
|
100
|
+
Two messages stay English: one written at its throw site rather than fixed on its error class (the chat agent reads those to correct its own tool calls), and the *before it starts* response's own `message`. An app that must be wholly in one language renders its own copy on failure.
|
|
101
101
|
|
|
102
|
-
So `error={run.error}` is safe to render verbatim
|
|
102
|
+
So `error={run.error}` is safe to render verbatim, and never machine-readable: **do not parse it** — the wording is copy and will change. If your app needs the retry-vs-futile distinction, file a `lotics report` asking for it.
|
|
103
103
|
|
|
104
104
|
### The agent asks back — `pendingChoice` / `answerChoice`
|
|
105
105
|
|
|
@@ -125,14 +125,12 @@ const run = useAgentRun("importer");
|
|
|
125
125
|
) : null}
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
The ask renders in the feed as a settled tool row once answered (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
answerable `pendingChoice` (the one unrecoverable corner surfaces a retryable `error`, never
|
|
135
|
-
a silent dead end). If the server refuses an answer (an invalid submission, a raced cancel
|
|
128
|
+
The ask renders in the feed as a settled tool row once answered. `run()` resolves
|
|
129
|
+
`{kind:"parked"}` when the run parks — the wizard renders off `pendingChoice` — and the
|
|
130
|
+
continuation's landing (from `answerChoice`) carries the final output, or `parked` again for a
|
|
131
|
+
follow-up ask. A dropped stream can't lose the question: the recovery poll rebuilds it from the
|
|
132
|
+
persisted run, so `awaiting_input` always yields an answerable `pendingChoice` (or a retryable
|
|
133
|
+
`error`). If the server refuses an answer (an invalid submission, a raced cancel
|
|
136
134
|
or expiry, a connection failure), `answerChoice` REJECTS and the pending question is
|
|
137
135
|
restored — surface the error and let the user submit again; nothing is half-committed.
|
|
138
136
|
`cancel()` on a parked run settles it `aborted` immediately; an unanswered park expires
|
|
@@ -153,19 +151,17 @@ The server validates the agent's submitted result strictly (unknown keys rejecte
|
|
|
153
151
|
|
|
154
152
|
Treat `output` as a trusted *shape* carrying untrusted *values*: render it into a review surface and let the user confirm before a workflow commits it. Never write model output straight to records without a review step.
|
|
155
153
|
|
|
156
|
-
### Errors —
|
|
154
|
+
### Errors — one landing, and one quiet mode
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
2. **An in-run failure resolves.** A model or tool error mid-run arrives as a stream frame: `run()` resolves `undefined`, `status` becomes `"error"`, and `error` carries the message. Checking only try/catch misses this path; checking only `status` misses the first.
|
|
156
|
+
A run that cannot start (AI-credit quota exhausted, the per-member concurrency cap, an undeclared alias, input validation, a network error) and one that fails mid-run both **resolve** `{kind:"failed", error}`, with `status` `"error"`. Only `answerChoice` rejects, when the server refuses an answer.
|
|
160
157
|
|
|
161
158
|
```tsx
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch (err) { /* quota / network / validation — surface err.message */ }
|
|
159
|
+
const landing = await recognize.run(input, { sessionId });
|
|
160
|
+
if (landing.kind === "failed") { /* surface landing.error */ }
|
|
161
|
+
else if (landing.kind === "settled" && landing.output === undefined) { /* no result */ }
|
|
166
162
|
```
|
|
167
163
|
|
|
168
|
-
|
|
164
|
+
A capped run's live message is a generic "The run was stopped." (the persisted run carries "Run exceeded the 20-minute limit."). Two modes are **quiet on the live stream**: a structured agent that finished without ever submitting a result, and a free-text agent that produced no text, land `settled` with no result — only the *persisted* run settles as an error. That is why the example checks `output === undefined`.
|
|
169
165
|
|
|
170
166
|
### `cancel` vs `abort`
|
|
171
167
|
|
|
@@ -175,7 +171,7 @@ Server-side failure modes are not all equally loud. A mid-run model/tool excepti
|
|
|
175
171
|
| Local effect | Clears state to `idle` | Clears state to `idle` |
|
|
176
172
|
| Use for | A user-facing **Stop** button | Unmount / navigating away (the hook already calls it on unmount) |
|
|
177
173
|
|
|
178
|
-
Both settle the in-flight `run()` promise
|
|
174
|
+
Both settle the in-flight `run()` promise with `{kind:"aborted"}` — a stop is not a failure. A dropped connection does **not** cancel a run; `cancel()` is the only stop path.
|
|
179
175
|
|
|
180
176
|
**Warning:** server-side cancellation is honored *between agent steps* — a cancel issued mid-way through one long generation takes effect at the next step boundary, not instantly. The UI should reflect "stopping" optimistically (the local state clears at once).
|
|
181
177
|
|
|
@@ -188,11 +184,11 @@ The run's lifetime is decoupled from the stream: the server drives it to complet
|
|
|
188
184
|
|
|
189
185
|
Either way the run itself is unaffected — it keeps executing server-side and settles in `app_agent_runs`, which is where its outcome is read.
|
|
190
186
|
|
|
191
|
-
**Anonymous runs poll with a capability token, handled for you.**
|
|
187
|
+
**Anonymous runs poll with a capability token, handled for you.** A public-app visitor has no member identity to authorize a poll by, so the run response carries a per-run token (`x-app-agent-run-token`) the SDK replays on the poll, on `cancel`, and on `answerChoice` — never surfaced to app code, never issued for a member run, and bound to ONE run.
|
|
192
188
|
|
|
193
|
-
The poll is bounded at 22 minutes — deliberately PAST the server's 20-minute hard run cap, so a live run always settles before the client gives up. A row still `running` at the deadline means the run's process died mid-flight (e.g. a crash that skipped the server's shutdown drain); the poll surfaces an error and the server's reaper repairs the row.
|
|
189
|
+
The poll is bounded at 22 minutes — deliberately PAST the server's 20-minute hard run cap, so a live run always settles before the client gives up. A row still `running` at the deadline means the run's process died mid-flight (e.g. a crash that skipped the server's shutdown drain); the poll surfaces an error and the server's reaper repairs the row. When no run id was ever received (the run never started), the landing is `failed` with no polling.
|
|
194
190
|
|
|
195
|
-
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`; the full settled answer is persisted server-side
|
|
191
|
+
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`; the full settled answer is persisted server-side and not client-readable (`useAgentRuns` reaches it on no transport — see the limitation below), so treat the streamed prefix as terminal.
|
|
196
192
|
|
|
197
193
|
### Sessions
|
|
198
194
|
|
|
@@ -212,35 +208,30 @@ Sessions are scoped to the authenticated member who ran them: two members using
|
|
|
212
208
|
|---|---|---|
|
|
213
209
|
| **Perceived natively** | `image/*`, `application/pdf` | A vision / document part — the agent literally sees it |
|
|
214
210
|
| **Materialized** | Word (`.docx`/`.doc`), Excel (`.xlsx`/`.xls`), CSV, and text (`.txt`, `.md`, `.json`, `.eml`, `.html`, `.xml`, `.yaml`) | Read server-side by the same engines `view_files` uses and inlined into the run's message, truncated at 40,000 characters with the agent told when that happened |
|
|
211
|
+
| **Unreadable** | Archives, audio, video | The run fails immediately, naming the file |
|
|
215
212
|
|
|
216
|
-
**Pictures inside a Word document are delivered as images**, so a scanned page
|
|
217
|
-
| **Unreadable** | Archives, audio, video | The run fails immediately, naming the file — a fact about the upload, not a gap in your configuration |
|
|
213
|
+
**Pictures inside a Word document are delivered as images**, so a scanned page photographed into a `.docx` is READ, not merely described; a picture skipped or unresolvable is counted and stated in the text.
|
|
218
214
|
|
|
219
215
|
Every file in a `multi` input is materialized — nothing is collapsed to the first. Get the ids from [`useFileUpload` / `useAttachments`](./files.md).
|
|
220
216
|
|
|
221
|
-
**So do not dispatch on the file type
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
only the content carries. Prefer separate output slots (`don_hang`, `hoa_don`, plus a named
|
|
227
|
-
`bo_qua` for what fits neither) over a `loai` discriminator beside a payload — which slot a
|
|
228
|
-
document lands in IS the classification, so there is no second field free to disagree with it, and
|
|
229
|
-
nothing disappears quietly.
|
|
217
|
+
**So do not dispatch on the file type** — the same form arrives as `.xlsx`, as a PDF and as a
|
|
218
|
+
photo of a printout, and routing on the extension runs the wrong extractor silently. Let one agent
|
|
219
|
+
read the drop and say what each document is. Prefer separate output slots (one per document kind,
|
|
220
|
+
plus a named slot for what fits neither) over a kind discriminator beside a payload: which slot a
|
|
221
|
+
document lands in IS the classification.
|
|
230
222
|
|
|
231
|
-
**Why no tool:**
|
|
223
|
+
**Why no tool:** fetching what the server already holds costs a model round-trip, and an agent
|
|
224
|
+
that reads its own input never gains the ability to read *other* files in the workspace.
|
|
232
225
|
|
|
233
226
|
**When to add a tool anyway.** Only to reach *past* what was inlined. A spreadsheet is materialized at 100 rows × 50 columns per sheet and a Word file at 1,000 elements; overflow is reported, never silent. If the agent must read row 4,000 of a large sheet, add `excel_get_range` / `excel_find_cells` (or `view_files` for an ad-hoc second look at a different file).
|
|
234
227
|
|
|
235
228
|
### Auth, quota, and bounds
|
|
236
229
|
|
|
237
|
-
- **Member-authenticated, embedded-only.** Agent runs require a signed-in member — an anonymous visitor to a public/standalone app is rejected with an explicit error ("App agent runs require an authenticated member…"). Runs work embedded in the product
|
|
230
|
+
- **Member-authenticated, embedded-only.** Agent runs require a signed-in member — an anonymous visitor to a public/standalone app is rejected with an explicit error ("App agent runs require an authenticated member…"). Runs work embedded in the product.
|
|
238
231
|
- **Owner-billed.** Token usage is recorded against the app's organization's AI credits. The quota is enforced before the first model call (a `run()` rejection) and re-checked between steps (a mid-run exhaustion settles the run as an error).
|
|
239
232
|
- **Hard duration cap: 20 minutes per run.** A capped run settles as an error — but with whatever partial transcript and captured output it produced, recoverable from the persisted run.
|
|
240
233
|
- **Concurrency cap: 5 in-flight runs per member.** Exceeding it rejects with "Too many agent runs in progress".
|
|
241
|
-
- **One tool result may not exceed 512 KiB
|
|
242
|
-
|
|
243
|
-
**Limitation (dev harness):** `lotics app dev` streams runs but does not forward the run id, so in the dev loop `cancel()` degrades to a local `abort()` (the run keeps executing server-side) and connection-drop recovery is unavailable. Both work fully in the embedded product.
|
|
234
|
+
- **One tool result may not exceed 512 KiB**, because it is replayed on every later step. A tool that overruns fails by name ("Tool \"x\" returned N bytes…"); narrow the call — read a range, not a whole sheet — or use a tool that returns a reference instead of the bytes.
|
|
244
235
|
|
|
245
236
|
---
|
|
246
237
|
|
|
@@ -248,7 +239,7 @@ nothing disappears quietly.
|
|
|
248
239
|
|
|
249
240
|
Reads a session's persisted run history, oldest-first: `{ runs, loading, error, refetch }`, where each `AgentRunRecord` carries `id`, `agent_alias`, `session_id`, `status`, `input`, `output`, `error_message`, `started_at`, `completed_at`. `opts` takes `{ enabled?, revalidateOnFocus? }`. A persisted record's `output` is the validated structured object for a structured agent and the **final answer string** for a free-text agent.
|
|
250
241
|
|
|
251
|
-
**Limitation:** the embedded product host
|
|
242
|
+
**Limitation:** the embedded product host implements no history op — `useAgentRuns` errors with "Unknown RPC op: agentRuns" in an embedded app, and a standalone caller is anonymous, so it is refused. The history *exists* server-side (it is what feeds session context), but this hook reads it from no transport. Do not build a session-log UI on it; keep the visible log in app state from the live `useAgentRun` results instead.
|
|
252
243
|
|
|
253
244
|
---
|
|
254
245
|
|
|
@@ -277,7 +268,7 @@ At least one of the four is required — an empty call rejects. The bridge carri
|
|
|
277
268
|
|
|
278
269
|
**Name the task in `prompt`.** Word/Excel file bytes are not inlined into the model's context — a clear brief ("update the header of this invoice…") makes the agent read the attached file first instead of asking what to do.
|
|
279
270
|
|
|
280
|
-
**Embedded-only.** `askAi` rejects in standalone mode ("askAi is only available when the app runs inside Lotics")
|
|
271
|
+
**Embedded-only.** `askAi` rejects in standalone mode ("askAi is only available when the app runs inside Lotics"). It also rejects on the rare embedded screen where the chat surface is unavailable ("chat is unavailable on this screen") — treat the returned promise's rejection as a real path and surface it.
|
|
281
272
|
|
|
282
273
|
`askAi` returns once the handoff is delivered; it never reports what the user or agent did afterwards. If the app needs the outcome, that's the razor telling you to use `useAgentRun`.
|
|
283
274
|
|
|
@@ -285,7 +276,7 @@ At least one of the four is required — an empty call rejects. The bridge carri
|
|
|
285
276
|
|
|
286
277
|
## `useAiContext(slot, context)` — tell the ambient chat what the member is looking at
|
|
287
278
|
|
|
288
|
-
A member using an app inside Lotics has an **ambient chat agent** riding alongside
|
|
279
|
+
A member using an app inside Lotics has an **ambient chat agent** riding alongside it. `useAiContext` publishes a snapshot of the **current screen's view state** into that chat, so "why is this one overdue?" resolves against which list is filtered to what, which record is open, and what's typed into a form.
|
|
289
280
|
|
|
290
281
|
```tsx
|
|
291
282
|
import { useAiContext } from "@lotics/app-sdk";
|
|
@@ -326,22 +317,15 @@ Declarative and lifecycle-bound: mounting or changing `context` pushes it; unmou
|
|
|
326
317
|
|
|
327
318
|
### A slot that never publishes looks exactly like a slot with nothing to say
|
|
328
319
|
|
|
329
|
-
|
|
330
|
-
`
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
The usual cause is a guard on the query state. `useQuery`'s `error` is `string | null` — it is
|
|
335
|
-
**never `undefined`** — so `error !== undefined` is always true and holds the guard shut on every
|
|
336
|
-
render. Write `if (loading || error) return;`, which is correct whichever of the two the field
|
|
337
|
-
turns out to be, and reads as the question you meant.
|
|
320
|
+
This hook has **no visible output**: a `useAiContext` behind a guard that never opens publishes
|
|
321
|
+
`null` forever while its screen renders perfectly. The usual cause is a guard on the query state —
|
|
322
|
+
`useQuery`'s `error` is `string | null`, so `error !== undefined` holds it shut; write
|
|
323
|
+
`if (loading || error) return;`.
|
|
338
324
|
|
|
339
|
-
**Verify by observing what is published, never by reading the code.**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
when the surface closes or the tab changes, and it does not publish while the first read is still
|
|
344
|
-
in flight — a count of zero taken from a pending query is an answer nothing measured.
|
|
325
|
+
**Verify by observing what is published, never by reading the code.** Open the deployed app in
|
|
326
|
+
the product with the chat beside it, and check that the context the chat names carries REAL values
|
|
327
|
+
matching the screen, clears to `null` when the surface closes, and does not publish while the
|
|
328
|
+
first read is still in flight.
|
|
345
329
|
|
|
346
330
|
### The caps (host-enforced — exceeding them truncates or drops, never errors)
|
|
347
331
|
|
|
@@ -360,30 +344,21 @@ Keep `description` tight and human-readable — it is the line the agent reads.
|
|
|
360
344
|
This hook is a **one-way push of data the app already showed this member**. (Chat can also PULL, through the app's declared aliases — a separate channel with its own `app:use` gate, below.) Two consequences hold the push to what was rendered:
|
|
361
345
|
|
|
362
346
|
- **`records` are raw `{ table_id, record_id }` refs, passed UNRESOLVED.** The app does not resolve them here; the member's own chat agent may read or act on them only where **that member's IAM already allows**. A ref to a record the member can't see stays inert.
|
|
363
|
-
- **`description` and `data` enter the agent's prompt as clearly-labeled DATA, never as instructions.** Text an app renders can't hijack the agent — the host wraps it as app-supplied view state
|
|
347
|
+
- **`description` and `data` enter the agent's prompt as clearly-labeled DATA, never as instructions.** Text an app renders can't hijack the agent — the host wraps it as app-supplied view state.
|
|
364
348
|
|
|
365
349
|
### The chat can also USE the app, not just be told about it
|
|
366
350
|
|
|
367
|
-
`useAiContext` is a PUSH — a snapshot of what one screen rendered. It cannot
|
|
368
|
-
answer a question about a record that is not on screen, and it is capped, so it
|
|
369
|
-
is not the whole story.
|
|
370
|
-
|
|
371
351
|
While a member has an app open, their chat agent can also **call that app's own
|
|
372
352
|
declared aliases** — named queries via `run_app_query`, workflows via
|
|
373
353
|
`run_app_workflow`. This adds no exposure: every declared alias is already
|
|
374
|
-
callable by that member
|
|
375
|
-
`
|
|
376
|
-
|
|
377
|
-
gated per turn on `app:use` for the app in the member's workspace, and runs
|
|
378
|
-
under the app's OWNER authority with `is_current_member` bound to the MEMBER
|
|
379
|
-
(exactly as the app's own UI does).
|
|
354
|
+
callable by that member ([the devtools test](./security.md#the-devtools-test)).
|
|
355
|
+
It is gated per turn on `app:use`, and runs under the app's OWNER authority with
|
|
356
|
+
`is_current_member` bound to the MEMBER, exactly as the app's own UI does.
|
|
380
357
|
|
|
381
358
|
The agent is handed a **catalog** of those aliases — each one's `description`
|
|
382
|
-
(a query's own, [
|
|
359
|
+
(a query's own, [set on its binding](./queries.md); a workflow's, off its
|
|
383
360
|
workflow row) and its **complete input contract** — so it picks the right one
|
|
384
|
-
and calls it correctly
|
|
385
|
-
alias you declare appears, however large the manifest, and every input appears
|
|
386
|
-
with it:
|
|
361
|
+
and calls it correctly. Every alias you declare appears, with every input:
|
|
387
362
|
|
|
388
363
|
```
|
|
389
364
|
workflows (run_app_workflow):
|
|
@@ -394,76 +369,42 @@ workflows (run_app_workflow):
|
|
|
394
369
|
|
|
395
370
|
`?` marks an input the caller may omit, `[]` one that takes a list, a
|
|
396
371
|
`select` spells out every legal value as `label=value` — **the agent sends the
|
|
397
|
-
value** — and a `record_link` names the table an id must come from.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
inline set that matches no table field — so an agent reading option values off
|
|
401
|
-
the underlying table would send ones your write rejects.
|
|
372
|
+
value** — and a `record_link` names the table an id must come from. Both tools
|
|
373
|
+
take an opaque inputs object, so this listing is the only place any of it is
|
|
374
|
+
legible.
|
|
402
375
|
|
|
403
376
|
The app's own `description` heads that listing as `about:`, on both surfaces. It
|
|
404
377
|
is the only channel that reaches the agent on **every** turn without the member
|
|
405
378
|
saying anything, so a standing process belongs there: the recurring job the app
|
|
406
379
|
exists for, which alias does it, and what the agent must leave alone.
|
|
407
380
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
input's `description` is the only place a DEFAULT is visible** — the catalog
|
|
415
|
-
renders a name, a type, a `?` and your prose, never the body. An optional input
|
|
416
|
-
whose default goes unstated is one the agent asks the member about instead of
|
|
417
|
-
omitting; write what omitting it means ("left blank = today").
|
|
381
|
+
**Your descriptions are what it reads**, here and inside your own agents' runs —
|
|
382
|
+
as a capability listing, never as instructions. **A `select` input's options are
|
|
383
|
+
part of that contract**, so prefer the `field: "fld_…"` form when the values come
|
|
384
|
+
from a real field. And **an input's `description` is the only place a DEFAULT is
|
|
385
|
+
visible** — write what omitting it means ("left blank = today"), or the agent
|
|
386
|
+
asks the member instead of omitting.
|
|
418
387
|
|
|
419
388
|
**Put anything irreversible behind a
|
|
420
389
|
[`wait_for_approval`](./workflows.md#wait_for_approval--the-one-wait-worth-binding):**
|
|
421
390
|
an email, a shipment, a published post, a write over a value with no version to
|
|
422
|
-
restore. The body's own gate is the only human checkpoint on this path
|
|
423
|
-
names the act in the language of the business and reaches the approvers the
|
|
424
|
-
workflow declares.
|
|
391
|
+
restore. The body's own gate is the only human checkpoint on this path.
|
|
425
392
|
|
|
426
393
|
**Shape a mutating alias around ONE call per job.** An alias that takes a single
|
|
427
|
-
record turns a fifteen-record job into fifteen executions and
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
screen's button, where the member picked the record by opening it; here nobody
|
|
431
|
-
picked anything.
|
|
394
|
+
record turns a fifteen-record job into fifteen executions, and a partial failure
|
|
395
|
+
leaves it half done with nothing that says so. Take an array input — one call,
|
|
396
|
+
one outcome.
|
|
432
397
|
|
|
433
398
|
**Report what it did NOT do.** The member's account of the write is assembled
|
|
434
|
-
from what the workflow returned
|
|
435
|
-
eleven and returns only success tells them all eleven were done. Return the
|
|
436
|
-
misses in `data`.
|
|
399
|
+
from what the workflow returned, so return the misses in `data`.
|
|
437
400
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
from the app's own buttons.
|
|
401
|
+
A workflow that should NOT be agent-reachable is not bound to the app at
|
|
402
|
+
all, since the member can already fire it from the app's own buttons.
|
|
441
403
|
|
|
442
|
-
**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
never be asked: the agent can fetch them when needed, fresher than a snapshot,
|
|
446
|
-
and a payload pushed on every message is carried by every later turn (see the
|
|
447
|
-
caps above).
|
|
404
|
+
**So push the one line that makes "this one" resolvable** — who is on screen and
|
|
405
|
+
where they stand — and no bulk rows in `data`: the agent fetches them fresher
|
|
406
|
+
when needed, and a pushed payload rides every later turn.
|
|
448
407
|
|
|
449
408
|
### Query freshness — the mutation companion
|
|
450
409
|
|
|
451
|
-
When the ambient chat agent's turn ends
|
|
452
|
-
|
|
453
|
-
**No-ops** with no embedding host (standalone on the app's own origin — there's no chat surface to inform) and in mock mode.
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
## Version floors
|
|
458
|
-
|
|
459
|
-
The floors below are when each capability shipped in `@lotics/app-sdk`; an app pinned older silently lacks them.
|
|
460
|
-
|
|
461
|
-
| Capability | Minimum version |
|
|
462
|
-
|---|---|
|
|
463
|
-
| `useAgentRun` (with `abort`) + `useAgentRuns` | `@lotics/app-sdk` 0.34 |
|
|
464
|
-
| `cancel()` (server-side stop) + connection-drop recovery | `@lotics/app-sdk` 0.37 |
|
|
465
|
-
| `parts` transcript (ai-sdk `UIMessagePart[]` — reasoning + per-tool `input`/`output`) | `@lotics/app-sdk` 0.54; renders with `@lotics/ui` ≥ 14.0 (`AgentRun` `parts` prop) |
|
|
466
|
-
| `askAi` | `@lotics/app-sdk` 0.45 |
|
|
467
|
-
| `useAiContext` (ambient-chat view state + auto query refetch on chat mutation) | `@lotics/app-sdk` 0.52 |
|
|
468
|
-
| The agent asks back (`pendingChoice`/`answerChoice`, `awaiting_input`) | `@lotics/app-sdk` 0.55 |
|
|
469
|
-
| A leg resolves an `AgentRunLanding` (`settled`/`parked`/`failed`/`aborted`) instead of `TOutput \| undefined` — the type is exported from the package root | `@lotics/app-sdk` 0.62.1 |
|
|
410
|
+
When the ambient chat agent's turn ends having mutated records, the host pushes every mounted query hook to re-read, so the screen reflects the change with no code and no reload. It refreshes only mounted hooks and never reaches into app data. **No-ops** standalone (there is no chat surface) and in mock mode.
|