@lotics/app-sdk 0.102.1 → 0.102.3
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 +65 -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/mutations.md +3 -2
- package/docs/queries.md +20 -1
- 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();
|
|
@@ -23787,6 +23826,9 @@ var queryAggregateColumnSchema = zod_default.object({
|
|
|
23787
23826
|
input_column: zod_default.string().optional().describe(
|
|
23788
23827
|
"Input column to aggregate. Required for non-count operations; ignored for count."
|
|
23789
23828
|
),
|
|
23829
|
+
filter: tableRecordFiltersSchema.optional().describe(
|
|
23830
|
+
"Aggregates only the input rows that match (SQL `FILTER`). Conditions name input columns and take `{{params.x}}`; aggregates with different filters in one node read the input once."
|
|
23831
|
+
),
|
|
23790
23832
|
separator: zod_default.string().max(8).optional().describe('string_agg only. Joins the values. Defaults to ", ".'),
|
|
23791
23833
|
distinct: zod_default.boolean().optional().describe(
|
|
23792
23834
|
"string_agg only. Collapses repeats \u2014 a group of containers sized 40HC/40HC/20DC aggregates to '20DC, 40HC'. Defaults to true, which is almost always what a summary column wants."
|
|
@@ -25313,12 +25355,12 @@ async function evaluateWorkflowExpression(expr2, ctx) {
|
|
|
25313
25355
|
}
|
|
25314
25356
|
function evaluateLambda(params, body, ctx, items) {
|
|
25315
25357
|
return async (...args) => {
|
|
25316
|
-
const
|
|
25358
|
+
const frame2 = {};
|
|
25317
25359
|
for (let i = 0; i < params.length; i++) {
|
|
25318
|
-
|
|
25360
|
+
frame2[params[i]] = args[i];
|
|
25319
25361
|
}
|
|
25320
25362
|
const stack = ctx.lambda_params ?? [];
|
|
25321
|
-
const newStack = [...stack,
|
|
25363
|
+
const newStack = [...stack, frame2];
|
|
25322
25364
|
const innerCtx = {
|
|
25323
25365
|
...ctx,
|
|
25324
25366
|
lambda_params: newStack,
|
|
@@ -25401,8 +25443,8 @@ function resolveReadRoot(source, ctx) {
|
|
|
25401
25443
|
const stack = ctx.lambda_params;
|
|
25402
25444
|
if (stack !== void 0) {
|
|
25403
25445
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
25404
|
-
const
|
|
25405
|
-
if (source.name in
|
|
25446
|
+
const frame2 = stack[i];
|
|
25447
|
+
if (source.name in frame2) return frame2[source.name];
|
|
25406
25448
|
}
|
|
25407
25449
|
}
|
|
25408
25450
|
throw new WorkflowEvalError(
|
|
@@ -25414,8 +25456,8 @@ function resolveReadRoot(source, ctx) {
|
|
|
25414
25456
|
const stack = ctx.lexical_bindings;
|
|
25415
25457
|
if (stack !== void 0) {
|
|
25416
25458
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
25417
|
-
const
|
|
25418
|
-
if (source.name in
|
|
25459
|
+
const frame2 = stack[i];
|
|
25460
|
+
if (source.name in frame2) return frame2[source.name];
|
|
25419
25461
|
}
|
|
25420
25462
|
}
|
|
25421
25463
|
throw new WorkflowEvalError(
|
|
@@ -25453,8 +25495,8 @@ function resolveForeachFrame(ctx, name, at2) {
|
|
|
25453
25495
|
at: at2
|
|
25454
25496
|
});
|
|
25455
25497
|
}
|
|
25456
|
-
const
|
|
25457
|
-
if (
|
|
25498
|
+
const frame2 = resolveNamedForeachFrame(stack, name);
|
|
25499
|
+
if (frame2 !== void 0) return frame2;
|
|
25458
25500
|
const known = stack.map((f) => f.name).filter((n) => n !== void 0);
|
|
25459
25501
|
throw new WorkflowEvalError(
|
|
25460
25502
|
`Read from ${at2} "${name}", but no enclosing foreach binds that name` + (known.length > 0 ? ` (in scope: ${known.join(", ")})` : ""),
|
|
@@ -27104,15 +27146,15 @@ async function predictRead(walk, step) {
|
|
|
27104
27146
|
if (typeof id !== "string" || record2 === void 0) return unknownAt(walk.ctx.step_outputs, step.id);
|
|
27105
27147
|
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
27148
|
}
|
|
27107
|
-
function bindLexical(
|
|
27108
|
-
if (held2 === UNKNOWN) return unknownAt(
|
|
27109
|
-
Object.defineProperty(
|
|
27149
|
+
function bindLexical(frame2, name, held2) {
|
|
27150
|
+
if (held2 === UNKNOWN) return unknownAt(frame2, name);
|
|
27151
|
+
Object.defineProperty(frame2, name, { enumerable: true, configurable: true, writable: true, value: held2 });
|
|
27110
27152
|
}
|
|
27111
27153
|
function setLexical(walk, name, held2) {
|
|
27112
27154
|
const stack = walk.ctx.lexical_bindings ?? [];
|
|
27113
27155
|
for (let at2 = stack.length - 1; at2 >= 0; at2--) {
|
|
27114
|
-
const
|
|
27115
|
-
if (
|
|
27156
|
+
const frame2 = stack[at2];
|
|
27157
|
+
if (frame2 !== void 0 && Object.prototype.hasOwnProperty.call(frame2, name)) return bindLexical(frame2, name, held2);
|
|
27116
27158
|
}
|
|
27117
27159
|
}
|
|
27118
27160
|
async function block(walk, steps) {
|
|
@@ -27179,8 +27221,8 @@ async function run(walk, step) {
|
|
|
27179
27221
|
return;
|
|
27180
27222
|
}
|
|
27181
27223
|
case "let_declare": {
|
|
27182
|
-
const
|
|
27183
|
-
if (
|
|
27224
|
+
const frame2 = walk.ctx.lexical_bindings?.at(-1);
|
|
27225
|
+
if (frame2 !== void 0) bindLexical(frame2, step.name, await attempt(walk, expr(step.initial)));
|
|
27184
27226
|
return;
|
|
27185
27227
|
}
|
|
27186
27228
|
case "assign":
|
|
@@ -28136,10 +28178,11 @@ function useAgentRun(alias) {
|
|
|
28136
28178
|
}
|
|
28137
28179
|
handleRef.current?.abort();
|
|
28138
28180
|
runIdRef.current = null;
|
|
28181
|
+
const mock = getMockAgent(alias);
|
|
28139
28182
|
const inflight = streamLeg(
|
|
28140
|
-
(onText) => rpcAgentRun({ alias, session_id: opts.sessionId, input: input ?? {} }, onText, (runId) => {
|
|
28183
|
+
(onText) => mock === null ? rpcAgentRun({ alias, session_id: opts.sessionId, input: input ?? {} }, onText, (runId) => {
|
|
28141
28184
|
runIdRef.current = runId;
|
|
28142
|
-
}),
|
|
28185
|
+
}) : playMockRun(typeof mock === "function" ? mock(input ?? {}) : mock, onText),
|
|
28143
28186
|
initialAgentRunState()
|
|
28144
28187
|
);
|
|
28145
28188
|
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/mutations.md
CHANGED
|
@@ -531,8 +531,9 @@ every read before the request answers:
|
|
|
531
531
|
- **Totals.** A `total`, a count per value (`total: { by }`), a group asked with `aggregate` or
|
|
532
532
|
declared by the query, and a parent's count or sum rollup move by exactly what the written row adds
|
|
533
533
|
or takes away; a group the last row leaves is gone, and a count per value gains the value a row
|
|
534
|
-
first holds. A mean, an extreme, a distinct count, a group by day,
|
|
535
|
-
counted in yet, a read whose rows a search or a cap
|
|
534
|
+
first holds. A mean, an extreme, a distinct count, a group by day, an aggregate with its own
|
|
535
|
+
`filter`, a group none of the read's rows counted in yet, a read whose rows a search or a cap
|
|
536
|
+
decides, or a row the app never read is left
|
|
536
537
|
as the server said it until a read asked after the write answers.
|
|
537
538
|
- **A refusal takes it back**, and `result` says why, as always. A success keeps it until a read
|
|
538
539
|
asked after the write settled answers — the stored value then replaces the prediction, whatever
|
package/docs/queries.md
CHANGED
|
@@ -277,7 +277,7 @@ came from.
|
|
|
277
277
|
|
|
278
278
|
Full semantics in §8. `by` may be empty (a single-row aggregate); `aggregates` needs ≥ 1 entry.
|
|
279
279
|
Aggregate operation × input-column type is validated at bind. Grouping collapses rows —
|
|
280
|
-
addressing is dropped.
|
|
280
|
+
addressing is dropped. An aggregate's `filter` limits it to the rows that match (§8).
|
|
281
281
|
|
|
282
282
|
### `window` — aggregate without collapsing
|
|
283
283
|
|
|
@@ -679,6 +679,25 @@ everything else requires an `input_column` whose type must be compatible — che
|
|
|
679
679
|
¹ opaque `json` columns support only the presence-counting six (`empty`/`filled`/`unique` and
|
|
680
680
|
their `percent_*` forms).
|
|
681
681
|
|
|
682
|
+
**`filter` — aggregate a subset.** Any aggregate, in `group` or `window`, takes a `filter` over its
|
|
683
|
+
input columns — the same tree as a `filter` node, `{{params.x}}` included, with a condition on an
|
|
684
|
+
omitted optional param dropped. The aggregate reads only the rows it matches (SQL `FILTER`), and
|
|
685
|
+
the rest of the node is unaffected, so several figures over one table cost one read of it:
|
|
686
|
+
|
|
687
|
+
```jsonc
|
|
688
|
+
{ "kind": "group", "from": { "kind": "from_table", "table_id": "…" }, "by": [],
|
|
689
|
+
"aggregates": [
|
|
690
|
+
{ "output": "open", "type": "number", "operation": "count",
|
|
691
|
+
"filter": { "node_type": "condition", "field_key": "closed_at", "operator": "is_empty" } },
|
|
692
|
+
{ "output": "closed_today", "type": "number", "operation": "count",
|
|
693
|
+
"filter": { "node_type": "condition", "field_key": "closed_at", "operator": "on",
|
|
694
|
+
"value": { "type": "period", "period": "day", "boundary": "start", "offset": 0 } } } ] }
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
A relative date resolves in its field's timezone while the column still names one field — over a
|
|
698
|
+
`from_table`, or a `project` of one. Over a `union` of different tables it names none; count per
|
|
699
|
+
table, then combine.
|
|
700
|
+
|
|
682
701
|
**`string_agg` — a summary column, not a dataset.** Every other operation counts or reduces to a
|
|
683
702
|
number; this one joins the values, so a child set answers "which ones?" in the parent row (the
|
|
684
703
|
sizes on a shipment, the tags on a ticket) without a second query.
|
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.3",
|
|
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": {
|