@lotics/app-sdk 0.52.1 → 0.52.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/dist/src/hooks.d.ts +7 -0
- package/dist/src/hooks.js +17 -1
- package/docs/ai.md +1 -1
- package/docs/mutations.md +5 -0
- package/package.json +1 -1
package/dist/src/hooks.d.ts
CHANGED
|
@@ -449,6 +449,13 @@ export interface AgentRunOptions {
|
|
|
449
449
|
/** Groups this run with prior runs in the same working session; the agent
|
|
450
450
|
* re-reads them for context. Mint a new id to "clear context". */
|
|
451
451
|
sessionId: string;
|
|
452
|
+
/** Deliberately abort the run in flight and start this one in its place.
|
|
453
|
+
* Without it, `run()` is SINGLE-FLIGHT: a call while a run is streaming
|
|
454
|
+
* returns the in-flight run's promise instead of starting (and billing) a
|
|
455
|
+
* second run — so an accidental double-press resolves with the first run's
|
|
456
|
+
* result. The aborted-and-replaced run still executes and bills server-side;
|
|
457
|
+
* replacement is a deliberate act, never a side effect of an extra click. */
|
|
458
|
+
replace?: boolean;
|
|
452
459
|
}
|
|
453
460
|
/** The live state + controls returned by `useAgentRun`. */
|
|
454
461
|
export interface UseAgentRun<TInput, TOutput> {
|
package/dist/src/hooks.js
CHANGED
|
@@ -440,6 +440,7 @@ export function useAgentRun(alias) {
|
|
|
440
440
|
// header). Lets the hook poll the run to completion if the stream connection
|
|
441
441
|
// drops, and cancel it server-side on an explicit stop.
|
|
442
442
|
const runIdRef = useRef(null);
|
|
443
|
+
const inflightRef = useRef(null);
|
|
443
444
|
// Guards setState after unmount and aborts any in-flight run on unmount, so a
|
|
444
445
|
// stream never keeps writing to a dead component (or leaks the transport).
|
|
445
446
|
const mountedRef = useRef(true);
|
|
@@ -455,6 +456,15 @@ export function useAgentRun(alias) {
|
|
|
455
456
|
setState(s);
|
|
456
457
|
}, []);
|
|
457
458
|
const run = useCallback((input, opts) => {
|
|
459
|
+
// Single-flight: an extra press must not become a second paid run — the
|
|
460
|
+
// old abort-and-restart default kept the first run executing (and
|
|
461
|
+
// billing) server-side while the client went blind to it. Joining the
|
|
462
|
+
// in-flight promise makes a double-click resolve with the first run's
|
|
463
|
+
// result; `replace: true` is the explicit opt-in to abort-and-restart.
|
|
464
|
+
if (inflightRef.current && !opts.replace) {
|
|
465
|
+
captureAppEvent("app_agent_run_deduped", { alias });
|
|
466
|
+
return inflightRef.current;
|
|
467
|
+
}
|
|
458
468
|
handleRef.current?.abort();
|
|
459
469
|
runIdRef.current = null;
|
|
460
470
|
let acc = initialAgentRunState();
|
|
@@ -486,7 +496,7 @@ export function useAgentRun(alias) {
|
|
|
486
496
|
safeSetState(null);
|
|
487
497
|
},
|
|
488
498
|
};
|
|
489
|
-
|
|
499
|
+
const inflight = handle.done
|
|
490
500
|
.then(async () => {
|
|
491
501
|
if (aborted)
|
|
492
502
|
return undefined;
|
|
@@ -538,6 +548,12 @@ export function useAgentRun(alias) {
|
|
|
538
548
|
safeSetState(acc);
|
|
539
549
|
throw err;
|
|
540
550
|
});
|
|
551
|
+
const tracked = inflight.finally(() => {
|
|
552
|
+
if (inflightRef.current === tracked)
|
|
553
|
+
inflightRef.current = null;
|
|
554
|
+
});
|
|
555
|
+
inflightRef.current = tracked;
|
|
556
|
+
return tracked;
|
|
541
557
|
}, [alias, safeSetState]);
|
|
542
558
|
const abort = useCallback(() => handleRef.current?.abort(), []);
|
|
543
559
|
const cancel = useCallback(() => {
|
package/docs/ai.md
CHANGED
|
@@ -49,7 +49,7 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
|
|
|
49
49
|
|
|
50
50
|
| Member | Type | What it is |
|
|
51
51
|
|---|---|---|
|
|
52
|
-
| `run` | `(input, { sessionId }) => Promise<TOutput \| undefined>` | Start a run. Streams progress into the hook's state and resolves to the structured output (`undefined` for a free-text or failed run).
|
|
52
|
+
| `run` | `(input, { sessionId, replace? }) => Promise<TOutput \| undefined>` | Start a run. Streams progress into the hook's state and resolves to the structured output (`undefined` for a free-text or failed run). **Single-flight:** while a run is in flight, calling it again returns the in-flight run's promise — an accidental double-press joins the first run instead of billing a second one (each dedup emits the `app_agent_run_deduped` analytics event). Pass `replace: true` to deliberately abort-and-restart; the replaced run still executes and bills server-side |
|
|
53
53
|
| `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this |
|
|
54
54
|
| `abort` | `() => void` | Stop listening **locally only** — the run keeps executing server-side and its result is still persisted. This is the unmount path (the hook calls it automatically on unmount) |
|
|
55
55
|
| `status` | `"idle" \| "streaming" \| "completed" \| "error"` | Whole-run state. `abort`/`cancel` reset it to `"idle"` (and clear the partial transcript) |
|
package/docs/mutations.md
CHANGED
|
@@ -199,6 +199,11 @@ When the alias declares `inputs`, the server validates the payload before the wo
|
|
|
199
199
|
is treated as *omitted* (HTML form controls emit `""` for "left blank"). Nested optional
|
|
200
200
|
object fields are plain-optional: omit the key. A required `text` input does accept `""` —
|
|
201
201
|
emptiness is not a type error; validate non-emptiness in the workflow body if it matters.
|
|
202
|
+
- **Required reference inputs reject empty** — a required `record_link` / `member` / `file`
|
|
203
|
+
must resolve to at least one real id: the single form rejects `""`, and the multi form
|
|
204
|
+
rejects `[]` (an empty array is the multi-analog of an empty value — it would otherwise slip
|
|
205
|
+
past both the required check and the reference bindings below and write an empty link). Make
|
|
206
|
+
the input `required: false` if "attach nothing" is valid; then `""` omits and `[]` clears.
|
|
202
207
|
- **Server-side reference bindings** — every `record_link` id must reference an existing
|
|
203
208
|
record in its declared `table_id`; every group-scoped `member` id must belong to the
|
|
204
209
|
declared group; every `file` id must live in the app's workspace. These are real write-time
|