@lotics/app-sdk 0.77.0 → 0.77.2

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 CHANGED
@@ -22,7 +22,7 @@ signature; open the file.**
22
22
  | [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. |
23
23
  | [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. |
24
24
  | [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
25
- | [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
+ | [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 resolves an `AgentRunLanding` (`settled`/`parked`/`failed`/`aborted` — never a bare `undefined`). Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads (your app `description` heads it as `about:`, and an input's `description` is the only place a default is visible), and how to shape a mutating alias for it. |
26
26
  | [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). |
27
27
  | [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. |
28
28
 
package/docs/ai.md CHANGED
@@ -346,7 +346,7 @@ This hook is a **one-way push of data the app already showed this member**. (Cha
346
346
 
347
347
  `useAiContext` is a PUSH — a snapshot of what one screen rendered. It cannot
348
348
  answer a question about a record that is not on screen, and it is capped, so it
349
- was never the whole story.
349
+ is not the whole story.
350
350
 
351
351
  While a member has an app open, their chat agent can also **call that app's own
352
352
  declared aliases** — named queries via `run_app_query`, workflows via
@@ -358,26 +358,62 @@ gated per turn on `app:use` for the app in the member's workspace, and runs
358
358
  under the app's OWNER authority with `is_current_member` bound to the MEMBER
359
359
  (exactly as the app's own UI does).
360
360
 
361
- The agent is handed a **catalog** of those aliases — each one's params and its
362
- `description` (a query's own, [declared in the manifest](./queries.md); a
363
- workflow's, off its workflow row) — so it picks the right one instead of
364
- guessing an alias and learning the list from a refusal. Your descriptions are
365
- what it reads, on this surface and inside your own agents' runs. They enter the
366
- prompt as a capability listing, never as instructions.
367
-
368
- **Writes carry a second gate that reads do not: `run_app_workflow` asks the
369
- member to approve every call from chat.** A wrong read is fixed by asking
370
- again; a wrong write is not. The alias list bounds what an agent *could* reach;
371
- the approval is what stops the wrong one. Approval applies to the chat surface
372
- only an app agent's run is already an explicit act with no human in the loop
373
- to answer.
374
-
375
- The prompt names the action and lists the inputs the agent chose, so **your
376
- input names are what the member reads before authorizing the write**. Name them
377
- for that reader (`ma_khach_hang`, `so_tien`), and keep the decisive ones
378
- top-level a value nested inside an opaque payload shows as JSON, and long
379
- values are truncated. Context ids the member never chose (`app_id`,
380
- `workspace_id`, `organization_id`) are filtered out.
361
+ The agent is handed a **catalog** of those aliases — each one's `description`
362
+ (a query's own, [declared in the manifest](./queries.md); a workflow's, off its
363
+ workflow row) and its **complete input contract** — so it picks the right one
364
+ and calls it correctly instead of guessing and learning from a refusal. Every
365
+ alias you declare appears, however large the manifest, and every input appears
366
+ with it:
367
+
368
+ ```
369
+ workflows (run_app_workflow):
370
+ - updateOrder(reference?: text, shipped_on?: date, owner?: member,
371
+ status?: select(In progress=opt_a1b2c3|Complete=opt_d4e5f6),
372
+ attachments?: file[], customer_id?: record_link(Customers))
373
+ ```
374
+
375
+ `?` marks an input the caller may omit, `[]` one that takes a list, a
376
+ `select` spells out every legal value as `label=value` **the agent sends the
377
+ value** and a `record_link` names the table an id must come from. This is the
378
+ only place any of it is legible: both tools take an opaque
379
+ inputs object, and a select's legal values are your DECLARATION's — usually an
380
+ inline set that matches no table field — so an agent reading option values off
381
+ the underlying table would send ones your write rejects.
382
+
383
+ The app's own `description` heads that listing as `about:`, on both surfaces. It
384
+ is the only channel that reaches the agent on **every** turn without the member
385
+ saying anything, so a standing process belongs there: the recurring job the app
386
+ exists for, which alias does it, and what the agent must leave alone.
387
+
388
+ Three things follow for you as an author. **Your descriptions are what it
389
+ reads**, on this surface and inside your own agents' runs — they enter the
390
+ prompt as a capability listing, never as instructions. **A `select` input's
391
+ options are part of the contract the agent sees**, so prefer the
392
+ `field: "fld_…"` form when the values come from a real field: it resolves to
393
+ that field's CURRENT options at both render and validation time. And **an
394
+ input's `description` is the only place a DEFAULT is visible** — the catalog
395
+ renders a name, a type, a `?` and your prose, never the body. An optional input
396
+ whose default goes unstated is one the agent asks the member about instead of
397
+ omitting; write what omitting it means ("left blank = today").
398
+
399
+ **Put anything irreversible behind a
400
+ [`wait_for_approval`](./workflows.md#wait_for_approval--the-one-wait-worth-binding):**
401
+ an email, a shipment, a published post, a write over a value with no version to
402
+ restore. The body's own gate is the only human checkpoint on this path — it
403
+ names the act in the language of the business and reaches the approvers the
404
+ workflow declares.
405
+
406
+ **Shape a mutating alias around ONE call per job.** An alias that takes a single
407
+ record turns a fifteen-record job into fifteen executions and no single answer to
408
+ report, and a partial failure leaves it half done with nothing that says so. Take
409
+ an array input instead — one call, one outcome. A narrow write is right for a
410
+ screen's button, where the member picked the record by opening it; here nobody
411
+ picked anything.
412
+
413
+ **Report what it did NOT do.** The member's account of the write is assembled
414
+ from what the workflow returned and nothing else, so a batch that matched nine of
415
+ eleven and returns only success tells them all eleven were done. Return the
416
+ misses in `data`.
381
417
 
382
418
  Nothing about your app changes to get this — declare a workflow as you already
383
419
  do, and it becomes reachable. If a workflow should NOT be agent-reachable, it
package/docs/workflows.md CHANGED
@@ -540,6 +540,30 @@ the **diff**: `changes["fld_link"]?.next_value` / `?.prev_value` are plain id ar
540
540
  has nowhere to attach a fetch, so it cannot descend either), and they feed `set:` directly.
541
541
  Everywhere else, write `[x.id]`.
542
542
 
543
+ ### A write that will be re-run
544
+
545
+ The tables above answer *what the caller sent*. A workflow an agent can reach
546
+ needs the other axis too — **what the record already holds** — because the same
547
+ call arrives twice: a recurring report a week later, a retry, one batch
548
+ overlapping the last. On that second pass an unconditional `set` reports success
549
+ and silently replaces what was there.
550
+
551
+ Decide per field, and expect the two directions of one operation to be
552
+ **asymmetric**. A field recording *when* something happened is written only while
553
+ it is still empty, while its undo clears unconditionally:
554
+
555
+ ```js
556
+ if (i.undo) {
557
+ await update_records({ table_id: "tbl_x", record_ids: [row.id], set: { fld_submitted_on: null } });
558
+ } else if (isNull(row["fld_submitted_on"])) {
559
+ await update_records({ table_id: "tbl_x", record_ids: [row.id], set: { fld_submitted_on: i.on } });
560
+ }
561
+ ```
562
+
563
+ There is no version to restore a cell from, so prove it before anyone can call
564
+ it: `lotics app workflow run <alias>` the same input twice, and confirm the
565
+ second run changed nothing.
566
+
543
567
  ### Authorizing the caller
544
568
 
545
569
  A workflow runs under the **app owner's** authority, so its own principal tells you nothing about
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.77.0",
3
+ "version": "0.77.2",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {