@lotics/app-sdk 0.59.1 → 0.59.4

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/rpc.d.ts CHANGED
@@ -178,6 +178,16 @@ export declare const APP_PUBLIC_SESSION_HEADER = "x-lotics-app-session";
178
178
  * `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
179
179
  */
180
180
  export declare function transportErrorMessage(status: number, parsed: unknown): string;
181
+ /**
182
+ * The error a stream that never started should throw.
183
+ *
184
+ * A streaming endpoint fails BEFORE the first byte like any other request — a 402 when the
185
+ * workspace is out of credits, a 403, a 404. Those bodies are structured errors, and throwing
186
+ * the body TEXT put the whole JSON on screen: `{"message":"Credit quota exceeded…","error":
187
+ * "quota_exceeded","plan_id":"free",…}`. Same rule as every other call: the `message` is the
188
+ * message, and a non-JSON or 5xx body never becomes one.
189
+ */
190
+ export declare function streamStartError(res: Response): Promise<Error>;
181
191
  /**
182
192
  * A package installation's alias→concrete-id maps — what the generated
183
193
  * `.lotics/app_fields.ts` of a package project resolves `F`/`OPT`/`ROLE`
package/dist/src/rpc.js CHANGED
@@ -257,8 +257,7 @@ export function rpcAgentRunContinue(payload, onText) {
257
257
  signal: controller.signal,
258
258
  });
259
259
  if (!res.ok || !res.body) {
260
- const text = await res.text().catch(() => "");
261
- throw new Error(text || `HTTP ${res.status}`);
260
+ throw await streamStartError(res);
262
261
  }
263
262
  const reader = res.body.getReader();
264
263
  const decoder = new TextDecoder();
@@ -295,8 +294,7 @@ function agentRunStandalone(payload, onText, onRunId) {
295
294
  signal: controller.signal,
296
295
  });
297
296
  if (!res.ok || !res.body) {
298
- const text = await res.text().catch(() => "");
299
- throw new Error(text || `HTTP ${res.status}`);
297
+ throw await streamStartError(res);
300
298
  }
301
299
  const runId = res.headers.get("x-app-agent-run-id");
302
300
  if (runId)
@@ -410,8 +408,11 @@ async function boot() {
410
408
  * A user-facing message for a transport/gateway failure — derived from the HTTP
411
409
  * status, never from the response body. A 524 (Cloudflare edge timeout on a long
412
410
  * run), any 5xx, or a non-JSON body (an HTML error page) must NOT surface its raw
413
- * body as the error message. Kept in parity (by value, no shared dep) with the
414
- * dev-loop transport in `packages/sdk/src/client.ts`.
411
+ * body as the error message.
412
+ *
413
+ * A by-value MIRROR of `@lotics/shared/transport_error`, which is canonical. This
414
+ * package ships to npm with zero internal dependencies, so it cannot import it;
415
+ * every other transport does. Change one, change both.
415
416
  */
416
417
  function gatewayErrorMessage(status) {
417
418
  if (status === 524) {
@@ -437,6 +438,28 @@ export function transportErrorMessage(status, parsed) {
437
438
  ? gatewayErrorMessage(status)
438
439
  : jsonMessage;
439
440
  }
441
+ /**
442
+ * The error a stream that never started should throw.
443
+ *
444
+ * A streaming endpoint fails BEFORE the first byte like any other request — a 402 when the
445
+ * workspace is out of credits, a 403, a 404. Those bodies are structured errors, and throwing
446
+ * the body TEXT put the whole JSON on screen: `{"message":"Credit quota exceeded…","error":
447
+ * "quota_exceeded","plan_id":"free",…}`. Same rule as every other call: the `message` is the
448
+ * message, and a non-JSON or 5xx body never becomes one.
449
+ */
450
+ export async function streamStartError(res) {
451
+ const text = await res.text().catch(() => "");
452
+ let parsed = null;
453
+ if (text) {
454
+ try {
455
+ parsed = JSON.parse(text);
456
+ }
457
+ catch {
458
+ // not JSON — a gateway HTML page; transportErrorMessage will not surface it
459
+ }
460
+ }
461
+ return new Error(transportErrorMessage(res.status, parsed));
462
+ }
440
463
  // Standalone requests carried no timeout: a hung or slow backend pinned the
441
464
  // fetch — and one of the browser's few per-origin connections — indefinitely,
442
465
  // surfacing as an app that "loads forever". Bound every standalone call the way
package/docs/ai.md CHANGED
@@ -90,6 +90,16 @@ The terminal `submit_result` call is captured into `output`, **not** rendered as
90
90
 
91
91
  `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
92
 
93
+ `run.error` is always a **sentence to show a person**, never a payload — on both the ways a run can fail.
94
+
95
+ *Before it starts*: the request is refused (most often a 402 when the workspace is out of AI credits — "Credit quota exceeded. Upgrade your plan for continued AI access." — or a 403/404). You get that response's `message` alone; the rest of the body (`error`, `plan_id`, `credits_used`, …) never reaches the string, and a non-JSON body or any 5xx is replaced by a status-derived message.
96
+
97
+ *After it starts*: the run dies mid-flight, and the message is written **in the triggering member's language** (the workspace default when an anonymous visitor triggered it). An error the platform wrote surfaces as-is, so running out of credits **during** a run still says to upgrade rather than to retry. A provider or tool failure does NOT — its wording is a debugging string that names our request shape, so it becomes one of two sentences: the request itself was rejected (an attachment that is corrupt or too large — retrying unchanged cannot help), or it broke transiently and is worth another run. The raw error is kept in error tracking with the run id.
98
+
99
+ Two caveats on that language guarantee. A message written at its throw site rather than fixed on its error class stays English — those strings are also what the chat agent reads to correct its own tool calls, so they are deliberately one language. And the *before it starts* case above is the API response's own `message`, which is not localized either. So an app that must be wholly in one language should render its own copy on failure rather than the platform's.
100
+
101
+ So `error={run.error}` is safe to render verbatim. What it is NOT is machine-readable: `status` tells you a run failed, not why, and the difference between "worth retrying" and "retrying cannot help" lives in the sentence rather than in a code. **Do not parse it** — the wording is copy and will change. If your app needs to act on the distinction (a retry button that disables itself when a retry is futile, an upgrade prompt on a quota hit), that discriminator can be exposed; it is classified server-side already and simply is not on the wire yet.
102
+
93
103
  ### The agent asks back — `pendingChoice` / `answerChoice`
94
104
 
95
105
  Every app agent carries the platform's MANDATORY interactive tool `ask_user_choice` —
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.59.1",
4
- "description": "Runtime SDK for Lotics custom-code apps typed hooks, postMessage bridge, mount entry point",
3
+ "version": "0.59.4",
4
+ "description": "Runtime SDK for Lotics custom-code apps \u2014 typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {