@lotics/app-sdk 0.42.1 → 0.43.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 +17 -0
- package/dist/src/agent_stream.d.ts +43 -9
- package/dist/src/agent_stream.js +56 -14
- package/dist/src/hooks.d.ts +26 -10
- package/dist/src/hooks.js +15 -3
- package/dist/src/index.d.ts +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -74,6 +74,23 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
|
|
|
74
74
|
- **Who's viewing** — **`useViewer()`** → the signed-in member (the *viewed* member under "View as").
|
|
75
75
|
Display-only — personalize or attribute; **never** scope rows by it (use an `is_current_member`
|
|
76
76
|
query filter server-side). Returns null for an anonymous public visitor.
|
|
77
|
+
- **Run an AI agent** — **`useAgentRun(alias)`** → `{ run, cancel, abort, status, items, text, steps,
|
|
78
|
+
output, error }` (agent declared in `package.json#lotics.agents`, authored via `set_app_agent`).
|
|
79
|
+
`run(input, { sessionId })` streams it. TWO kinds, both first-class: a **structured** agent (declares
|
|
80
|
+
`outputs`) lands its typed result in **`output`** (the `submit_result` shape) — `undefined` if the run
|
|
81
|
+
produced none, **never a stray string** (so a present `output` is the declared shape; still validate
|
|
82
|
+
untrusted INNER fields the model authored, e.g. an array that may be missing/mistyped); a **free-text**
|
|
83
|
+
agent's answer is the transcript prose (**`text`**). Feed the live **`items`** transcript straight to
|
|
84
|
+
`@lotics/ui` `AgentRun` — `<AgentRun items={run.items} state={run.status === "streaming" ? "streaming"
|
|
85
|
+
: run.status === "error" ? "error" : "done"} labelForTool={…} />` — NO hand-assembly (`text`/`steps`
|
|
86
|
+
are backward-compat views of `items`). The transcript carries the agent's **thinking** (a distinct
|
|
87
|
+
`reasoning` segment, kept OUT of `text`) and each tool's **input / output / running→done→error state**;
|
|
88
|
+
`AgentRun` shows thinking collapsed and tool I/O in a press-to-open peek (revealed on demand, not in the
|
|
89
|
+
feed) — all for free, no extra wiring. Wire a Stop button to **`cancel()`** (stops the run server-side,
|
|
90
|
+
saves tokens; `abort()` is local-only, for unmount). `run()` THROWS on quota/network — try/catch +
|
|
91
|
+
surface. Needs an authenticated member (embedded apps only, no anonymous runs). Image/PDF `file`
|
|
92
|
+
inputs are materialized into the agent's vision — pair with `useFileUpload`. `items`/`cancel` need
|
|
93
|
+
`@lotics/app-sdk` ≥ 0.43; the reasoning + per-tool I/O reveal needs `@lotics/ui` ≥ 7.13.
|
|
77
94
|
- **Device location** — **`requestGeofencedLocation(zones)`** → a structured outcome
|
|
78
95
|
`{ ok:true, coords } | { ok:false, reason:"denied"|"unavailable"|"outside" }`; **`isWithinZone`**
|
|
79
96
|
is the pure check. Read directly in the iframe (host delegates the permission); the gate is
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
* matter for a run feed and fold them into `AgentRunState`. Unknown chunk types
|
|
9
9
|
* are ignored, so an AI-SDK version that adds chunk kinds never breaks the SDK.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Chunks fold into an ordered `items` transcript — answer prose, thinking
|
|
12
|
+
* (`reasoning`, kept a distinct segment so the UI can collapse it), and tool steps
|
|
13
|
+
* (each carrying its `input`/`output`/`status` for an on-demand reveal), in stream
|
|
14
|
+
* order — that feeds `@lotics/ui` `AgentRun` directly. An agent can be either kind:
|
|
15
|
+
* a STRUCTURED agent (declares `outputs`) emits its result as the input of the
|
|
16
|
+
* injected `submit_result` tool → `output`; a FREE-TEXT agent's result is its prose
|
|
17
|
+
* → the transcript's text. `output` is NEVER the free-text (see
|
|
18
|
+
* `AgentRunState.output`). Pure + reducer-shaped so it is fully unit-testable
|
|
19
|
+
* against recorded frames — no live model needed.
|
|
15
20
|
*/
|
|
16
21
|
export interface AgentRunStep {
|
|
17
22
|
id: string;
|
|
@@ -19,14 +24,42 @@ export interface AgentRunStep {
|
|
|
19
24
|
detail?: string;
|
|
20
25
|
status: "running" | "done" | "error";
|
|
21
26
|
kind?: "step" | "tool";
|
|
27
|
+
/** The tool call's input (arguments). Carried for on-demand reveal, NOT shown in
|
|
28
|
+
* the feed — `@lotics/ui` `AgentRun` renders it in a press-to-open peek. */
|
|
29
|
+
input?: unknown;
|
|
30
|
+
/** The tool call's result, once it returns. On-demand reveal, same as `input`. */
|
|
31
|
+
output?: unknown;
|
|
32
|
+
/** The tool failure message, when `status` is `"error"`. */
|
|
33
|
+
errorText?: string;
|
|
22
34
|
}
|
|
35
|
+
/** One entry in the ordered run transcript, in the order it streamed: the agent's
|
|
36
|
+
* answer prose (`text`), its thinking (`reasoning` — shown collapsed, revealed on
|
|
37
|
+
* demand), or a tool it ran (`step`). This is structurally the `AgentRunItem` that
|
|
38
|
+
* `@lotics/ui`'s `AgentRun` takes, so `<AgentRun items={run.items} state={…} />`
|
|
39
|
+
* needs no adapter — the SDK can't import the UI type (it must stay off the
|
|
40
|
+
* react-native-web dep tree), it mirrors the shape. */
|
|
41
|
+
export type AgentRunItem = {
|
|
42
|
+
type: "text";
|
|
43
|
+
id: string;
|
|
44
|
+
text: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "reasoning";
|
|
47
|
+
id: string;
|
|
48
|
+
text: string;
|
|
49
|
+
} | ({
|
|
50
|
+
type: "step";
|
|
51
|
+
} & AgentRunStep);
|
|
23
52
|
export interface AgentRunState {
|
|
24
53
|
status: "streaming" | "completed" | "error";
|
|
25
|
-
/** The
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** The structured result — `submit_result`'s input
|
|
54
|
+
/** The ordered transcript — prose interleaved with the tool steps, as it
|
|
55
|
+
* streamed. The single source of truth for the feed; `useAgentRun` derives the
|
|
56
|
+
* backward-compat `text` / `steps` from it. */
|
|
57
|
+
items: AgentRunItem[];
|
|
58
|
+
/** The structured result — the `submit_result` tool's input — or `undefined`.
|
|
59
|
+
* NEVER the free-text answer: a free-text agent (or a structured one that
|
|
60
|
+
* finished without submitting) has no `output`; its answer is the transcript's
|
|
61
|
+
* text. Coercing text here would hand a consumer reading `output.<field>` a bare
|
|
62
|
+
* string, crashing it ("... is not iterable"). */
|
|
30
63
|
output?: unknown;
|
|
31
64
|
error?: string;
|
|
32
65
|
}
|
|
@@ -38,6 +71,7 @@ interface Chunk {
|
|
|
38
71
|
toolName?: string;
|
|
39
72
|
toolCallId?: string;
|
|
40
73
|
input?: unknown;
|
|
74
|
+
output?: unknown;
|
|
41
75
|
errorText?: string;
|
|
42
76
|
finishReason?: string;
|
|
43
77
|
}
|
package/dist/src/agent_stream.js
CHANGED
|
@@ -8,45 +8,87 @@
|
|
|
8
8
|
* matter for a run feed and fold them into `AgentRunState`. Unknown chunk types
|
|
9
9
|
* are ignored, so an AI-SDK version that adds chunk kinds never breaks the SDK.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Chunks fold into an ordered `items` transcript — answer prose, thinking
|
|
12
|
+
* (`reasoning`, kept a distinct segment so the UI can collapse it), and tool steps
|
|
13
|
+
* (each carrying its `input`/`output`/`status` for an on-demand reveal), in stream
|
|
14
|
+
* order — that feeds `@lotics/ui` `AgentRun` directly. An agent can be either kind:
|
|
15
|
+
* a STRUCTURED agent (declares `outputs`) emits its result as the input of the
|
|
16
|
+
* injected `submit_result` tool → `output`; a FREE-TEXT agent's result is its prose
|
|
17
|
+
* → the transcript's text. `output` is NEVER the free-text (see
|
|
18
|
+
* `AgentRunState.output`). Pure + reducer-shaped so it is fully unit-testable
|
|
19
|
+
* against recorded frames — no live model needed.
|
|
15
20
|
*/
|
|
16
21
|
/** The tool the backend injects to carry a typed structured result. */
|
|
17
22
|
const SUBMIT_TOOL = "submit_result";
|
|
18
23
|
export function initialAgentRunState() {
|
|
19
|
-
return { status: "streaming",
|
|
24
|
+
return { status: "streaming", items: [] };
|
|
25
|
+
}
|
|
26
|
+
/** Grow the trailing prose segment of the given kind, or open a new one (after a
|
|
27
|
+
* tool ran, or when the kind flips text↔reasoning) — so the transcript interleaves
|
|
28
|
+
* answer prose, thinking, and tools in the order they streamed. */
|
|
29
|
+
function appendProse(items, kind, piece) {
|
|
30
|
+
const last = items[items.length - 1];
|
|
31
|
+
if (last?.type === kind) {
|
|
32
|
+
return [...items.slice(0, -1), { ...last, text: last.text + piece }];
|
|
33
|
+
}
|
|
34
|
+
return [...items, { type: kind, id: `${kind}-${items.length}`, text: piece }];
|
|
35
|
+
}
|
|
36
|
+
/** Apply an update to the tool step with the given call id (by identity), leaving
|
|
37
|
+
* the rest untouched. A missing id (older frame) or no match is a no-op. */
|
|
38
|
+
function updateStep(items, toolCallId, fn) {
|
|
39
|
+
if (!toolCallId)
|
|
40
|
+
return items;
|
|
41
|
+
return items.map((it) => (it.type === "step" && it.id === toolCallId ? fn(it) : it));
|
|
20
42
|
}
|
|
21
43
|
/** Fold one chunk into the run state. Returns a new state (immutable). */
|
|
22
44
|
export function reduceAgentChunk(state, chunk) {
|
|
23
45
|
switch (chunk.type) {
|
|
24
|
-
case "text-delta":
|
|
46
|
+
case "text-delta": {
|
|
47
|
+
const piece = chunk.delta ?? "";
|
|
48
|
+
return piece ? { ...state, items: appendProse(state.items, "text", piece) } : state;
|
|
49
|
+
}
|
|
25
50
|
case "reasoning-delta": {
|
|
51
|
+
// Thinking is its OWN segment (not folded into the answer prose) so the UI can
|
|
52
|
+
// keep it collapsed / revealed-on-demand rather than inline in the answer.
|
|
26
53
|
const piece = chunk.delta ?? "";
|
|
27
|
-
return piece ? { ...state,
|
|
54
|
+
return piece ? { ...state, items: appendProse(state.items, "reasoning", piece) } : state;
|
|
28
55
|
}
|
|
56
|
+
case "tool-input-start":
|
|
29
57
|
case "tool-input-available": {
|
|
30
58
|
const name = chunk.toolName ?? "tool";
|
|
31
59
|
if (name === SUBMIT_TOOL) {
|
|
32
|
-
// The agent emitted its structured result.
|
|
60
|
+
// The agent emitted its structured result — not a transcript step.
|
|
33
61
|
return { ...state, output: chunk.input };
|
|
34
62
|
}
|
|
63
|
+
const id = chunk.toolCallId ?? `${name}-${state.items.length}`;
|
|
64
|
+
// Upsert by call id: the step opens "running" the moment the call fires and
|
|
65
|
+
// settles when its output arrives (below), so the feed shows a live tool, not
|
|
66
|
+
// an instantly-done one. `input` rides along for the on-demand peek.
|
|
67
|
+
const existing = state.items.some((it) => it.type === "step" && it.id === id);
|
|
68
|
+
if (existing) {
|
|
69
|
+
return { ...state, items: updateStep(state.items, id, (s) => ({ ...s, label: s.label || name, input: chunk.input ?? s.input })) };
|
|
70
|
+
}
|
|
35
71
|
return {
|
|
36
72
|
...state,
|
|
37
|
-
|
|
38
|
-
...state.steps,
|
|
39
|
-
{ id: chunk.toolCallId ?? `${name}-${state.steps.length}`, label: name, kind: "tool", status: "done" },
|
|
40
|
-
],
|
|
73
|
+
items: [...state.items, { type: "step", id, label: name, kind: "tool", status: "running", input: chunk.input }],
|
|
41
74
|
};
|
|
42
75
|
}
|
|
76
|
+
case "tool-output-available":
|
|
77
|
+
return { ...state, items: updateStep(state.items, chunk.toolCallId, (s) => ({ ...s, status: "done", output: chunk.output })) };
|
|
78
|
+
case "tool-output-error":
|
|
79
|
+
return { ...state, items: updateStep(state.items, chunk.toolCallId, (s) => ({ ...s, status: "error", errorText: chunk.errorText ?? "The tool failed." })) };
|
|
43
80
|
case "error":
|
|
44
81
|
return { ...state, status: "error", error: chunk.errorText ?? "The run failed." };
|
|
45
82
|
case "abort":
|
|
46
83
|
return { ...state, status: state.status === "streaming" ? "error" : state.status, error: state.error ?? "The run was stopped." };
|
|
47
84
|
case "finish": {
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
// Settle the status, and settle any tool still "running" (its output frame
|
|
86
|
+
// never came) to "done" so nothing spins forever. `output` is whatever
|
|
87
|
+
// `submit_result` set (else undefined) — never the accumulated text: a
|
|
88
|
+
// structured-output consumer reads `output.<field>`, so a stray free-text
|
|
89
|
+
// string there would crash it. The free-text answer is always in `text`.
|
|
90
|
+
const items = state.items.map((it) => (it.type === "step" && it.status === "running" ? { ...it, status: "done" } : it));
|
|
91
|
+
return { ...state, status: state.status === "error" ? "error" : "completed", items };
|
|
50
92
|
}
|
|
51
93
|
default:
|
|
52
94
|
return state;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type AgentRunStep } from "./agent_stream.js";
|
|
1
|
+
import { type AgentRunStep, type AgentRunItem } from "./agent_stream.js";
|
|
2
2
|
import type { AppWorkflows, AppWorkflowResults, AppQueries, AppAgents, AppAgentResults } from "./types.js";
|
|
3
3
|
import type { ResolvedMember } from "./members.js";
|
|
4
4
|
import type { ResolvedOption } from "./select.js";
|
|
5
|
-
export type { AgentRunStep, AgentRunState } from "./agent_stream.js";
|
|
5
|
+
export type { AgentRunStep, AgentRunState, AgentRunItem } from "./agent_stream.js";
|
|
6
6
|
/** Fields shared by every query hook's return value. */
|
|
7
7
|
interface QueryStateBase {
|
|
8
8
|
/**
|
|
@@ -427,25 +427,41 @@ export interface UseAgentRun<TInput, TOutput> {
|
|
|
427
427
|
* user-facing "Stop" button to this, not `abort`. */
|
|
428
428
|
cancel: () => void;
|
|
429
429
|
status: "idle" | "streaming" | "completed" | "error";
|
|
430
|
-
/** The
|
|
430
|
+
/** The ordered run transcript — prose interleaved with tool steps, in stream
|
|
431
|
+
* order. Feed straight to `@lotics/ui` `AgentRun`:
|
|
432
|
+
* `<AgentRun items={run.items} state={run.status === "streaming" ? "streaming" : run.status === "error" ? "error" : "done"} />`
|
|
433
|
+
* — no hand-assembly. */
|
|
434
|
+
items: AgentRunItem[];
|
|
435
|
+
/** The agent's ANSWER prose (every `text` segment concatenated), accumulating
|
|
436
|
+
* live — excludes thinking (`reasoning` is its own segment in `items`). For a
|
|
437
|
+
* FREE-TEXT agent this IS the result; a structured agent's result is `output`.
|
|
438
|
+
* Derived from `items`. */
|
|
431
439
|
text: string;
|
|
432
|
-
/**
|
|
440
|
+
/** Only the tool/step items (each with its `input`/`output`/`status`) — backward
|
|
441
|
+
* compat with the older steps-only `AgentRun`. Prefer `items` (it keeps the
|
|
442
|
+
* text↔reasoning↔tool ordering the feed renders). Derived from `items`. */
|
|
433
443
|
steps: AgentRunStep[];
|
|
434
|
-
/** The
|
|
444
|
+
/** The structured result once the run completes — the agent's `submit_result`
|
|
445
|
+
* output. `undefined` when the run produced none (a free-text agent, or one that
|
|
446
|
+
* finished without submitting); a free-text answer lives in `text`, never here.
|
|
447
|
+
* So it is safe to treat a present `output` as the declared shape — but still
|
|
448
|
+
* validate untrusted inner fields (the model authored the `submit_result` body,
|
|
449
|
+
* so e.g. an array field may be missing/mistyped). */
|
|
435
450
|
output?: TOutput;
|
|
436
451
|
error?: string;
|
|
437
452
|
}
|
|
438
453
|
/**
|
|
439
454
|
* Run a streaming agent declared in `package.json` lotics.agents and invoked by
|
|
440
|
-
* alias. `run(input, { sessionId })` starts it; progress streams into `status`
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* session's history with `useAgentRuns`.
|
|
455
|
+
* alias. `run(input, { sessionId })` starts it; progress streams into `status` +
|
|
456
|
+
* the ordered `items` transcript (feed straight to `@lotics/ui` `AgentRun`). A
|
|
457
|
+
* STRUCTURED agent's result lands in `output`; a FREE-TEXT agent's answer is the
|
|
458
|
+
* transcript's prose (`text`). Read the session's history with `useAgentRuns`.
|
|
444
459
|
*
|
|
445
460
|
* ```tsx
|
|
446
461
|
* const recognize = useAgentRun("recognize");
|
|
447
462
|
* await recognize.run({ image_file_id }, { sessionId });
|
|
448
|
-
* // recognize.
|
|
463
|
+
* // <AgentRun items={recognize.items} state={recognize.status === "streaming" ? "streaming" : "done"} />
|
|
464
|
+
* // then read recognize.output (structured) or recognize.text (free-text)
|
|
449
465
|
* ```
|
|
450
466
|
*/
|
|
451
467
|
export declare function useAgentRun<K extends keyof AppAgents & string>(alias: K): UseAgentRun<AppAgents[K], AgentOutputOf<K>>;
|
package/dist/src/hooks.js
CHANGED
|
@@ -406,8 +406,11 @@ export function useAgentRun(alias) {
|
|
|
406
406
|
if (aborted)
|
|
407
407
|
return undefined;
|
|
408
408
|
// `finish` normally settles status; guard a stream that ended without one.
|
|
409
|
+
// Keep `output` as whatever `submit_result` set (else undefined) — never
|
|
410
|
+
// coerce the free-text into it (that string would crash a structured
|
|
411
|
+
// consumer reading `output.<field>`; the answer stays in `text`).
|
|
409
412
|
if (acc.status === "streaming") {
|
|
410
|
-
acc = { ...acc, status: "completed"
|
|
413
|
+
acc = { ...acc, status: "completed" };
|
|
411
414
|
safeSetState(acc);
|
|
412
415
|
}
|
|
413
416
|
captureAppEvent("app_agent_run", { alias, ok: acc.status !== "error" });
|
|
@@ -448,17 +451,26 @@ export function useAgentRun(alias) {
|
|
|
448
451
|
void rpc("agentRun.cancel", { run_id: runId }).catch(() => { });
|
|
449
452
|
handleRef.current?.abort();
|
|
450
453
|
}, []);
|
|
454
|
+
// `items` is the source of truth; `text` (all prose) and `steps` (tool items
|
|
455
|
+
// only) are the backward-compat views derived from it.
|
|
456
|
+
const items = state?.items ?? EMPTY_ITEMS;
|
|
457
|
+
const text = useMemo(() => items.reduce((acc, i) => (i.type === "text" ? acc + i.text : acc), ""), [items]);
|
|
458
|
+
const steps = useMemo(() => items.flatMap((i) => (i.type === "step" ? [{ id: i.id, label: i.label, detail: i.detail, status: i.status, kind: i.kind, input: i.input, output: i.output, errorText: i.errorText }] : [])), [items]);
|
|
451
459
|
return {
|
|
452
460
|
run,
|
|
453
461
|
abort,
|
|
454
462
|
cancel,
|
|
455
463
|
status: state?.status ?? "idle",
|
|
456
|
-
|
|
457
|
-
|
|
464
|
+
items,
|
|
465
|
+
text,
|
|
466
|
+
steps,
|
|
458
467
|
output: state?.output,
|
|
459
468
|
error: state?.error,
|
|
460
469
|
};
|
|
461
470
|
}
|
|
471
|
+
/** Stable empty transcript so an idle hook returns a constant `items` reference
|
|
472
|
+
* (no new [] each render → dependents don't re-run needlessly). */
|
|
473
|
+
const EMPTY_ITEMS = [];
|
|
462
474
|
/**
|
|
463
475
|
* Poll a single run until it leaves `running` — the resume path when a run's
|
|
464
476
|
* stream connection drops mid-flight. Bounded just past the server-side max-run
|
package/dist/src/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
18
|
export type { MountOptions } from "./mount.js";
|
|
19
19
|
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
20
|
-
export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunRecord, AgentRunState, AgentRunStep, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
20
|
+
export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunRecord, AgentRunState, AgentRunStep, AgentRunItem, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
21
21
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
22
22
|
export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
23
23
|
export { useViewer } from "./viewer.js";
|