@lotics/app-sdk 0.102.3 → 0.103.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 CHANGED
@@ -17,7 +17,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
17
17
  |---|---|
18
18
  | [docs/data_fetching.md](./docs/data_fetching.md) | The reads: `useQuery(alias, params, opts)` — its rows (`limit`), numbered pages (`page`), a keyset feed (`more`), its `total` or the total alone, with `total: { by }` a count per value of one column — `useQueries` for reads known only at render, `queryAll` outside React, `exportQuery` for a file of them the server makes; every hook answers one `QueryState`. The ROW type (`RowOf` — the alias's projected columns and nothing else), runtime `sort`/`filter` keys, cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`, `readCreatedAt`/`readUpdatedAt`), the SDK's own cache — **arrival revalidates** — **realtime push**, a write drawn on every read from the press, a count as its own full scan, and the search-as-you-type and record-picker patterns. |
19
19
  | [docs/queries.md](./docs/queries.md) | **The query engine reference** — AST node kinds, per-field-type operators, filters/params/pruning, free-text search, combining tables, shaping (aggregates, date buckets, windows), runtime refinement bounds, limits and the efficiency playbook. |
20
- | [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path; `useWorkflows` for writes that are data), the `WorkflowResult` resolve-never-throw contract (`field_errors`), typed inputs, every write drawn on every read from the press — predicted from the workflow's own steps, taken back on a refusal, `pending` meanwhile — the re-read a successful write triggers over the tables its body names, diff-before-update, locked records, `useNewRecord`, read-after-write ordering (`writesSettled()`), `useRecording`. |
20
+ | [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path; `useWorkflows` for writes that are data), the `WorkflowResult` resolve-never-throw contract (`field_errors`), typed inputs, every write drawn on every read from the press — predicted from the workflow's own steps, taken back on a refusal, `pending` meanwhile — the re-read a successful write triggers over the tables its body names, diff-before-update, locked records, `useNewRecord`, read-after-write ordering (`writesSettled()`), `useRecording`, `useRecordings` over several aliases. |
21
21
  | [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY reference** — the JS subset a body may use, opaque `fld_*`/`opt_*` keys, every step form, the accepted sugar, helpers, record-write surfaces, the traps and the verify loop. |
22
22
  | [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
23
23
  | [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload` and its `fidelity`, `renameFile` (a new file over the same bytes), `useAttachments`/`useAttachmentPiles`, `readFiles` and presigned URLs (a bearer credential — never logged or persisted), workflow-generated files, naming a zip's entries, the delivery bounds. |
@@ -444,9 +444,14 @@ async function runUploadPipeline(file, rpc2, options = {}) {
444
444
  var IDLE_RECORDING = { phase: "idle" };
445
445
  var states = /* @__PURE__ */ new Map();
446
446
  var listeners = /* @__PURE__ */ new Set();
447
+ var version = 0;
447
448
  function emit() {
449
+ version += 1;
448
450
  for (const listener of listeners) listener();
449
451
  }
452
+ function recordingsVersion() {
453
+ return version;
454
+ }
450
455
  function subscribeRecordings(listener) {
451
456
  listeners.add(listener);
452
457
  return () => {
@@ -1115,6 +1120,7 @@ async function standaloneUpload(file, fidelity) {
1115
1120
  export {
1116
1121
  __export,
1117
1122
  DEFAULT_IMAGE_FIDELITY,
1123
+ recordingsVersion,
1118
1124
  subscribeRecordings,
1119
1125
  recordingStateOf,
1120
1126
  anyRecordingLive,
package/dist/index.d.ts CHANGED
@@ -11,8 +11,8 @@ export type { AttachedFile, AttachmentPiles, AttachmentPilesOptions, Attachments
11
11
  export { useComments, useCommentCounts } from "./comments.js";
12
12
  export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
13
13
  export { useAppContext, useViewer, useWorkspaceCurrency, useWorkspaceTimezone } from "./viewer.js";
14
- export { useRecording } from "./recording.js";
15
- export type { UseRecording } from "./recording.js";
14
+ export { useRecording, useRecordings } from "./recording.js";
15
+ export type { UseRecording, UseRecordings } from "./recording.js";
16
16
  export type { RecordingState, RecordingInputs } from "./recording_state.js";
17
17
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
18
18
  export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "./geolocation.js";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  postHostNotification,
13
13
  readRecordingSnapshot,
14
14
  recordingStateOf,
15
+ recordingsVersion,
15
16
  replaceRecordings,
16
17
  rpc,
17
18
  rpcAgentRun,
@@ -21,7 +22,7 @@ import {
21
22
  subscribeRecordings,
22
23
  subscribeUrlParams,
23
24
  urlParam
24
- } from "./chunk-ZOFKWF6V.js";
25
+ } from "./chunk-CRJQYSDI.js";
25
26
 
26
27
  // src/mount.tsx
27
28
  import { createRoot } from "react-dom/client";
@@ -24234,6 +24235,11 @@ var appQueryDeclarationSchema = zod_default.object({
24234
24235
  "Document templates an export of this query's rows may fill, by the name the export asks for. An export names one of these or none; a template it does not declare is refused."
24235
24236
  )
24236
24237
  });
24238
+ var appAgentWritesSchema = zod_default.object({
24239
+ table_id: zod_default.string().min(1).describe("The table of the row written"),
24240
+ row: zod_default.string().min(1).describe("The input naming that row: a single `record_link` input to `table_id`"),
24241
+ fields: zod_default.record(zod_default.string().min(1), zod_default.string().min(1)).refine((fields) => Object.keys(fields).length > 0, { message: "writes.fields maps at least one output" }).describe("Output name \u2192 the field key of `table_id` it is written to")
24242
+ }).strict();
24237
24243
  var appAgentDeclarationSchema = zod_default.object({
24238
24244
  instructions: zod_default.string().min(1).describe("System instructions for the agent \u2014 the task it performs per run."),
24239
24245
  tool_names: zod_default.array(zod_default.string().min(1)).describe(
@@ -24265,6 +24271,9 @@ var appAgentDeclarationSchema = zod_default.object({
24265
24271
  ).refine(
24266
24272
  (outputs) => outputs === void 0 || Object.values(outputs).every((o) => appWorkflowOutputDepth(o) <= MAX_APP_WORKFLOW_OUTPUT_DEPTH),
24267
24273
  { message: `output schema nesting exceeds the max depth of ${MAX_APP_WORKFLOW_OUTPUT_DEPTH}` }
24274
+ ),
24275
+ writes: appAgentWritesSchema.optional().describe(
24276
+ "Where the run's outputs land: each named output written to a field of the one row an input names, under the app's authority, once the result validates \u2014 locks and hooks apply as to any update. Omit for an agent whose output is only returned."
24268
24277
  )
24269
24278
  });
24270
24279
  var appThemeSchema = zod_default.object({
@@ -24396,6 +24405,12 @@ var agentStepSchema = stepBaseSchema.extend({
24396
24405
  model_tier: zod_default.enum(MODEL_TIERS).optional(),
24397
24406
  output: agentOutputSpecSchema
24398
24407
  });
24408
+ var appAgentStepSchema = stepBaseSchema.extend({
24409
+ type: zod_default.literal("app_agent"),
24410
+ alias: zod_default.string().min(1),
24411
+ input: zod_default.record(zod_default.string(), toolInputValueSchema),
24412
+ wait: zod_default.literal(false).optional()
24413
+ });
24399
24414
  var breakStepSchema = stepBaseSchema.extend({
24400
24415
  type: zod_default.literal("break")
24401
24416
  });
@@ -24422,6 +24437,7 @@ var workflowStepSchema = zod_default.lazy(
24422
24437
  returnStepSchema,
24423
24438
  validateStepSchema,
24424
24439
  agentStepSchema,
24440
+ appAgentStepSchema,
24425
24441
  breakStepSchema,
24426
24442
  continueStepSchema,
24427
24443
  letDeclareStepSchema,
@@ -25044,6 +25060,7 @@ function stepChildStepArrays(step) {
25044
25060
  case "bind":
25045
25061
  case "validate":
25046
25062
  case "agent":
25063
+ case "app_agent":
25047
25064
  case "break":
25048
25065
  case "continue":
25049
25066
  case "let_declare":
@@ -25076,6 +25093,7 @@ function changesRows(toolName) {
25076
25093
  }
25077
25094
  function rowWriteOf(step) {
25078
25095
  if (step.type === "agent") return step.tool_names.some(changesRows) ? { table_id: null } : void 0;
25096
+ if (step.type === "app_agent") return { table_id: null };
25079
25097
  if (step.type !== "tool_call") return void 0;
25080
25098
  const rows = toolRows(step.tool_name);
25081
25099
  if (rows === null) return void 0;
@@ -27204,7 +27222,8 @@ async function run(walk, step) {
27204
27222
  if (write2 !== void 0) note(walk, { kind: "unknown", table_id: write2.table_id });
27205
27223
  return unknownAt(walk.ctx.step_outputs, step.id);
27206
27224
  }
27207
- case "agent": {
27225
+ case "agent":
27226
+ case "app_agent": {
27208
27227
  const write2 = rowWriteOf(step);
27209
27228
  if (write2 !== void 0) note(walk, { kind: "unknown", table_id: write2.table_id });
27210
27229
  return unknownAt(walk.ctx.step_outputs, step.id);
@@ -31086,33 +31105,54 @@ function useCommentCounts(args) {
31086
31105
  }
31087
31106
 
31088
31107
  // src/recording.ts
31089
- import { useCallback as useCallback7, useSyncExternalStore as useSyncExternalStore3 } from "react";
31108
+ import { useCallback as useCallback7, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
31090
31109
  var NO_MOCKS = {};
31110
+ async function startRecording(alias, inputs) {
31111
+ if (getMockRecordings()?.[alias]) return { started: true };
31112
+ return rpc("recording.start", { alias, inputs });
31113
+ }
31114
+ async function stopRecording(alias) {
31115
+ if (getMockRecordings()?.[alias]) return;
31116
+ await rpc("recording.stop", { alias });
31117
+ }
31118
+ function busyWith(mocks, hostBusy) {
31119
+ return hostBusy || Object.values(mocks).some((state) => state.phase === "live");
31120
+ }
31091
31121
  function useRecording(alias) {
31092
31122
  const { recordingEnabled } = useAppContext();
31093
31123
  const mocks = getMockRecordings() ?? NO_MOCKS;
31094
31124
  const mocked = mocks[alias];
31095
31125
  const hostState = useSyncExternalStore3(subscribeRecordings, () => recordingStateOf(alias));
31096
31126
  const hostBusy = useSyncExternalStore3(subscribeRecordings, anyRecordingLive);
31097
- const start = useCallback7(
31098
- async (inputs) => {
31099
- if (getMockRecordings()?.[alias]) return { started: true };
31100
- return rpc("recording.start", { alias, inputs: inputs ?? {} });
31101
- },
31102
- [alias]
31103
- );
31104
- const stop = useCallback7(async () => {
31105
- if (getMockRecordings()?.[alias]) return;
31106
- await rpc("recording.stop", { alias });
31107
- }, [alias]);
31127
+ const start = useCallback7((inputs) => startRecording(alias, inputs ?? {}), [alias]);
31128
+ const stop = useCallback7(() => stopRecording(alias), [alias]);
31108
31129
  return {
31109
31130
  available: mocked !== void 0 || recordingEnabled,
31110
31131
  state: mocked ?? hostState,
31111
- busy: hostBusy || Object.values(mocks).some((state) => state.phase === "live"),
31132
+ busy: busyWith(mocks, hostBusy),
31112
31133
  start,
31113
31134
  stop
31114
31135
  };
31115
31136
  }
31137
+ function useRecordings(aliases) {
31138
+ const { recordingEnabled } = useAppContext();
31139
+ const mocks = getMockRecordings() ?? NO_MOCKS;
31140
+ const version3 = useSyncExternalStore3(subscribeRecordings, recordingsVersion);
31141
+ const hostBusy = useSyncExternalStore3(subscribeRecordings, anyRecordingLive);
31142
+ const key = aliases.join("\n");
31143
+ const states = useMemo4(
31144
+ () => Object.fromEntries(key.split("\n").filter((alias) => alias !== "").map((alias) => [alias, mocks[alias] ?? recordingStateOf(alias)])),
31145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31146
+ [key, version3, mocks]
31147
+ );
31148
+ return {
31149
+ available: recordingEnabled || aliases.some((alias) => mocks[alias] !== void 0),
31150
+ states,
31151
+ busy: busyWith(mocks, hostBusy),
31152
+ start: startRecording,
31153
+ stop: stopRecording
31154
+ };
31155
+ }
31116
31156
 
31117
31157
  // src/geolocation.ts
31118
31158
  var EARTH_RADIUS_M = 6371e3;
@@ -31320,7 +31360,7 @@ function useRecents(key, options = {}) {
31320
31360
  }
31321
31361
 
31322
31362
  // src/use_url_state.ts
31323
- import { useCallback as useCallback9, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
31363
+ import { useCallback as useCallback9, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
31324
31364
  function useUrlState(defs) {
31325
31365
  const defsRef = useRef5(defs);
31326
31366
  defsRef.current = defs;
@@ -31340,7 +31380,7 @@ function useUrlState(defs) {
31340
31380
  unsubscribe();
31341
31381
  };
31342
31382
  }, []);
31343
- const values2 = useMemo4(() => decodeAll(defsRef.current, params), [params]);
31383
+ const values2 = useMemo5(() => decodeAll(defsRef.current, params), [params]);
31344
31384
  const setValues = useCallback9(
31345
31385
  (patch) => {
31346
31386
  editedRef.current = true;
@@ -31424,6 +31464,7 @@ export {
31424
31464
  useQuery,
31425
31465
  useRecents,
31426
31466
  useRecording,
31467
+ useRecordings,
31427
31468
  useUrlState,
31428
31469
  useViewer,
31429
31470
  useWorkflow,
@@ -44,4 +44,22 @@ export interface UseRecording<I> {
44
44
  */
45
45
  export declare function useRecording<K extends keyof AppWorkflows & string>(alias: K): UseRecording<ActInputsOf<K>>;
46
46
  export declare function useRecording(alias: string): UseRecording<Record<string, unknown>>;
47
+ export interface UseRecordings {
48
+ /** As {@link UseRecording}'s: whether the host records here at all. */
49
+ available: boolean;
50
+ /** The latest recording through each alias asked, idle where none ran. */
51
+ states: Readonly<Record<string, RecordingState>>;
52
+ /** Any of this app's recordings is live, through any alias. */
53
+ busy: boolean;
54
+ start: (alias: string, inputs: Record<string, unknown>) => Promise<{
55
+ started: boolean;
56
+ }>;
57
+ stop: (alias: string) => Promise<void>;
58
+ }
59
+ /**
60
+ * {@link useRecording} over several aliases at once — for a screen drawing an
61
+ * unknown number of recording acts, which cannot call a hook per alias. One
62
+ * snapshot per change: the host's every-second push while live redraws once.
63
+ */
64
+ export declare function useRecordings(aliases: readonly string[]): UseRecordings;
47
65
  export {};
@@ -33,6 +33,7 @@ export type RecordingState = {
33
33
  inputs: RecordingInputs;
34
34
  };
35
35
  export declare const IDLE_RECORDING: RecordingState;
36
+ export declare function recordingsVersion(): number;
36
37
  export declare function subscribeRecordings(listener: () => void): () => void;
37
38
  export declare function recordingStateOf(alias: string): RecordingState;
38
39
  export declare function anyRecordingLive(): boolean;
package/dist/router.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isEmbedded,
3
3
  setUrlParams
4
- } from "./chunk-ZOFKWF6V.js";
4
+ } from "./chunk-CRJQYSDI.js";
5
5
 
6
6
  // src/router.tsx
7
7
  import { useEffect } from "react";
package/docs/ai.md CHANGED
@@ -29,6 +29,7 @@ A declaration carries:
29
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
+ | `writes` | Optional — where the result lands: `{ table_id, row, fields }`. `row` is a single `record_link` input to `table_id`; `fields` maps each output to a field key of that table. Once a run's result validates, each output it carries is written to that row under the app's authority, with the table's locks and hooks applied; an output the result omits is left alone. A write that fails fails the run. Requires `outputs` |
32
33
 
33
34
  **Structured vs free-text is the load-bearing split.** A structured agent's result arrives in `run.output` (the server strictly validates the submitted result against the declared schema — see the `output` typing section for the exact guarantee); a free-text agent's answer is the transcript's prose (`run.text`) and its `output` stays `undefined` — never a stray string, so a consumer reading `output.<field>` can't crash on a free-text answer.
34
35
 
package/docs/mutations.md CHANGED
@@ -674,6 +674,11 @@ replaces the first's state):
674
674
  | `filed` | `execution_id`, `inputs` | the workflow ran and succeeded |
675
675
  | `failed` | `message`, `inputs` | transcription failed, or filing was refused or its run failed |
676
676
 
677
+ A screen drawing a number of recording acts it only learns at run time reads them together with
678
+ **`useRecordings(aliases)`**: `available` and `busy` as above, `states` (each alias's latest
679
+ recording, idle where none ran), and `start(alias, inputs)` / `stop(alias)` — the same calls, by
680
+ alias.
681
+
677
682
  `inputs` is what `start` was passed, so a screen with one act per row can tell which row the
678
683
  recording is about. **`busy`** is true while any recording this app started is live, through
679
684
  any alias: stand the other acts down, since the host would refuse them. A recording in another
package/docs/workflows.md CHANGED
@@ -150,6 +150,7 @@ The body is a sequence of statements, each of which maps 1:1 onto a stored step.
150
150
  | Pause for an event | `await wait_for_event({ event_type: "payment" \| "webhook", event_ref, timeout_in_minutes? });` |
151
151
  | Pause for a decision | `const a = await wait_for_approval({ approvers, prompt, timeout_in_minutes? });` |
152
152
  | LLM reasoning | `const x = await agent({ instructions, input, tools, model?, output });` |
153
+ | Run a declared agent | `const x = await app_agent({ alias, input, wait? });` |
153
154
  | Guard | `validate({ checks: [{ fail_when, field_key?, message }, …] });` |
154
155
  | End the run | `return({ status, message, field_errors?, data? });` |
155
156
 
@@ -313,6 +314,22 @@ name" when broken:
313
314
  `model` is an optional pin — omit it to follow the platform default. The step is atomic (no wait
314
315
  or approval inside it).
315
316
 
317
+ ### `app_agent` — run one of this app's agents as one step
318
+
319
+ ```js
320
+ const scored = await app_agent({ alias: "scorer", input: { deal: trigger.app_workflow.inputs.deal } });
321
+ ```
322
+
323
+ `alias` names an agent this app declares; `input` is an object literal checked against its
324
+ `inputs`. The step resolves to the agent's declared `outputs` — typed, so `scored.summary` is
325
+ checked at save — or to its final text when it declares none, and applies the agent's `writes`.
326
+ The run shows in the agent's history like any other. It exists only in an app's own workflows,
327
+ and a run an agent started (through `run_app_workflow`) refuses it: an agent never starts an agent.
328
+
329
+ `wait: false` starts the run and resolves at once to `{ run_id }`. The workflow goes on, and what
330
+ it wrote stands; the agent's result — its `writes` included — lands when the run settles, and a
331
+ run that fails fails in the agent's history, not the workflow.
332
+
316
333
  ### `try` / `catch`
317
334
 
318
335
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.102.3",
3
+ "version": "0.103.0",
4
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": {