@lotics/app-sdk 0.60.0 → 0.60.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/docs/ai.md +45 -3
- package/docs/queries.md +15 -0
- package/package.json +1 -1
package/docs/ai.md
CHANGED
|
@@ -306,18 +306,60 @@ Declarative and lifecycle-bound: mounting or changing `context` pushes it; unmou
|
|
|
306
306
|
| `slot` | ≤ 50 chars |
|
|
307
307
|
| `description` | ≤ 1000 chars (truncated with `…`) |
|
|
308
308
|
| `records` | ≤ 20 refs |
|
|
309
|
-
| `data` | must JSON-serialize to ≤ 2000 chars, else
|
|
309
|
+
| `data` | must JSON-serialize to ≤ 2000 chars, else it is **replaced by an `omitted` marker** the agent reads (the description is kept) |
|
|
310
310
|
| slots per app | ≤ 8 (a 9th evicts the least-recently-updated) |
|
|
311
311
|
|
|
312
|
-
Keep `description` tight and human-readable — it is the line the agent reads. Put anything structured (filter/sort/form values) in `data`, and remember it is
|
|
312
|
+
Keep `description` tight and human-readable — it is the line the agent reads. Put anything structured (filter/sort/form values) in `data`, and remember it is replaced whole past its size cap: the agent is told the state existed but not what it was, so don't hide load-bearing facts there that aren't also in `description`. It is replaced rather than silently dropped so the agent cannot mistake "too big to send" for "there was nothing".
|
|
313
313
|
|
|
314
314
|
### Security — push-only, a snapshot of what was already rendered
|
|
315
315
|
|
|
316
|
-
This is a **one-way push of data the app already showed this member
|
|
316
|
+
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:
|
|
317
317
|
|
|
318
318
|
- **`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.
|
|
319
319
|
- **`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 (see [security](./security.md) for the labeled-data convention).
|
|
320
320
|
|
|
321
|
+
### The chat can also USE the app, not just be told about it
|
|
322
|
+
|
|
323
|
+
`useAiContext` is a PUSH — a snapshot of what one screen rendered. It cannot
|
|
324
|
+
answer a question about a record that is not on screen, and it is capped, so it
|
|
325
|
+
was never the whole story.
|
|
326
|
+
|
|
327
|
+
While a member has an app open, their chat agent can also **call that app's own
|
|
328
|
+
declared aliases** — named queries via `run_app_query`, workflows via
|
|
329
|
+
`run_app_workflow`. This adds no exposure: every declared alias is already
|
|
330
|
+
callable by that member against `POST /v1/apps/{id}/query` and
|
|
331
|
+
`POST /v1/apps/{id}/workflows/{alias}/execute`, so the manifest already IS
|
|
332
|
+
their reachable surface — the agent simply reaches it without devtools. It is
|
|
333
|
+
gated per turn on `app:use` for the app in the member's workspace, and runs
|
|
334
|
+
under the app's OWNER authority with `is_current_member` bound to the MEMBER
|
|
335
|
+
(exactly as the app's own UI does).
|
|
336
|
+
|
|
337
|
+
**Writes carry a second gate that reads do not: `run_app_workflow` asks the
|
|
338
|
+
member to approve every call from chat.** A wrong read is fixed by asking
|
|
339
|
+
again; a wrong write is not. The alias list bounds what an agent *could* reach;
|
|
340
|
+
the approval is what stops the wrong one. Approval applies to the chat surface
|
|
341
|
+
only — an app agent's run is already an explicit act with no human in the loop
|
|
342
|
+
to answer.
|
|
343
|
+
|
|
344
|
+
The prompt names the action and lists the inputs the agent chose, so **your
|
|
345
|
+
input names are what the member reads before authorizing the write**. Name them
|
|
346
|
+
for that reader (`ma_khach_hang`, `so_tien`), and keep the decisive ones
|
|
347
|
+
top-level — a value nested inside an opaque payload shows as JSON, and long
|
|
348
|
+
values are truncated. Context ids the member never chose (`app_id`,
|
|
349
|
+
`workspace_id`, `organization_id`) are filtered out.
|
|
350
|
+
|
|
351
|
+
Nothing about your app changes to get this — declare a workflow as you already
|
|
352
|
+
do, and it becomes reachable. If a workflow should NOT be agent-reachable, it
|
|
353
|
+
does not belong in the manifest at all, since the member can already fire it
|
|
354
|
+
from the app's own buttons.
|
|
355
|
+
|
|
356
|
+
**What this means for what you push.** Keep `description` — the one line that
|
|
357
|
+
says who is on screen and where they stand, which is what makes "this one"
|
|
358
|
+
resolvable. Don't pack bulk rows into `data` to pre-answer questions that may
|
|
359
|
+
never be asked: the agent can fetch them when needed, fresher than a snapshot,
|
|
360
|
+
and a payload pushed on every message is carried by every later turn (see the
|
|
361
|
+
caps above).
|
|
362
|
+
|
|
321
363
|
### Query freshness — the mutation companion
|
|
322
364
|
|
|
323
365
|
When the ambient chat agent's turn ends and it mutated records, the host pushes every mounted query hook to re-read, so the screen the member is looking at reflects the agent's change without a manual refresh. That companion behavior is automatic — you write no code for it — and is documented with the query caching contract in [data_fetching](./data_fetching.md#caching-loading-states-and-errors).
|
package/docs/queries.md
CHANGED
|
@@ -120,6 +120,15 @@ Delivery-layer enrichment (applied to the response, per request):
|
|
|
120
120
|
only for authenticated members of the app's own org).
|
|
121
121
|
- **`select` cells** — bare option-key arrays become `{ key, label }[]` (colors ride in
|
|
122
122
|
`useFieldOptions`, not the cell — see [members_and_options.md](./members_and_options.md)).
|
|
123
|
+
**Enrichment needs the column's source addressing**, which a passthrough projection carries, a
|
|
124
|
+
union carries when its arms agree (or per-row, when the compiler emits it), and a **link
|
|
125
|
+
extraction** carries as of the target field it reads. The one shape that has none is a select
|
|
126
|
+
reached through a **lookup FIELD**: the column addresses the lookup, whose own type is `lookup`,
|
|
127
|
+
not `select` — so the cell stays bare `opt_*` keys AND `useFieldOptions` omits the column
|
|
128
|
+
entirely, and reading `.label` off it renders the option KEY. Carry the keys out
|
|
129
|
+
(`readSelect(cell).map((o) => o.key)`) and resolve them against an alias that projects the
|
|
130
|
+
underlying field DIRECTLY — `useFieldOptions` takes no params, so any alias whose schema
|
|
131
|
+
contains it works.
|
|
123
132
|
- **`number` columns** arrive as JS numbers (Postgres `numeric` strings are coerced; a
|
|
124
133
|
high-precision decimal that would lose digits stays a string). **`date`/`datetime`** columns
|
|
125
134
|
arrive as canonical wall-clock strings (`YYYY-MM-DD` / `YYYY-MM-DDTHH:mm`).
|
|
@@ -350,6 +359,12 @@ target table can silently re-point (or break) the extraction — prefer field **
|
|
|
350
359
|
target's names are volatile. Each extraction is a correlated subquery evaluated per output
|
|
351
360
|
row — cheap on a filtered detail read, expensive over thousands of rows.
|
|
352
361
|
|
|
362
|
+
An extracted column **is** addressed to the field it reads on the target table, so the delivery
|
|
363
|
+
layer treats it like any other field-backed column: an extracted `select` enriches to
|
|
364
|
+
`{key,label}` and appears in `useFieldOptions`, and an extracted date resolves relative-date
|
|
365
|
+
filters in the source field's timezone. It is still never **writable** — a correlated subquery is
|
|
366
|
+
not a write path, and `writable_target` rejects it.
|
|
367
|
+
|
|
353
368
|
### The expression escape hatch
|
|
354
369
|
|
|
355
370
|
`{ "expression": "…" }` compiles a small jexpr subset to SQL. Column references are
|