@lotics/app-sdk 0.102.0 → 0.102.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 +1 -1
- package/dist/agent_stream.d.ts +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +62 -22
- package/dist/mock.d.ts +7 -1
- package/dist/mock_agent_run.d.ts +23 -0
- package/docs/ai.md +8 -8
- package/docs/queries.md +17 -38
- package/docs/runtime.md +14 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
|
|
|
25
25
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
|
|
26
26
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming parts, the agent's ask-back), `askAi`, `useAiContext`, and what the member's own chat agent can do with the app while it is open. |
|
|
27
27
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, why a per-input bound is a tenancy floor rather than an authorization check. |
|
|
28
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()` and the errors it reports to the host (`reportAppError`), the two transports, the API address a standalone bundle reads out of its own page (`<meta name="lotics-api-base">`), `rpc()`, the mock harness (`fixture` + `?__mock=1`), `openExternal`/`openApp`/`downloadFile`, geofencing, the peer dependencies. |
|
|
28
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()` and the errors it reports to the host (`reportAppError`), the two transports, the API address a standalone bundle reads out of its own page (`<meta name="lotics-api-base">`), `rpc()`, the mock harness (`fixture` + `?__mock=1` — queries, workflows and agents, a mocked agent's run played line by line), `openExternal`/`openApp`/`downloadFile`, geofencing, the peer dependencies. |
|
|
29
29
|
|
|
30
30
|
## Non-negotiables (each detailed in its doc)
|
|
31
31
|
|
package/dist/agent_stream.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Folds the AI-SDK UI-message SSE stream into the `UIMessagePart[]` shape
|
|
3
3
|
* `@lotics/ui` `AgentRun` renders. `ai` is imported for types only, so no `ai`
|
|
4
4
|
* runtime enters the app bundle; unknown chunk types are ignored. A structured
|
|
5
|
-
* agent's `
|
|
5
|
+
* agent's `output` is the result the server accepted (`data-run-output`, sent
|
|
6
|
+
* before `finish`); until it arrives, `submit_result`'s input — never a part.
|
|
6
7
|
*/
|
|
7
8
|
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
8
9
|
/** An ai-sdk message part — the render model, tool-set-agnostic. */
|
|
@@ -12,7 +13,7 @@ export declare const INTERACTIVE_TOOLS: ReadonlySet<string>;
|
|
|
12
13
|
export interface AgentRunState {
|
|
13
14
|
status: "streaming" | "awaiting_input" | "completed" | "error";
|
|
14
15
|
parts: AgentUIPart[];
|
|
15
|
-
/** `submit_result`'s input, never the free text — a consumer reading `output.<field>` would crash on a string. */
|
|
16
|
+
/** The structured result — `submit_result`'s input as it streams, the server's accepted one from `data-run-output` on; never the free text — a consumer reading `output.<field>` would crash on a string. */
|
|
16
17
|
output?: unknown;
|
|
17
18
|
error?: string;
|
|
18
19
|
}
|
|
@@ -92,6 +93,7 @@ interface Chunk {
|
|
|
92
93
|
output?: unknown;
|
|
93
94
|
errorText?: string;
|
|
94
95
|
finishReason?: string;
|
|
96
|
+
data?: unknown;
|
|
95
97
|
}
|
|
96
98
|
export declare function reduceAgentChunk(state: AgentRunState, chunk: Chunk): AgentRunState;
|
|
97
99
|
/** Complete chunks so far and the partial frame to carry forward; `[DONE]` is dropped. */
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type { ResolvedMember } from "./members.js";
|
|
|
28
28
|
export { readSelect } from "./select.js";
|
|
29
29
|
export type { ResolvedOption } from "./select.js";
|
|
30
30
|
export type { AppFixture, MockExport, MockQuery, MockQueryCall, MockWorkflow } from "./mock.js";
|
|
31
|
+
export type { MockAgent, MockAgentRun } from "./mock_agent_run.js";
|
|
31
32
|
export type { AppWorkflows, AppWorkflowResults, AppQueries, AppQueryColumns, AppAgents, AppAgentResults } from "./types.js";
|
|
32
33
|
export { row, readLinks, readFiles, readLocked, readCreatedAt, readUpdatedAt, PENDING } from "./row.js";
|
|
33
34
|
export type { ResolvedLink, AppFile } from "./row.js";
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,10 @@ function getMockWorkflow(alias) {
|
|
|
59
59
|
if (!isMockMode()) return null;
|
|
60
60
|
return registeredFixture?.workflows?.[alias] ?? null;
|
|
61
61
|
}
|
|
62
|
+
function getMockAgent(alias) {
|
|
63
|
+
if (!isMockMode()) return null;
|
|
64
|
+
return registeredFixture?.agents?.[alias] ?? null;
|
|
65
|
+
}
|
|
62
66
|
function getMockExport(alias) {
|
|
63
67
|
if (!isMockMode()) return null;
|
|
64
68
|
return registeredFixture?.exports?.[alias] ?? null;
|
|
@@ -91,8 +95,8 @@ function reportAppError(kind, error51) {
|
|
|
91
95
|
function firstFrame(error51, stack) {
|
|
92
96
|
const header = String(error51);
|
|
93
97
|
const frames = stack.startsWith(header) ? stack.slice(header.length) : stack;
|
|
94
|
-
const
|
|
95
|
-
return
|
|
98
|
+
const frame2 = frames.split("\n").find((line) => line.trim() !== "");
|
|
99
|
+
return frame2 === void 0 ? null : withoutOrigins(frame2.trim()).slice(0, MESSAGE_LIMIT);
|
|
96
100
|
}
|
|
97
101
|
function withoutOrigins(text2) {
|
|
98
102
|
return text2.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^/\s)]+/gi, "").replace(/[?#][^\s)]*?(?=(?::\d+){1,2}\)?(?:\s|$)|[\s)]|$)/g, "");
|
|
@@ -387,6 +391,8 @@ function reduceAgentChunk(state, chunk) {
|
|
|
387
391
|
return { ...state, parts: updateTool(state.parts, chunk.toolCallId, (p) => ({ type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-available", input: p.input, output: chunk.output })) };
|
|
388
392
|
case "tool-output-error":
|
|
389
393
|
return { ...state, parts: updateTool(state.parts, chunk.toolCallId, (p) => ({ type: "dynamic-tool", toolName: p.toolName, toolCallId: p.toolCallId, state: "output-error", input: p.input, errorText: chunk.errorText ?? "The tool failed." })) };
|
|
394
|
+
case "data-run-output":
|
|
395
|
+
return chunk.data !== null && typeof chunk.data === "object" ? { ...state, output: chunk.data } : state;
|
|
390
396
|
case "error":
|
|
391
397
|
return { ...state, status: "error", error: chunk.errorText ?? "The run failed." };
|
|
392
398
|
case "abort":
|
|
@@ -415,8 +421,8 @@ function parseSseChunks(buffer) {
|
|
|
415
421
|
const chunks = [];
|
|
416
422
|
const frames = buffer.split("\n\n");
|
|
417
423
|
const rest = frames.pop() ?? "";
|
|
418
|
-
for (const
|
|
419
|
-
for (const line of
|
|
424
|
+
for (const frame2 of frames) {
|
|
425
|
+
for (const line of frame2.split("\n")) {
|
|
420
426
|
const trimmed = line.startsWith("data:") ? line.slice(5).trim() : "";
|
|
421
427
|
if (!trimmed || trimmed === "[DONE]") continue;
|
|
422
428
|
try {
|
|
@@ -444,6 +450,39 @@ function landingOf(state) {
|
|
|
444
450
|
}
|
|
445
451
|
}
|
|
446
452
|
|
|
453
|
+
// src/mock_agent_run.ts
|
|
454
|
+
var BEAT_MS = 700;
|
|
455
|
+
var frame = (chunk) => `data: ${JSON.stringify(chunk)}
|
|
456
|
+
|
|
457
|
+
`;
|
|
458
|
+
function playMockRun(run2, onText, beat = BEAT_MS) {
|
|
459
|
+
const verdict = run2.error !== void 0 ? frame({ type: "error", errorText: run2.error }) : frame({ type: "data-run-output", data: run2.output ?? {} });
|
|
460
|
+
const beats = [...(run2.steps ?? []).map((line) => frame({ type: "text-delta", delta: `${line}
|
|
461
|
+
|
|
462
|
+
` })), `${verdict}${frame({ type: "finish" })}`];
|
|
463
|
+
let timer;
|
|
464
|
+
let stop = () => {
|
|
465
|
+
};
|
|
466
|
+
const done = new Promise((resolve) => {
|
|
467
|
+
stop = resolve;
|
|
468
|
+
const play = (at2) => {
|
|
469
|
+
timer = setTimeout(() => {
|
|
470
|
+
onText(beats[at2] ?? "");
|
|
471
|
+
if (at2 === beats.length - 1) resolve();
|
|
472
|
+
else play(at2 + 1);
|
|
473
|
+
}, beat);
|
|
474
|
+
};
|
|
475
|
+
play(0);
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
done,
|
|
479
|
+
abort: () => {
|
|
480
|
+
clearTimeout(timer);
|
|
481
|
+
stop();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
447
486
|
// src/store.ts
|
|
448
487
|
import { useCallback as useCallback2, useEffect as useEffect2, useRef, useSyncExternalStore } from "react";
|
|
449
488
|
var slots = /* @__PURE__ */ new Map();
|
|
@@ -25313,12 +25352,12 @@ async function evaluateWorkflowExpression(expr2, ctx) {
|
|
|
25313
25352
|
}
|
|
25314
25353
|
function evaluateLambda(params, body, ctx, items) {
|
|
25315
25354
|
return async (...args) => {
|
|
25316
|
-
const
|
|
25355
|
+
const frame2 = {};
|
|
25317
25356
|
for (let i = 0; i < params.length; i++) {
|
|
25318
|
-
|
|
25357
|
+
frame2[params[i]] = args[i];
|
|
25319
25358
|
}
|
|
25320
25359
|
const stack = ctx.lambda_params ?? [];
|
|
25321
|
-
const newStack = [...stack,
|
|
25360
|
+
const newStack = [...stack, frame2];
|
|
25322
25361
|
const innerCtx = {
|
|
25323
25362
|
...ctx,
|
|
25324
25363
|
lambda_params: newStack,
|
|
@@ -25401,8 +25440,8 @@ function resolveReadRoot(source, ctx) {
|
|
|
25401
25440
|
const stack = ctx.lambda_params;
|
|
25402
25441
|
if (stack !== void 0) {
|
|
25403
25442
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
25404
|
-
const
|
|
25405
|
-
if (source.name in
|
|
25443
|
+
const frame2 = stack[i];
|
|
25444
|
+
if (source.name in frame2) return frame2[source.name];
|
|
25406
25445
|
}
|
|
25407
25446
|
}
|
|
25408
25447
|
throw new WorkflowEvalError(
|
|
@@ -25414,8 +25453,8 @@ function resolveReadRoot(source, ctx) {
|
|
|
25414
25453
|
const stack = ctx.lexical_bindings;
|
|
25415
25454
|
if (stack !== void 0) {
|
|
25416
25455
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
25417
|
-
const
|
|
25418
|
-
if (source.name in
|
|
25456
|
+
const frame2 = stack[i];
|
|
25457
|
+
if (source.name in frame2) return frame2[source.name];
|
|
25419
25458
|
}
|
|
25420
25459
|
}
|
|
25421
25460
|
throw new WorkflowEvalError(
|
|
@@ -25453,8 +25492,8 @@ function resolveForeachFrame(ctx, name, at2) {
|
|
|
25453
25492
|
at: at2
|
|
25454
25493
|
});
|
|
25455
25494
|
}
|
|
25456
|
-
const
|
|
25457
|
-
if (
|
|
25495
|
+
const frame2 = resolveNamedForeachFrame(stack, name);
|
|
25496
|
+
if (frame2 !== void 0) return frame2;
|
|
25458
25497
|
const known = stack.map((f) => f.name).filter((n) => n !== void 0);
|
|
25459
25498
|
throw new WorkflowEvalError(
|
|
25460
25499
|
`Read from ${at2} "${name}", but no enclosing foreach binds that name` + (known.length > 0 ? ` (in scope: ${known.join(", ")})` : ""),
|
|
@@ -27104,15 +27143,15 @@ async function predictRead(walk, step) {
|
|
|
27104
27143
|
if (typeof id !== "string" || record2 === void 0) return unknownAt(walk.ctx.step_outputs, step.id);
|
|
27105
27144
|
walk.ctx.step_outputs[step.id] = { output: modelled(step.id, { id, table_id: record2.table_id, data: knownCells(walk.input.tables, record2.table_id, record2.cells) }) };
|
|
27106
27145
|
}
|
|
27107
|
-
function bindLexical(
|
|
27108
|
-
if (held2 === UNKNOWN) return unknownAt(
|
|
27109
|
-
Object.defineProperty(
|
|
27146
|
+
function bindLexical(frame2, name, held2) {
|
|
27147
|
+
if (held2 === UNKNOWN) return unknownAt(frame2, name);
|
|
27148
|
+
Object.defineProperty(frame2, name, { enumerable: true, configurable: true, writable: true, value: held2 });
|
|
27110
27149
|
}
|
|
27111
27150
|
function setLexical(walk, name, held2) {
|
|
27112
27151
|
const stack = walk.ctx.lexical_bindings ?? [];
|
|
27113
27152
|
for (let at2 = stack.length - 1; at2 >= 0; at2--) {
|
|
27114
|
-
const
|
|
27115
|
-
if (
|
|
27153
|
+
const frame2 = stack[at2];
|
|
27154
|
+
if (frame2 !== void 0 && Object.prototype.hasOwnProperty.call(frame2, name)) return bindLexical(frame2, name, held2);
|
|
27116
27155
|
}
|
|
27117
27156
|
}
|
|
27118
27157
|
async function block(walk, steps) {
|
|
@@ -27179,8 +27218,8 @@ async function run(walk, step) {
|
|
|
27179
27218
|
return;
|
|
27180
27219
|
}
|
|
27181
27220
|
case "let_declare": {
|
|
27182
|
-
const
|
|
27183
|
-
if (
|
|
27221
|
+
const frame2 = walk.ctx.lexical_bindings?.at(-1);
|
|
27222
|
+
if (frame2 !== void 0) bindLexical(frame2, step.name, await attempt(walk, expr(step.initial)));
|
|
27184
27223
|
return;
|
|
27185
27224
|
}
|
|
27186
27225
|
case "assign":
|
|
@@ -28136,10 +28175,11 @@ function useAgentRun(alias) {
|
|
|
28136
28175
|
}
|
|
28137
28176
|
handleRef.current?.abort();
|
|
28138
28177
|
runIdRef.current = null;
|
|
28178
|
+
const mock = getMockAgent(alias);
|
|
28139
28179
|
const inflight = streamLeg(
|
|
28140
|
-
(onText) => rpcAgentRun({ alias, session_id: opts.sessionId, input: input ?? {} }, onText, (runId) => {
|
|
28180
|
+
(onText) => mock === null ? rpcAgentRun({ alias, session_id: opts.sessionId, input: input ?? {} }, onText, (runId) => {
|
|
28141
28181
|
runIdRef.current = runId;
|
|
28142
|
-
}),
|
|
28182
|
+
}) : playMockRun(typeof mock === "function" ? mock(input ?? {}) : mock, onText),
|
|
28143
28183
|
initialAgentRunState()
|
|
28144
28184
|
);
|
|
28145
28185
|
const tracked = inflight.finally(() => {
|
package/dist/mock.d.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Design-time fixtures, active only with BOTH `mount(<App />, { fixture })`
|
|
3
3
|
* and the `?__mock=1` URL param, so demo data in a bundle never answers real
|
|
4
4
|
* traffic. A mocked workflow never runs, so it writes nothing and nothing is
|
|
5
|
-
* drawn ahead of it; a success re-reads every mounted read.
|
|
5
|
+
* drawn ahead of it; a success re-reads every mounted read. A mocked agent is
|
|
6
|
+
* played, never run: its lines, then its result or its error.
|
|
6
7
|
* `useFileUpload` is not mocked.
|
|
7
8
|
*/
|
|
8
9
|
import type { QueryAggregate } from "./shared_types.js";
|
|
9
10
|
import type { UploadedFile, WorkflowResult } from "./hooks.js";
|
|
11
|
+
import type { MockAgent } from "./mock_agent_run.js";
|
|
10
12
|
import type { ExportCall, QuerySortKey } from "./queries.js";
|
|
11
13
|
import type { RecordingState } from "./recording_state.js";
|
|
12
14
|
/** A result, or a function of the inputs — return a slow promise to review the pending state. */
|
|
@@ -29,6 +31,8 @@ export type MockExport = (call: ExportCall & {
|
|
|
29
31
|
export interface AppFixture {
|
|
30
32
|
queries?: Record<string, MockQuery>;
|
|
31
33
|
workflows?: Record<string, MockWorkflow>;
|
|
34
|
+
/** `useAgentRun(alias)` plays the run — or the one made of its input — and never parks. */
|
|
35
|
+
agents?: Record<string, MockAgent>;
|
|
32
36
|
/** By the query exported. */
|
|
33
37
|
exports?: Record<string, MockExport>;
|
|
34
38
|
/** `useRecording(alias)` reads as available; `start`/`stop` change nothing. */
|
|
@@ -41,6 +45,8 @@ export declare function hasMockFlag(): boolean;
|
|
|
41
45
|
export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>> | null;
|
|
42
46
|
/** Null for an unmocked alias, which executes for real. */
|
|
43
47
|
export declare function getMockWorkflow(alias: string): MockWorkflow | null;
|
|
48
|
+
/** Null for an unmocked alias, which runs for real. */
|
|
49
|
+
export declare function getMockAgent(alias: string): MockAgent | null;
|
|
44
50
|
/** Null for an unmocked alias, which the server exports. */
|
|
45
51
|
export declare function getMockExport(alias: string): MockExport | null;
|
|
46
52
|
export declare function getMockRecordings(): Record<string, RecordingState> | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A RUN PLAYED WITHOUT A MODEL, for design time: the lines a run writes as it works, one a beat, then how it ends — the
|
|
3
|
+
* result the server accepted, or the error it failed with — streamed as the server streams a run, so the one reducer
|
|
4
|
+
* that reads a real run reads this one.
|
|
5
|
+
*/
|
|
6
|
+
export interface MockAgentRun {
|
|
7
|
+
/** What the run writes as it works, a line a beat. */
|
|
8
|
+
steps?: readonly string[];
|
|
9
|
+
/** A structured agent's result, as the server hands it once it accepted it. */
|
|
10
|
+
output?: Record<string, unknown>;
|
|
11
|
+
/** The run fails with these words instead, its output never sent. */
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
/** A run, or one made of what the run was handed. */
|
|
15
|
+
export type MockAgent = MockAgentRun | ((input: Record<string, unknown>) => MockAgentRun);
|
|
16
|
+
/**
|
|
17
|
+
* Plays `run` into `onText` as `rpcAgentRun` streams a real one: a line each `beat`, then its verdict — the error, or
|
|
18
|
+
* the accepted output — with `finish`, as the server sends them. `abort` stops it where it stands.
|
|
19
|
+
*/
|
|
20
|
+
export declare function playMockRun(run: MockAgentRun, onText: (chunk: string) => void, beat?: number): {
|
|
21
|
+
done: Promise<void>;
|
|
22
|
+
abort: () => void;
|
|
23
|
+
};
|
package/docs/ai.md
CHANGED
|
@@ -65,7 +65,7 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
65
65
|
| `answerChoice` | `(answers: {value, custom}[]) => Promise<AgentRunLanding<TOutput>>` | Answer the pending question(s) and CONTINUE the run — one entry per question, aligned by index (exactly what `ClarifyWizard`'s `onSubmit` yields). Streams the continuation into the same `parts`; resolves like `run`. Rejects when nothing is pending — and when the server refuses the answer, in which case the pending question is restored for a retry |
|
|
66
66
|
| `parts` | `AgentUIPart[]` | The ordered live transcript as **ai-sdk `UIMessage.parts`** — answer prose, thinking, and tool calls, in stream order. The single source of truth for the feed; hand it straight to `@lotics/ui` `AgentRun` |
|
|
67
67
|
| `text` | `string` | The agent's **answer prose** (every `text` part concatenated), accumulating live. Excludes thinking. For a free-text agent this IS the result |
|
|
68
|
-
| `output` | `TOutput \| undefined` | The structured result once
|
|
68
|
+
| `output` | `TOutput \| undefined` | The structured result the server accepted — read it once `status` is `"completed"`. `undefined` for a free-text agent |
|
|
69
69
|
| `error` | `string \| undefined` | The failure message when `status` is `"error"` |
|
|
70
70
|
|
|
71
71
|
### The `parts` transcript → `@lotics/ui` `AgentRun`
|
|
@@ -78,7 +78,7 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
78
78
|
| `"reasoning"` | `text`, `state` | The agent's thinking — a **distinct part**, kept out of `text`, so `AgentRun` shows it collapsed / revealed on demand. Only produced when the agent's declared model supports thinking (**Sonnet / Opus**, not Haiku) — a Haiku agent never emits reasoning |
|
|
79
79
|
| `"dynamic-tool"` | `toolName`, `toolCallId`, `state` (ai's tool lifecycle — `input-available` → `output-available` / `output-error`), `input`, `output`, `errorText` | One tool call — opens running the moment it fires, settles when its result arrives. `input`/`output` ride along for `AgentRun`'s on-demand reveal, not for the feed row |
|
|
80
80
|
|
|
81
|
-
The terminal `submit_result` call is
|
|
81
|
+
The terminal `submit_result` call is **not** rendered as a tool part (structured agents therefore have no visible final step); `output` is the result the server accepted, sent as the run finishes. Source/file parts aren't emitted by app agents. `parts` is exactly what `@lotics/ui` `AgentRun` renders, so the pairing needs **no adapter and no hand-assembly**:
|
|
82
82
|
|
|
83
83
|
```tsx
|
|
84
84
|
<AgentRun
|
|
@@ -89,7 +89,7 @@ The terminal `submit_result` call is captured into `output`, **not** rendered as
|
|
|
89
89
|
/>
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`AgentRun` renders thinking collapsed, groups consecutive tool calls, and expands each tool's input/output in place on press — all for free. Running it in a bounded container (a dialog, a panel)? Wrap it in `@lotics/ui`'s `FollowScroll` so the container follows the stream instead of letting new content grow below the fold. **Errors surface two ways:** a per-tool failure is an `output-error` part (amber dot, reason in the expanded Error panel — the feed flows on, exactly like a run that retried and recovered); a **breaking** error that killed the run lives in `run.error` (not in `parts`) — pass it as `error` and it renders as a terminal danger row. Never rebuild this feed by hand.
|
|
92
|
+
`AgentRun` renders thinking collapsed, groups consecutive tool calls, and expands each tool's input/output in place on press — all for free. Running it in a bounded container (a dialog, a panel)? Wrap it in `@lotics/ui`'s `FollowScroll` so the container follows the stream instead of letting new content grow below the fold. **Errors surface two ways:** a per-tool failure is an `output-error` part (amber dot, reason in the expanded Error panel — the feed flows on, exactly like a run that retried and recovered); a **breaking** error that killed the run lives in `run.error` (not in `parts`) — pass it as `error` and it renders as a terminal danger row. Never rebuild this feed by hand. To see its live, done and failed states without a run, mock the agent (`fixture.agents`, [runtime](./runtime.md)): the run is played through the same stream reader, never billed.
|
|
93
93
|
|
|
94
94
|
`run.error` is always a **sentence to show a person**, never a payload — on both the ways a run can fail.
|
|
95
95
|
|
|
@@ -139,7 +139,7 @@ answer may park again.
|
|
|
139
139
|
|
|
140
140
|
### `output` typing and the inner-field caveat
|
|
141
141
|
|
|
142
|
-
The server validates the agent's submitted result strictly (unknown keys rejected, required fields present, types checked, `select` values bound to the declared options); an invalid submission is rejected back to the agent with the validation errors, and the agent retries. A run only settles **completed** with a result that passed this — so on a completed run, `output.items.map(...)` on a declared array field is safe. What that does **not** guarantee:
|
|
142
|
+
The server validates the agent's submitted result strictly (unknown keys rejected, required fields present, types checked, `select` values bound to the declared options); an invalid submission is rejected back to the agent with the validation errors, and the agent retries. A submission whose JSON arrived broken is not retried: the agent would rewrite it from memory and drop fields, so the run fails instead. A run only settles **completed** with a result that passed this — so on a completed run, `output.items.map(...)` on a declared array field is safe. What that does **not** guarantee:
|
|
143
143
|
|
|
144
144
|
- a field declared `required: false` may be absent;
|
|
145
145
|
- a `json`-typed output field is passthrough — anything goes inside it;
|
|
@@ -147,21 +147,21 @@ The server validates the agent's submitted result strictly (unknown keys rejecte
|
|
|
147
147
|
timestamp, an address shape) but not for being the *right* value — a parseable date can still
|
|
148
148
|
be the wrong date;
|
|
149
149
|
- every **value** was authored by the model — a schema-valid string can still be semantically wrong;
|
|
150
|
-
-
|
|
150
|
+
- while the run streams, `output` holds the submit call's **arguments** as they arrive, before the server has checked them — only a `completed` run's `output` is the accepted result.
|
|
151
151
|
|
|
152
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.
|
|
153
153
|
|
|
154
|
-
### Errors — one landing
|
|
154
|
+
### Errors — one landing
|
|
155
155
|
|
|
156
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.
|
|
157
157
|
|
|
158
158
|
```tsx
|
|
159
159
|
const landing = await recognize.run(input, { sessionId });
|
|
160
160
|
if (landing.kind === "failed") { /* surface landing.error */ }
|
|
161
|
-
else if (landing.kind === "settled"
|
|
161
|
+
else if (landing.kind === "settled") { /* review landing.output */ }
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
The stream ends on the persisted run's outcome, so the landing and the run never disagree: a structured agent that finished without a valid result, one whose result arrived unreadable, and a free-text agent that produced no text all land `failed`, with the message the run carries (a capped run's "Run exceeded the 20-minute limit." included).
|
|
165
165
|
|
|
166
166
|
### `cancel` vs `abort`
|
|
167
167
|
|
package/docs/queries.md
CHANGED
|
@@ -234,7 +234,7 @@ supports the full §5 operator matrix **except**: `traversal` nodes, `locked`, a
|
|
|
234
234
|
`record_id` works on any row-level derived query (rejected over a `group`, which has no row
|
|
235
235
|
identity). When a `filter` directly wraps a `union` of `project(from_table)` arms and every
|
|
236
236
|
condition targets bare passthrough columns, the engine pushes the predicate into each arm's
|
|
237
|
-
source filter automatically
|
|
237
|
+
source filter automatically, where it runs once per record; otherwise it evaluates post-union.
|
|
238
238
|
|
|
239
239
|
### `join` — combine two row sets
|
|
240
240
|
|
|
@@ -545,9 +545,9 @@ Negation-shaped inner operators (`has_none_of`, `not_equals`, `is_none_of`,
|
|
|
545
545
|
|
|
546
546
|
Traversals are **source-layer only** (rejected on derived columns — push them into
|
|
547
547
|
`from_table.filter`). Each hop reaches the linked rows by primary key from the ids in the link
|
|
548
|
-
cell. Under an OR beside column conditions a traversal runs once per row and takes
|
|
549
|
-
|
|
550
|
-
|
|
548
|
+
cell. Under an OR beside column conditions a traversal runs once per row and takes a membership
|
|
549
|
+
condition off its index with it; give it a `union` arm of its own when the other arms must keep
|
|
550
|
+
theirs. Two access classes:
|
|
551
551
|
|
|
552
552
|
- **Self-scoped** — inner operator `is_current_member` / `is_not_current_member`: tests only
|
|
553
553
|
the viewer's own membership on the linked row, leaks nothing, and is exempt from the linked
|
|
@@ -871,7 +871,7 @@ ordering and scoping into the template or its params.
|
|
|
871
871
|
Any other execution failure returns a generic `query execution failed` (the real error — which
|
|
872
872
|
may embed SQL — is server-logged only). Bind-time and validation errors are always specific.
|
|
873
873
|
|
|
874
|
-
###
|
|
874
|
+
### What a query reads, in plain terms
|
|
875
875
|
|
|
876
876
|
Records are stored partitioned by workspace, so a query reads only its own workspace's slice; a
|
|
877
877
|
`from_table` narrows that slice to the table's rows, and within them:
|
|
@@ -879,39 +879,18 @@ Records are stored partitioned by workspace, so a query reads only its own works
|
|
|
879
879
|
- **GIN-served (fast at any size):** *positive* exact-membership filters at the **source
|
|
880
880
|
layer** — `select`/`select_member`/`select_record_link` `has_any_of` / `has_all_of`, and
|
|
881
881
|
`is_current_member`. These compile to containment the JSONB GIN index serves.
|
|
882
|
-
- **Trigram-served:** `from_table.search` (§7).
|
|
883
|
-
- **
|
|
884
|
-
comparisons
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
template. Fine on thousands of rows; on very large tables these dominate latency and are the
|
|
895
|
-
usual timeout cause.
|
|
896
|
-
|
|
897
|
-
**Filter shape drives latency.** Equality/range/sort predicates in the bound template are
|
|
898
|
-
index-served; lead with those or a GIN-served membership filter / `search`, and let scan-shaped
|
|
899
|
-
predicates refine the already-narrowed set. Derived-layer filters run over the subquery result
|
|
900
|
-
(no index), so **filter at the source layer whenever the field exists there** — the runtime
|
|
901
|
-
filter is for caller-driven refinement, not for the main cut (a runtime-only field gets no
|
|
902
|
-
managed index). (The engine pushes eligible filter-over-union predicates down automatically,
|
|
903
|
-
but don't rely on that for other shapes.)
|
|
904
|
-
|
|
905
|
-
**A sorted page reads in its sort index — declare the order it is read in.** A page sorted by one
|
|
906
|
-
column that passes a number or date field unchanged (a bare `project` source with no `type`
|
|
907
|
-
override, an `unpivot` passthrough, or a row column that reads the same field in every row) reads
|
|
908
|
-
the table in that field's sort index and stops at the page, instead of reading every row and
|
|
909
|
-
sorting. Over a `union`, each arm is paged this way on its own and the arms are merged, so a
|
|
910
|
-
register over several tables or `unpivot` sides is fast at any size — don't hand-split it into
|
|
911
|
-
per-table queries you merge client-side. The index exists when a bound template sorts that field
|
|
912
|
-
in the same direction and blank position: give the query its default order as a top-level `sort`,
|
|
913
|
-
and a page the app sorts the same way is served. A text column, a computed, literal or cast column,
|
|
914
|
-
a sort on more than one key, and a cursor (`keyset`) page sort every row.
|
|
882
|
+
- **Trigram-served:** `from_table.search` (§7), within the one table.
|
|
883
|
+
- **Every row of the table:** everything else — text equality and `contains`, number and date
|
|
884
|
+
comparisons, sorts, negations, emptiness, files predicates. Fine on thousands of rows; the cost
|
|
885
|
+
grows with the table, and on very large tables these dominate latency and are the usual
|
|
886
|
+
timeout cause.
|
|
887
|
+
|
|
888
|
+
**Filter shape drives latency.** Lead with a GIN-served membership filter or `search`, which
|
|
889
|
+
narrow the table before its rows are read, and let the other conditions refine that set.
|
|
890
|
+
Derived-layer filters run over the subquery result, after any `unpivot` fan-out, so **filter at
|
|
891
|
+
the source layer whenever the field exists there** — the runtime filter is for caller-driven
|
|
892
|
+
refinement, not for the main cut. (The engine moves eligible filter-over-union predicates into
|
|
893
|
+
each arm's source automatically, but don't rely on that for other shapes.)
|
|
915
894
|
|
|
916
895
|
**An aggregate arm you cannot filter costs its whole table, every execution.** A `join` whose right
|
|
917
896
|
side is a `group` over entire tables, keyed on a value the LEFT side supplies at run time, has no
|
package/docs/runtime.md
CHANGED
|
@@ -54,6 +54,12 @@ mount(<App />, {
|
|
|
54
54
|
? { status: "error", message: "Row 2 has no customer." }
|
|
55
55
|
: new Promise((r) => setTimeout(() => r({ status: "success" }), 1200)),
|
|
56
56
|
},
|
|
57
|
+
agents: {
|
|
58
|
+
// A run's lines, then the result it lands — or a FUNCTION of the run's input.
|
|
59
|
+
recognize: { steps: ["Reading the invoice."], output: { total: 1250 } },
|
|
60
|
+
// Or how it fails.
|
|
61
|
+
triage: { error: "The run ended without a result. Run it again." },
|
|
62
|
+
},
|
|
57
63
|
},
|
|
58
64
|
});
|
|
59
65
|
```
|
|
@@ -62,7 +68,7 @@ Activation is a **two-step gate** — both must hold, so demo data shipping in t
|
|
|
62
68
|
bundle never leaks into normal traffic:
|
|
63
69
|
|
|
64
70
|
1. A fixture is registered via `mount({ fixture })` (`AppFixture` type:
|
|
65
|
-
`dist/mock.d.ts` — `{ queries?, workflows?, exports?, recordings? }`).
|
|
71
|
+
`dist/mock.d.ts` — `{ queries?, workflows?, agents?, exports?, recordings? }`).
|
|
66
72
|
2. The page URL carries `?__mock=1` (exactly `1`). Without the flag the fixture
|
|
67
73
|
is completely inert.
|
|
68
74
|
|
|
@@ -99,12 +105,17 @@ pending state. Calling `mount` again (HMR) replaces the registration last-write-
|
|
|
99
105
|
**Reach the in-flight state with a function** that resolves on a timer: an app
|
|
100
106
|
whose only AI surface is a workflow calling `agent(...)` otherwise has no
|
|
101
107
|
non-billing path to its own thinking / done / error screens.
|
|
108
|
+
- **A mocked agent is PLAYED, never run.** `useAgentRun(alias)` streams the
|
|
109
|
+
fixture's `steps` a line a beat, then lands its `output` — or fails with its
|
|
110
|
+
`error` — through the same stream reader a real run goes through, so the pane's
|
|
111
|
+
live, done and failed states are reviewable with no spend. It never parks on a
|
|
112
|
+
question, and there is no run to cancel on the server.
|
|
102
113
|
- **A mocked recording is a canned state.** `recordings: { log_visit: { phase:
|
|
103
114
|
"live", elapsed_seconds: 754, inputs: { site: "rec_1" } } }` makes
|
|
104
115
|
`useRecording("log_visit")` available with that `state`; `start` and `stop`
|
|
105
116
|
resolve without recording anything or moving the state.
|
|
106
|
-
- **Not mocked:** uploads, `useFieldOptions`, members, comments
|
|
107
|
-
|
|
117
|
+
- **Not mocked:** uploads, `useFieldOptions`, members, comments. In mock mode
|
|
118
|
+
those still hit the real transport.
|
|
108
119
|
- **Analytics is disabled** whenever `?__mock=1` is present, fixture or not — a
|
|
109
120
|
screenshot/design-time load emits no events.
|
|
110
121
|
- The `__mock` param-name prefix is reserved by the SDK; don't use it for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.102.
|
|
3
|
+
"version": "0.102.2",
|
|
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": {
|