@lotics/app-sdk 0.66.1 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/dist/src/hooks.js +28 -10
- package/docs/ai.md +2 -2
- package/docs/data_fetching.md +12 -8
- package/docs/members_and_options.md +4 -4
- package/docs/queries.md +13 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ signature; open the file.**
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
18
|
-
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
18
|
+
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — data discipline, the search-as-you-type + record-picker patterns. |
|
|
19
19
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
20
20
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + link descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
|
|
21
21
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
package/dist/src/hooks.js
CHANGED
|
@@ -31,19 +31,24 @@ export function useWorkflow(alias) {
|
|
|
31
31
|
// rows (no retry loop that masks the error), and honor the focus/reconnect
|
|
32
32
|
// opt-out.
|
|
33
33
|
//
|
|
34
|
-
// `revalidateIfStale
|
|
35
|
-
// -
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
34
|
+
// `revalidateIfStale` is LEFT AT SWR's DEFAULT (`true`): re-mounting a screen
|
|
35
|
+
// renders its cached rows AND re-reads them. Pinning it `false` saved a re-query
|
|
36
|
+
// per navigation and cost every screen a hand-rolled arrival refetch — where
|
|
37
|
+
// forgetting is SILENT (no error, no empty state, just last visit's data until a
|
|
38
|
+
// hard reload), which is not a mistake an author gets told about once and learns.
|
|
39
|
+
//
|
|
40
|
+
// The default costs one BACKGROUND request per re-mount of a warm key and
|
|
41
|
+
// nothing visible: SWR's `isLoading` is true only on an initial load with no
|
|
42
|
+
// cached data, so nothing blanks to a skeleton, and `dedupingInterval` collapses
|
|
43
|
+
// a burst of navigation into one fetch.
|
|
44
|
+
//
|
|
45
|
+
// `useAppContext` (viewer.ts) still pins it off — the app's identity is read
|
|
46
|
+
// once at boot and cannot change under the reader. `useInfiniteQuery` is the
|
|
47
|
+
// one hook this does not reach; see its own note.
|
|
42
48
|
function swrConfig(revalidateOnFocus) {
|
|
43
49
|
return {
|
|
44
50
|
revalidateOnFocus,
|
|
45
51
|
revalidateOnReconnect: revalidateOnFocus,
|
|
46
|
-
revalidateIfStale: false,
|
|
47
52
|
shouldRetryOnError: false,
|
|
48
53
|
};
|
|
49
54
|
}
|
|
@@ -152,7 +157,20 @@ export function useInfiniteQuery(alias, params, opts) {
|
|
|
152
157
|
sort,
|
|
153
158
|
filter,
|
|
154
159
|
});
|
|
155
|
-
},
|
|
160
|
+
},
|
|
161
|
+
// `revalidateFirstPage: false` — `loadMore()` fetches the next page and only
|
|
162
|
+
// that page. SWR's default re-fetches page 1 on every load-more, costing a
|
|
163
|
+
// request per scroll and reshuffling the top of the feed under a reader
|
|
164
|
+
// looking further down it (pinned by the keyset test).
|
|
165
|
+
//
|
|
166
|
+
// THE COST, stated because it is otherwise invisible: this hook does not
|
|
167
|
+
// arrival-revalidate the way the other two do. SWRInfinite decides per page
|
|
168
|
+
// and never consults `revalidateIfStale`, and neither lever that would change
|
|
169
|
+
// that is cheap — `revalidateFirstPage` pays per load-more, `revalidateOnMount`
|
|
170
|
+
// refetches EVERY loaded page. A cold key still fetches, focus/reconnect and
|
|
171
|
+
// `refetch()` still refresh; a feed that must be fresh on arrival calls
|
|
172
|
+
// `refetch()`.
|
|
173
|
+
{ revalidateFirstPage: false, ...swrConfig(revalidateOnFocus) });
|
|
156
174
|
// SWRInfinite leaves an in-flight page slot `undefined` until it resolves —
|
|
157
175
|
// operate on resolved pages only so a page being appended never crashes the
|
|
158
176
|
// flatten or skews the counts.
|
package/docs/ai.md
CHANGED
|
@@ -24,8 +24,8 @@ A declaration carries:
|
|
|
24
24
|
| `knowledge_doc_ids` | The knowledge docs the agent may read, and the whole set it can reach — a doc absent from this list is unreadable even if the agent names its id. Declare `grep_knowledge` / `read_knowledge` in `tool_names` to read them. There is no size limit and nothing is inlined, so a multi-megabyte reference corpus (a full tariff, a regulation set) is a normal declaration. Reach for `code_exec` only to COMPUTE across the corpus — counting, cross-referencing — never merely to read it |
|
|
25
25
|
| `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records. You list them; there is no "all of them" shorthand, and adding a query to the app never widens an existing agent. Omitted means the agent reads nothing |
|
|
26
26
|
| `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
|
|
27
|
-
| `
|
|
28
|
-
| `effort_level` | Optional reasoning depth for adaptive-thinking
|
|
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
|
+
| `effort_level` | Optional reasoning depth for adaptive-thinking tiers. Requires an explicit `model_tier` pin — effort is tuned per tier |
|
|
29
29
|
| `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 |
|
|
30
30
|
| `outputs` | Optional typed output schema. Declared → **structured agent** (the run must emit a matching result); omitted → **free-text agent** (the answer is the final prose) |
|
|
31
31
|
|
package/docs/data_fetching.md
CHANGED
|
@@ -67,14 +67,18 @@ server validates system conditions by `type` and never reads `field_key` on them
|
|
|
67
67
|
- **Cache identity** is the tuple `(alias, params, pageSize, sort, filter)` (+ the page index for
|
|
68
68
|
the paged hooks). Object *contents* are hashed, not references — passing a fresh inline
|
|
69
69
|
`{ status: "open" }` each render is the same key; you never need to memoize params.
|
|
70
|
-
- The cache **survives unmount/remount
|
|
71
|
-
and
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
- The cache **survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
|
|
71
|
+
renders the cached rows instantly *and* revalidates them in the background, so a list reflects what
|
|
72
|
+
another screen changed while you were away. Identical concurrent reads dedupe to one request.
|
|
73
|
+
Freshness comes from four places — arrival, window focus / tab return / network reconnect
|
|
74
|
+
(`revalidateOnFocus`, default on), the host's post-chat-turn poke, and explicit `refetch()`.
|
|
75
|
+
**Still call `refetch()` after a write the user is watching for**: arrival covers navigation, not a
|
|
76
|
+
mutation made on the screen you are already standing on.
|
|
77
|
+
- **`useInfiniteQuery` does not arrival-revalidate** — the one exception. A warm feed re-mounted
|
|
78
|
+
refetches nothing; a cold key still fetches, and focus / reconnect / the chat poke still refresh.
|
|
79
|
+
Re-fetching page 1 on arrival would cost a request on every `loadMore()` and reshuffle the top of
|
|
80
|
+
the feed under a reader scrolling further down it, and re-fetching every loaded page grows without
|
|
81
|
+
bound. **A feed that must be fresh on arrival calls `refetch()`.**
|
|
78
82
|
- **`loading`** is `true` only on the *initial* load of a key — a request is in flight and there
|
|
79
83
|
are no rows yet. It stays `false` during background revalidation of a key that already has rows,
|
|
80
84
|
so consumers never blank loaded data to a spinner on refetch. A key *change* (new params, sort,
|
|
@@ -89,10 +89,10 @@ name**, not the field key. Each `FieldOptions`:
|
|
|
89
89
|
|
|
90
90
|
### Caching & freshness
|
|
91
91
|
|
|
92
|
-
Field config is slow-changing, so the hook
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
Field config is slow-changing, so the hook does **not** revalidate on window focus or reconnect — it
|
|
93
|
+
re-reads on arrival (a remount) like the query hooks, and on an explicit `refetch()`. The option set
|
|
94
|
+
is resolved live from field config at fetch time, so an option added, renamed, or recolored in the
|
|
95
|
+
table flows through on the next fetch, with no app redeploy. `opts.enabled` defers the fetch (e.g.
|
|
96
96
|
until an edit drawer opens). State: `{ fields, loading, isValidating, error, refetch }`.
|
|
97
97
|
|
|
98
98
|
### Coloring a stored value (the `byKey` idiom)
|
package/docs/queries.md
CHANGED
|
@@ -780,6 +780,19 @@ template as derived nodes, in this order: `filter` (narrow) → `sort` (order)
|
|
|
780
780
|
- **`count: true`** — returns `{ total }` only: a single-row COUNT over the *filtered* set,
|
|
781
781
|
ignoring sort/limit/offset. Drives "Page 1 of N".
|
|
782
782
|
|
|
783
|
+
**A terminal aggregate cannot be refined — plan the total accordingly.** Because refinement wraps
|
|
784
|
+
*around* the template over its **output** columns, a query that has already collapsed (a `group`
|
|
785
|
+
with an empty `by` and a `sum`, say) has no source column left for the filter to name, so a facet
|
|
786
|
+
the list narrows by 400s against the total beside it. `window` is not a way out either: the frame
|
|
787
|
+
aggregate is computed before the refinement wraps, so the number describes the unfiltered set.
|
|
788
|
+
Both alternatives are worse than they look — baking the facet into the aggregate stops it being a
|
|
789
|
+
runtime facet (a filter TREE is not a scalar param), and leaving the total unfiltered puts a count
|
|
790
|
+
that follows the facet next to a sum that does not. **Project the summed column plus the columns
|
|
791
|
+
the facet can name, fetch under the same runtime `filter`, and total in the browser** — correct and
|
|
792
|
+
facet-aware, but it ships every matching row, so it holds only where the scope is already small
|
|
793
|
+
(one member's own records, a single project). A workspace-wide total that must follow a runtime
|
|
794
|
+
facet has no server-side shape today.
|
|
795
|
+
|
|
783
796
|
The SDK hooks map onto this directly (`dist/src/hooks.d.ts` for exact signatures): `useQuery`
|
|
784
797
|
sends `limit: pageSize, offset: 0` (a cap, not pagination) plus `opts.sort`/`opts.filter`;
|
|
785
798
|
`useInfiniteQuery` pages by **keyset** (`keyset: true` + the prior `next_cursor`), so its scroll
|