@lotics/app-sdk 0.60.3 → 0.61.1
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/dist/src/agent_stream.d.ts +0 -2
- package/dist/src/agent_stream.js +18 -2
- package/dist/src/rpc.d.ts +2 -0
- package/dist/src/rpc.js +51 -1
- package/docs/ai.md +3 -1
- package/package.json +2 -2
|
@@ -104,8 +104,6 @@ export interface PendingChoice {
|
|
|
104
104
|
toolCallId: string;
|
|
105
105
|
questions: ChoiceQuestion[];
|
|
106
106
|
}
|
|
107
|
-
/** The run's pending `ask_user_choice`, parsed for rendering (a `ClarifyWizard`
|
|
108
|
-
* maps 1:1) — non-null exactly while the run is parked awaiting the answer. */
|
|
109
107
|
export declare function pendingInteractiveCall(state: AgentRunState): PendingChoice | null;
|
|
110
108
|
/**
|
|
111
109
|
* Fold the user's picks into the tool's output shape — `answers` aligns to
|
package/dist/src/agent_stream.js
CHANGED
|
@@ -66,6 +66,15 @@ export function adoptSettledRun(state, settled) {
|
|
|
66
66
|
}
|
|
67
67
|
/** The run's pending `ask_user_choice`, parsed for rendering (a `ClarifyWizard`
|
|
68
68
|
* maps 1:1) — non-null exactly while the run is parked awaiting the answer. */
|
|
69
|
+
/** Untrusted model output — a non-JSON string is data, not an exception. */
|
|
70
|
+
function decodeJson(raw) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
export function pendingInteractiveCall(state) {
|
|
70
79
|
if (state.status !== "awaiting_input")
|
|
71
80
|
return null;
|
|
@@ -81,9 +90,16 @@ export function pendingInteractiveCall(state) {
|
|
|
81
90
|
return null;
|
|
82
91
|
}
|
|
83
92
|
function parseChoiceQuestions(input) {
|
|
84
|
-
|
|
93
|
+
// A model may hand the tool its arguments as a JSON STRING rather than an
|
|
94
|
+
// object. The SERVER accepts either and parks the run, so refusing the string
|
|
95
|
+
// here strands a live `awaiting_input` run: the question exists, the run is
|
|
96
|
+
// answerable, and the UI reports it as unrecoverable. Parse before validating
|
|
97
|
+
// — a string that isn't JSON falls through to the same empty result as any
|
|
98
|
+
// other malformed input, which `adoptGuarded` surfaces loudly and retryably.
|
|
99
|
+
const decoded = typeof input === "string" ? decodeJson(input) : input;
|
|
100
|
+
if (!decoded || typeof decoded !== "object")
|
|
85
101
|
return [];
|
|
86
|
-
const record =
|
|
102
|
+
const record = decoded;
|
|
87
103
|
if (!Array.isArray(record.questions))
|
|
88
104
|
return [];
|
|
89
105
|
const questions = [];
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -170,6 +170,8 @@ export declare function rpcAgentRunContinue(payload: AgentRunContinuePayload, on
|
|
|
170
170
|
* `APP_PUBLIC_SESSION_HEADER`; both sides are pinned by tests.
|
|
171
171
|
*/
|
|
172
172
|
export declare const APP_PUBLIC_SESSION_HEADER = "x-lotics-app-session";
|
|
173
|
+
/** The run token header — mirrored server-side by `APP_AGENT_RUN_TOKEN_HEADER`. */
|
|
174
|
+
export declare const APP_AGENT_RUN_TOKEN_HEADER = "x-app-agent-run-token";
|
|
173
175
|
/**
|
|
174
176
|
* The error message for a non-ok response. A genuine JSON error (a 4xx carrying
|
|
175
177
|
* a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
|
package/dist/src/rpc.js
CHANGED
|
@@ -250,6 +250,11 @@ export function rpcAgentRunContinue(payload, onText) {
|
|
|
250
250
|
const headers = { "content-type": "application/json" };
|
|
251
251
|
if (sessionToken)
|
|
252
252
|
headers[APP_PUBLIC_SESSION_HEADER] = sessionToken;
|
|
253
|
+
// Continuing an anonymous parked run is authorized by the same per-run
|
|
254
|
+
// capability that reads it — there is no member to authorize instead.
|
|
255
|
+
const continueToken = runTokens.get(payload.run_id);
|
|
256
|
+
if (continueToken)
|
|
257
|
+
headers[APP_AGENT_RUN_TOKEN_HEADER] = continueToken;
|
|
253
258
|
const res = await fetch(`${API_BASE}/v1/apps/${app_id}/agent-runs/${encodeURIComponent(payload.run_id)}/continue`, {
|
|
254
259
|
method: "POST",
|
|
255
260
|
headers,
|
|
@@ -297,8 +302,12 @@ function agentRunStandalone(payload, onText, onRunId) {
|
|
|
297
302
|
throw await streamStartError(res);
|
|
298
303
|
}
|
|
299
304
|
const runId = res.headers.get("x-app-agent-run-id");
|
|
300
|
-
if (runId)
|
|
305
|
+
if (runId) {
|
|
306
|
+
// Anonymous runs carry their retrieval capability here; a member run does
|
|
307
|
+
// not, and `rememberRunToken` no-ops on the null.
|
|
308
|
+
rememberRunToken(runId, res.headers.get(APP_AGENT_RUN_TOKEN_HEADER));
|
|
301
309
|
onRunId?.(runId);
|
|
310
|
+
}
|
|
302
311
|
const reader = res.body.getReader();
|
|
303
312
|
const decoder = new TextDecoder();
|
|
304
313
|
try {
|
|
@@ -343,6 +352,40 @@ let sessionToken = null;
|
|
|
343
352
|
* `APP_PUBLIC_SESSION_HEADER`; both sides are pinned by tests.
|
|
344
353
|
*/
|
|
345
354
|
export const APP_PUBLIC_SESSION_HEADER = "x-lotics-app-session";
|
|
355
|
+
/**
|
|
356
|
+
* Per-run retrieval credentials for ANONYMOUS agent runs, keyed by run id.
|
|
357
|
+
*
|
|
358
|
+
* A member's run is authorized by their identity, so reading it back needs
|
|
359
|
+
* nothing extra. An anonymous run on a publicly-shared app has no member to key
|
|
360
|
+
* on (`triggered_by_member_id` is null by design), so the server mints a
|
|
361
|
+
* capability token bound to that one run and returns it on the run response.
|
|
362
|
+
* Without it the recovery poll 404s and a completed answer is lost to any
|
|
363
|
+
* dropped connection — routine on mobile for a run measured in tens of seconds.
|
|
364
|
+
*
|
|
365
|
+
* Transport-level, like `sessionToken` above and for the same reason: it is a
|
|
366
|
+
* credential, not app state. Threading it through the hook would put a bearer
|
|
367
|
+
* into the app's typed surface for every app to forward by hand.
|
|
368
|
+
*
|
|
369
|
+
* Only the STANDALONE transport needs this — an embedded app always runs under
|
|
370
|
+
* a member session, so it never produces an anonymous run.
|
|
371
|
+
*/
|
|
372
|
+
const runTokens = new Map();
|
|
373
|
+
/** Bound the map so a long-lived page cannot accumulate tokens without limit.
|
|
374
|
+
* Insertion-ordered, so the oldest entry is the first key. */
|
|
375
|
+
const MAX_TRACKED_RUN_TOKENS = 8;
|
|
376
|
+
function rememberRunToken(runId, token) {
|
|
377
|
+
if (!token)
|
|
378
|
+
return;
|
|
379
|
+
runTokens.set(runId, token);
|
|
380
|
+
while (runTokens.size > MAX_TRACKED_RUN_TOKENS) {
|
|
381
|
+
const oldest = runTokens.keys().next();
|
|
382
|
+
if (oldest.done)
|
|
383
|
+
break;
|
|
384
|
+
runTokens.delete(oldest.value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** The run token header — mirrored server-side by `APP_AGENT_RUN_TOKEN_HEADER`. */
|
|
388
|
+
export const APP_AGENT_RUN_TOKEN_HEADER = "x-app-agent-run-token";
|
|
346
389
|
/**
|
|
347
390
|
* The cookie the app-host Worker sets after the visitor clears the password
|
|
348
391
|
* gate. Readable by design — the SDK forwards its value as the session header
|
|
@@ -473,6 +516,11 @@ async function apiCall(method, path, body, opts) {
|
|
|
473
516
|
if (sessionToken && !opts?.skipAuth) {
|
|
474
517
|
headers[APP_PUBLIC_SESSION_HEADER] = sessionToken;
|
|
475
518
|
}
|
|
519
|
+
// The per-run capability for an ANONYMOUS run — the only thing that authorizes
|
|
520
|
+
// reading or cancelling it, since there is no member identity to check.
|
|
521
|
+
if (opts?.runToken) {
|
|
522
|
+
headers[APP_AGENT_RUN_TOKEN_HEADER] = opts.runToken;
|
|
523
|
+
}
|
|
476
524
|
const controller = new AbortController();
|
|
477
525
|
let didTimeout = false;
|
|
478
526
|
const timeoutId = setTimeout(() => {
|
|
@@ -716,6 +764,7 @@ async function standaloneAgentRunGet(p) {
|
|
|
716
764
|
const { app_id } = await boot();
|
|
717
765
|
const r = (await apiCall("GET", `/v1/apps/${app_id}/agent-runs/${encodeURIComponent(p.run_id)}`, undefined, {
|
|
718
766
|
appId: app_id,
|
|
767
|
+
runToken: runTokens.get(p.run_id),
|
|
719
768
|
}));
|
|
720
769
|
return { run: r.run };
|
|
721
770
|
}
|
|
@@ -725,6 +774,7 @@ async function standaloneAgentRunCancel(p) {
|
|
|
725
774
|
const { app_id } = await boot();
|
|
726
775
|
await apiCall("POST", `/v1/apps/${app_id}/agent-runs/${encodeURIComponent(p.run_id)}/cancel`, {}, {
|
|
727
776
|
appId: app_id,
|
|
777
|
+
runToken: runTokens.get(p.run_id),
|
|
728
778
|
});
|
|
729
779
|
return { ok: true };
|
|
730
780
|
}
|
package/docs/ai.md
CHANGED
|
@@ -21,7 +21,7 @@ A declaration carries:
|
|
|
21
21
|
|---|---|
|
|
22
22
|
| `instructions` | System instructions — the task the agent performs per run |
|
|
23
23
|
| `tool_names` | The tools the agent may call, resolved against the platform's automation tool registry. The capability boundary for everything EXCEPT workspace data — the run can use nothing else. May be empty — including for an agent that reads documents, since a [`file` input carries its own content](#file-inputs--what-the-agent-can-actually-see) |
|
|
24
|
-
| `knowledge_doc_ids` | The knowledge docs the agent may read
|
|
24
|
+
| `knowledge_doc_ids` | The knowledge docs the agent may read, and the whole set it can reach — a doc absent from this list is unreadable even if the agent names its id. Declare `grep_knowledge` / `read_knowledge` in `tool_names` to read them. There is no size limit and nothing is inlined, so a multi-megabyte reference corpus (a full tariff, a regulation set) is a normal declaration. Reach for `code_exec` only to COMPUTE across the corpus — counting, cross-referencing — never merely to read it |
|
|
25
25
|
| `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records |
|
|
26
26
|
| `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
|
|
27
27
|
| `model_id` | Optional chat model pin. Omit (preferred) to follow the platform default chat model, resolved at run time — the agent tracks model generations with no rewrite. Pin only a deliberate, tested choice |
|
|
@@ -184,6 +184,8 @@ The run's lifetime is decoupled from the stream: the server drives it to complet
|
|
|
184
184
|
|
|
185
185
|
Both truncation shapes emit the `app_agent_stream_truncated` analytics event (`kind: "connection_error" | "clean_end"`, with whether the row rescued the result), so edge-cut frequency is visible fleet-wide.
|
|
186
186
|
|
|
187
|
+
**Anonymous runs poll with a capability token, handled for you.** On a publicly-shared app a visitor has no member identity, so the server cannot authorize their poll by ownership — `triggered_by_member_id` is null by design. Instead the run response carries a per-run token (`x-app-agent-run-token`); the SDK stores it against that run id and replays it on the poll, on `cancel`, and on `answerChoice`. It is transport-level, like the password-session token — never surfaced to app code, and never issued for a member run (identity already authorizes those). Nothing to wire: an app calls `useAgentRun` the same way on both. The token is bound to ONE run, so it cannot read another visitor's, and it is why a dropped connection on a public app recovers rather than losing the answer.
|
|
188
|
+
|
|
187
189
|
The poll is bounded at 22 minutes — deliberately PAST the server's 20-minute hard run cap, so a live run always settles before the client gives up. A row still `running` at the deadline means the run's process died mid-flight (e.g. a crash that skipped the server's shutdown drain); the poll surfaces an error and the server's reaper repairs the row. Only when no run id was ever received (the run never started) does the failure reject before any polling.
|
|
188
190
|
|
|
189
191
|
On recovery, `output` adopts the row's output **only when it is an object** (a structured result) — the "`output` is never a stray string" rule holds on every path. A **free-text** run recovered from truncation keeps only the streamed prefix in `text`; the full settled answer is persisted server-side but is not currently client-readable (`useAgentRuns` can't reach it on any transport — see the limitation below), so treat the streamed prefix as terminal for now.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Runtime SDK for Lotics custom-code apps
|
|
3
|
+
"version": "0.61.1",
|
|
4
|
+
"description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|