@lotics/app-sdk 0.41.0 → 0.42.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/AGENTS.md +8 -2
- package/dist/src/hooks.d.ts +6 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/rpc.d.ts +8 -0
- package/dist/src/rpc.js +44 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -39,7 +39,10 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
|
|
|
39
39
|
- **Mutate (the ONLY write path)** — **`useWorkflow(alias)`** → an async fn returning a typed
|
|
40
40
|
`WorkflowResult` `{ status, message?, files?, data? }`. `data` is whatever the workflow
|
|
41
41
|
`return({ data })`'d (typed automatically from the alias contract). After a known mutation,
|
|
42
|
-
call the owning query's `refetch()`.
|
|
42
|
+
call the owning query's `refetch()`. Handle failures by checking `result.status === "error"`,
|
|
43
|
+
not just `try/catch`: a transport/gateway failure (a 524 timeout on a long run, any 5xx, a
|
|
44
|
+
non-JSON error page) **resolves** with `{ status: "error", message }` (a friendly, body-free
|
|
45
|
+
message) — it does not throw an HTML body.
|
|
43
46
|
- **Upload a file** — **`useFileUpload()`** → `{ upload, uploading, error }`. Mints a presigned URL
|
|
44
47
|
and PUTs the bytes straight to storage; the file is inert until a workflow attaches it to a
|
|
45
48
|
`files` field. Emits `app_file_uploaded`.
|
|
@@ -52,7 +55,10 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
|
|
|
52
55
|
to a `DisplayFile` (snake → camel — `mime_type`→`mimeType`, `preview_url`→`url` — the app owns this
|
|
53
56
|
data→UI adapter; the SDK never imports `@lotics/ui`) for
|
|
54
57
|
`<FileThumbnail file={{ id, filename, mimeType, url }} uploading={f.status === "uploading"} />`;
|
|
55
|
-
`sendDisabled` gates on `uploading` and the send payload is `fileIds`.
|
|
58
|
+
`sendDisabled` gates on `uploading` and the send payload is `fileIds`. To persist several files into ONE
|
|
59
|
+
record, declare the workflow input `{ type: "file", multi: true }` — the body receives `fileIds` as
|
|
60
|
+
`ReadonlyArray<FileId>` and writes it straight to a `files` field (a single `file` input is one id → wrap `[id]`).
|
|
61
|
+
Don't hand-roll
|
|
56
62
|
`createObjectURL`/upload/revoke per app. For a full add-files SCREEN (not the composer pill), map
|
|
57
63
|
each `AttachedFile` to a `FileUpload` (ready → `{ status: "complete", id, file }`, else
|
|
58
64
|
`{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`) and feed `@lotics/ui`
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -164,6 +164,12 @@ type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] exte
|
|
|
164
164
|
*
|
|
165
165
|
* `data` is the structured value the workflow returned via `return({ data })`,
|
|
166
166
|
* typed per the alias's declared `outputs` schema (`unknown` when none was declared).
|
|
167
|
+
*
|
|
168
|
+
* A transport/gateway failure (a Cloudflare 524 timeout on a long run, any 5xx,
|
|
169
|
+
* or a non-JSON error page) **resolves** with `{ status: "error", message }` —
|
|
170
|
+
* a body-free, friendly message — rather than rejecting with a raw HTML body.
|
|
171
|
+
* So an app handles every failure (handled workflow error AND transport error)
|
|
172
|
+
* by checking `result.status === "error"`; it never receives gateway HTML.
|
|
167
173
|
*/
|
|
168
174
|
export interface WorkflowResult<TData = unknown> {
|
|
169
175
|
status: "success" | "error";
|
package/dist/src/hooks.js
CHANGED
|
@@ -26,7 +26,7 @@ export function useWorkflow(alias) {
|
|
|
26
26
|
return useCallback(async (inputs) => {
|
|
27
27
|
try {
|
|
28
28
|
const result = await rpc("workflow", { alias, inputs: inputs ?? {} });
|
|
29
|
-
captureAppEvent("app_workflow_run", { alias, ok:
|
|
29
|
+
captureAppEvent("app_workflow_run", { alias, ok: result.status !== "error" });
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
32
|
catch (err) {
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -86,3 +86,11 @@ export declare function subscribeUrlParams(cb: (params: UrlParams) => void): ()
|
|
|
86
86
|
* standalone: the SDK reads the public endpoint's body directly.
|
|
87
87
|
*/
|
|
88
88
|
export declare function rpcAgentRun(payload: AgentRunPayload, onText: (chunk: string) => void, onRunId?: (runId: string) => void): AgentRunHandle;
|
|
89
|
+
/**
|
|
90
|
+
* The error message for a non-ok response. A genuine JSON error (a 4xx carrying
|
|
91
|
+
* a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
|
|
92
|
+
* 5xx, or a JSON body without a `message` falls back to a body-free,
|
|
93
|
+
* status-derived message — so a raw HTML body never becomes the message.
|
|
94
|
+
* `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
|
|
95
|
+
*/
|
|
96
|
+
export declare function transportErrorMessage(status: number, parsed: unknown): string;
|
package/dist/src/rpc.js
CHANGED
|
@@ -334,6 +334,37 @@ async function acquireSessionToken(appId) {
|
|
|
334
334
|
},
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* A user-facing message for a transport/gateway failure — derived from the HTTP
|
|
339
|
+
* status, never from the response body. A 524 (Cloudflare edge timeout on a long
|
|
340
|
+
* run), any 5xx, or a non-JSON body (an HTML error page) must NOT surface its raw
|
|
341
|
+
* body as the error message. Kept in parity (by value, no shared dep) with the
|
|
342
|
+
* dev-loop transport in `packages/sdk/src/client.ts`.
|
|
343
|
+
*/
|
|
344
|
+
function gatewayErrorMessage(status) {
|
|
345
|
+
if (status === 524) {
|
|
346
|
+
return "The request took too long to finish (gateway timeout). It may still be running — check back in a moment, or try again.";
|
|
347
|
+
}
|
|
348
|
+
if (status >= 500) {
|
|
349
|
+
return "The service is temporarily unavailable. Please try again shortly.";
|
|
350
|
+
}
|
|
351
|
+
return "The service returned an unexpected response. Please try again.";
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The error message for a non-ok response. A genuine JSON error (a 4xx carrying
|
|
355
|
+
* a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
|
|
356
|
+
* 5xx, or a JSON body without a `message` falls back to a body-free,
|
|
357
|
+
* status-derived message — so a raw HTML body never becomes the message.
|
|
358
|
+
* `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
|
|
359
|
+
*/
|
|
360
|
+
export function transportErrorMessage(status, parsed) {
|
|
361
|
+
const jsonMessage = parsed && typeof parsed.message === "string"
|
|
362
|
+
? parsed.message
|
|
363
|
+
: null;
|
|
364
|
+
return parsed === null || status >= 500 || jsonMessage === null
|
|
365
|
+
? gatewayErrorMessage(status)
|
|
366
|
+
: jsonMessage;
|
|
367
|
+
}
|
|
337
368
|
async function apiCall(method, path, body, opts) {
|
|
338
369
|
const headers = {};
|
|
339
370
|
if (body)
|
|
@@ -368,8 +399,9 @@ async function apiCall(method, path, body, opts) {
|
|
|
368
399
|
await acquireSessionToken(appId);
|
|
369
400
|
return apiCall(method, path, body, { ...opts, appId });
|
|
370
401
|
}
|
|
371
|
-
|
|
372
|
-
|
|
402
|
+
// Never surface a non-JSON body (a gateway HTML error page) or a 5xx body as
|
|
403
|
+
// the message — emit a body-free, status-derived message instead.
|
|
404
|
+
throw new Error(transportErrorMessage(res.status, parsed));
|
|
373
405
|
}
|
|
374
406
|
return parsed ?? (text ? text : {});
|
|
375
407
|
}
|
|
@@ -480,7 +512,16 @@ async function standaloneFieldOptions(p) {
|
|
|
480
512
|
}
|
|
481
513
|
async function standaloneWorkflow(p) {
|
|
482
514
|
const { app_id } = await boot();
|
|
483
|
-
|
|
515
|
+
try {
|
|
516
|
+
return await apiCall("POST", `/v1/apps/${app_id}/workflows/${encodeURIComponent(p.alias)}/execute`, { inputs: p.inputs }, { appId: app_id });
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
// A transport/gateway failure resolves to a WorkflowResult error (never a
|
|
520
|
+
// rejection carrying a raw body) so an app reads `result.status === "error"`
|
|
521
|
+
// uniformly with a handled workflow error. `apiCall` already sanitized the
|
|
522
|
+
// message, so it never contains an HTML body.
|
|
523
|
+
return { status: "error", message: err instanceof Error ? err.message : "The workflow failed to run." };
|
|
524
|
+
}
|
|
484
525
|
}
|
|
485
526
|
async function standaloneAgentRuns(p) {
|
|
486
527
|
const { app_id } = await boot();
|