@belonguniverseai/react-sdk 0.4.2 → 0.5.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/dist/api/agent-grants.d.ts +33 -0
- package/dist/api/client.d.ts +104 -10
- package/dist/api/retry-fetch.d.ts +3 -1
- package/dist/api/task-schedules.d.ts +14 -0
- package/dist/browser/agent-link.d.ts +18 -0
- package/dist/browser/agent-touch.d.ts +105 -0
- package/dist/browser/spa-navigate.d.ts +24 -1
- package/dist/component/{arc-COH48Rtu.js → arc-CtyXH_sw.js} +2 -2
- package/dist/component/{architectureDiagram-3BPJPVTR-B5qmE5k5.js → architectureDiagram-3BPJPVTR-C6NZifN6.js} +3 -3
- package/dist/component/{blockDiagram-GPEHLZMM-lkovkWkX.js → blockDiagram-GPEHLZMM-_-K66LP4.js} +4 -4
- package/dist/component/{c4Diagram-AAUBKEIU-CaWDjxTr.js → c4Diagram-AAUBKEIU-DL-K-sZW.js} +3 -3
- package/dist/component/{channel-DNnfyuhY.js → channel-scCA9sxi.js} +2 -2
- package/dist/component/{chunk-2J33WTMH-I7HgfCPn.js → chunk-2J33WTMH-Dkd9OyM1.js} +2 -2
- package/dist/component/{chunk-4BX2VUAB-CZBTp8pg.js → chunk-4BX2VUAB-JbEXcQ3q.js} +2 -2
- package/dist/component/{chunk-55IACEB6-C-RnqcTj.js → chunk-55IACEB6-Dg96dHvQ.js} +2 -2
- package/dist/component/{chunk-727SXJPM-BFV7YyWV.js → chunk-727SXJPM-Cu3-awqi.js} +6 -6
- package/dist/component/{chunk-AQP2D5EJ-BDVikNJT.js → chunk-AQP2D5EJ-DLX62MPh.js} +4 -4
- package/dist/component/{chunk-FMBD7UC4-C864-npb.js → chunk-FMBD7UC4-BxU0KQRK.js} +2 -2
- package/dist/component/{chunk-ND2GUHAM-DuitYR9A.js → chunk-ND2GUHAM-CEQj3so6.js} +2 -2
- package/dist/component/{chunk-QZHKN3VN-B9uI1TYV.js → chunk-QZHKN3VN-BWgwT04-.js} +2 -2
- package/dist/component/{classDiagram-4FO5ZUOK-BUdCEjaj.js → classDiagram-4FO5ZUOK-Ton6m2pw.js} +3 -3
- package/dist/component/{classDiagram-v2-Q7XG4LA2-BUdCEjaj.js → classDiagram-v2-Q7XG4LA2-Ton6m2pw.js} +3 -3
- package/dist/component/{cose-bilkent-S5V4N54A-Dgk__ohs.js → cose-bilkent-S5V4N54A-Dja09hp6.js} +2 -2
- package/dist/component/{dagre-BM42HDAG-DGHFHFPf.js → dagre-BM42HDAG-Ch6fODe-.js} +2 -2
- package/dist/component/{diagram-2AECGRRQ-DvGSzrwW.js → diagram-2AECGRRQ-DdDr6tHi.js} +3 -3
- package/dist/component/{diagram-5GNKFQAL-XebuIyjn.js → diagram-5GNKFQAL-BT2qfjUH.js} +4 -4
- package/dist/component/{diagram-KO2AKTUF-slUL6XEO.js → diagram-KO2AKTUF-UcWrBVNK.js} +3 -3
- package/dist/component/{diagram-LMA3HP47-BEPY8GOZ.js → diagram-LMA3HP47-D-BCOeIt.js} +3 -3
- package/dist/component/{diagram-OG6HWLK6-DHI_0aIk.js → diagram-OG6HWLK6-CJDJgfQO.js} +4 -4
- package/dist/component/{erDiagram-TEJ5UH35-D-6DbJr-.js → erDiagram-TEJ5UH35-kY5sdKqg.js} +5 -5
- package/dist/component/{flowDiagram-I6XJVG4X-Cw87OG_x.js → flowDiagram-I6XJVG4X-9OddgRiE.js} +7 -7
- package/dist/component/{ganttDiagram-6RSMTGT7-B47s5HUB.js → ganttDiagram-6RSMTGT7-Dc9t8llA.js} +3 -3
- package/dist/component/{gitGraphDiagram-PVQCEYII-DqFW3uiq.js → gitGraphDiagram-PVQCEYII-DYjn9Ff5.js} +4 -4
- package/dist/component/{highlighted-body-OFNGDK62-CBU3EdTE.js → highlighted-body-OFNGDK62-D_TDzrFY.js} +2 -2
- package/dist/component/index.js +2 -2
- package/dist/component/{infoDiagram-5YYISTIA-_0W78xb_.js → infoDiagram-5YYISTIA-CBPoKi7E.js} +2 -2
- package/dist/component/{ishikawaDiagram-YF4QCWOH-CvybNkGE.js → ishikawaDiagram-YF4QCWOH-C3rChBg5.js} +2 -2
- package/dist/component/{journeyDiagram-JHISSGLW-DgLrQs9G.js → journeyDiagram-JHISSGLW-DVTybrGG.js} +5 -5
- package/dist/component/{kanban-definition-UN3LZRKU-ReYnw5Xl.js → kanban-definition-UN3LZRKU-DNMLC4NQ.js} +3 -3
- package/dist/component/{linear-Cih5KQ00.js → linear-B_oPUR6_.js} +2 -2
- package/dist/component/{mermaid-GHXKKRXX-B6ix-Z04.js → mermaid-GHXKKRXX-CjZr9GWr.js} +28679 -27638
- package/dist/component/{mindmap-definition-RKZ34NQL-hRhkTdF6.js → mindmap-definition-RKZ34NQL-CErhrdm3.js} +4 -4
- package/dist/component/{pieDiagram-4H26LBE5-DAAEUZCY.js → pieDiagram-4H26LBE5-Cno02n3_.js} +4 -4
- package/dist/component/{quadrantDiagram-W4KKPZXB-Benxdf_G.js → quadrantDiagram-W4KKPZXB-DQV7ytCz.js} +3 -3
- package/dist/component/{requirementDiagram-4Y6WPE33-BxvkbDYP.js → requirementDiagram-4Y6WPE33-DBORDX5u.js} +4 -4
- package/dist/component/{sankeyDiagram-5OEKKPKP-RPwwVwu3.js → sankeyDiagram-5OEKKPKP-gNBaVNW1.js} +2 -2
- package/dist/component/{sequenceDiagram-3UESZ5HK-C-prnAtS.js → sequenceDiagram-3UESZ5HK-BlxJztMa.js} +4 -4
- package/dist/component/{stateDiagram-AJRCARHV-BEIQAx9e.js → stateDiagram-AJRCARHV-2ziF_Zes.js} +3 -3
- package/dist/component/{stateDiagram-v2-BHNVJYJU-BD5dkY8l.js → stateDiagram-v2-BHNVJYJU-BWmzKTMS.js} +3 -3
- package/dist/component/{timeline-definition-PNZ67QCA-DQJPMkfL.js → timeline-definition-PNZ67QCA-OcHhxXWM.js} +3 -3
- package/dist/component/{vennDiagram-CIIHVFJN-CreOqXMc.js → vennDiagram-CIIHVFJN-Fl9VlPVe.js} +2 -2
- package/dist/component/{wardleyDiagram-YWT4CUSO--dbQqySn.js → wardleyDiagram-YWT4CUSO-BNHUW0Ex.js} +3 -3
- package/dist/component/{xychartDiagram-2RQKCTM6-BOSkmQiX.js → xychartDiagram-2RQKCTM6-Bog3Ddue.js} +3 -3
- package/dist/components/ai-elements/dock-link.d.ts +27 -0
- package/dist/components/ai-elements/link-safety-modal.d.ts +46 -1
- package/dist/components/ai-elements/message.d.ts +29 -2
- package/dist/components/ai-elements/prompt-input.d.ts +1 -1
- package/dist/components/embed/file-preview-kind.d.ts +6 -1
- package/dist/components/embed/parse-csv.d.ts +22 -5
- package/dist/components/embed/use-preview-bytes.d.ts +12 -0
- package/dist/components/embed/use-workspace-preview-file.d.ts +34 -0
- package/dist/components/scheduled/ScheduledDetailSurface.d.ts +2 -2
- package/dist/components/ui/input-group.d.ts +7 -4
- package/dist/embed/approval-bus.d.ts +5 -0
- package/dist/embed/slash-commands.d.ts +41 -12
- package/dist/embed/voice/agent-queue.d.ts +106 -0
- package/dist/embed/voice/call-roster-store.d.ts +110 -0
- package/dist/embed/voice/line-dispatch.d.ts +57 -18
- package/dist/embed/voice/line-run-reader.d.ts +35 -55
- package/dist/embed/voice/line-slots.d.ts +6 -2
- package/dist/embed/voice-call-lines.d.ts +58 -11
- package/dist/embed/voice-call-pacing.d.ts +47 -9
- package/dist/embed/voice-call.d.ts +70 -14
- package/dist/i18n/messages/en.d.ts +30 -20
- package/dist/i18n/messages/surfaces/aiElements.d.ts +3 -0
- package/dist/i18n/messages/surfaces/scheduled.d.ts +9 -0
- package/dist/i18n/messages/surfaces/stream.d.ts +31 -16
- package/dist/stream/chat-upload.d.ts +29 -3
- package/dist/stream/run-line-progress.d.ts +62 -0
- package/dist/stream/sandbox-output.d.ts +31 -3
- package/dist/stream/stream-state-store.d.ts +56 -0
- package/dist/stream/tool-error-copy.d.ts +4 -0
- package/dist/test/fetch-stubs.d.ts +16 -0
- package/dist/types/public.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-line FIFO for work an operator asks an agent to do WHILE that agent is
|
|
3
|
+
* already busy — the structure behind "queue two things on Agent 2, then
|
|
4
|
+
* drop one" (Slice 3 of the voice-operator plan). File-scope-singleton
|
|
5
|
+
* idiom, same as `line-slots.ts` / `voice-call-lines.ts` beside it: one
|
|
6
|
+
* module-level `Map<number, QueuedRequest[]>`, no class, no React.
|
|
7
|
+
*
|
|
8
|
+
* WHAT THIS REPLACES: today, sending work to a busy agent POSTs it
|
|
9
|
+
* immediately — the message lands in Postgres and the server's own drain
|
|
10
|
+
* loop (SAD §5.1 / CLAUDE.md §37) gets to it whenever the agent frees up.
|
|
11
|
+
* That queue is invisible (no way to ask "what's waiting for Agent 2?"),
|
|
12
|
+
* unorderable (no way to promote the third ask over the second), and
|
|
13
|
+
* undroppable (no way to take one back). This module is the queue moved
|
|
14
|
+
* into the browser, so a caller can read it back, drop an entry, or reorder
|
|
15
|
+
* it before anything is sent. (Wiring `dispatchToLine`'s busy-line path
|
|
16
|
+
* through this queue, and adding the voice-facing `manage_queue` tool, are
|
|
17
|
+
* later tasks — see NOT WIRED IN YET below.)
|
|
18
|
+
*
|
|
19
|
+
* WHY CALL-SCOPED AND NOT DURABLE — the trade-off, spelled out:
|
|
20
|
+
*
|
|
21
|
+
* - A held `QueuedRequest` lives ONLY in this in-memory Map. Today's
|
|
22
|
+
* behavior is strictly more durable: a queued follow-up is already
|
|
23
|
+
* POSTed and sitting in Postgres the instant the operator says it, so it
|
|
24
|
+
* survives a tab crash, a network blip, anything short of the session
|
|
25
|
+
* itself being deleted. Moving the queue into the browser gives up that
|
|
26
|
+
* durability in exchange for the operator being able to see and steer
|
|
27
|
+
* it — that is the entire point, and the trade needs to be made with
|
|
28
|
+
* open eyes, not by accident.
|
|
29
|
+
* - `drainAllQueues()` is what keeps that loss narrow. It is the hang-up
|
|
30
|
+
* flush: called as part of ending a call normally, it empties every
|
|
31
|
+
* line's queue and hands back everything that was waiting so it can be
|
|
32
|
+
* POSTed to the server exactly as if this module had never sat in front
|
|
33
|
+
* of it. A normal end-of-call therefore loses nothing — the queue is
|
|
34
|
+
* just a staging area the operator got to look inside before the same
|
|
35
|
+
* dispatch that would have happened anyway.
|
|
36
|
+
* - The durability genuinely AT RISK is narrower still than it first
|
|
37
|
+
* sounds: a live voice call is not something a page reload survives
|
|
38
|
+
* regardless of this module — there is no reload-resume contract for
|
|
39
|
+
* the call itself (unlike the §41 guarantee for an already-dispatched
|
|
40
|
+
* agent turn, which is a Postgres/Redis-backed run, not the call). So
|
|
41
|
+
* "a reload loses the queue" is not a NEW failure this module
|
|
42
|
+
* introduces; the call was already gone. What is genuinely new is the
|
|
43
|
+
* sliver between "something got queued" and "the call ends" during
|
|
44
|
+
* which the tab could crash before `drainAllQueues()` ever runs — a
|
|
45
|
+
* real but narrow window, and the one honest cost of this design.
|
|
46
|
+
*
|
|
47
|
+
* NOT WIRED IN YET: this ships as a standalone module with its own tests
|
|
48
|
+
* and no production caller. A later task connects `enqueueForLine` /
|
|
49
|
+
* `dequeueForLine` to `dispatchToLine`'s busy-line path and `drainAllQueues`
|
|
50
|
+
* to the hang-up handler; another adds the voice-facing `manage_queue` tool
|
|
51
|
+
* on top of `peekQueue` / `dropQueuedAt` / `moveQueued`.
|
|
52
|
+
*/
|
|
53
|
+
export type QueuedRequest = {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly request: string;
|
|
56
|
+
};
|
|
57
|
+
/** Append `request` to the back of `line`'s queue, minting a fresh id
|
|
58
|
+
* (`nanoid()`, the same generator `line-dispatch.ts` uses for
|
|
59
|
+
* `clientMessageId`). Always succeeds — there is no bound on queue depth. */
|
|
60
|
+
export declare const enqueueForLine: (line: number, request: string) => QueuedRequest;
|
|
61
|
+
/** Pop and return the FRONT of `line`'s queue, or `null` when it's empty. */
|
|
62
|
+
export declare const dequeueForLine: (line: number) => QueuedRequest | null;
|
|
63
|
+
/** A read-only snapshot of `line`'s queue, front to back. Never mutates. */
|
|
64
|
+
export declare const peekQueue: (line: number) => readonly QueuedRequest[];
|
|
65
|
+
/** How many requests are waiting on `line`. */
|
|
66
|
+
export declare const queueDepth: (line: number) => number;
|
|
67
|
+
/** Remove and return the item at `index` in `line`'s queue, or `null` when
|
|
68
|
+
* `index` is out of range — an explicit `< 0 || >= length` bound (§10: never
|
|
69
|
+
* truthiness on a length or index), so a bad index is an honest miss rather
|
|
70
|
+
* than a silent no-op that could be mistaken for success. */
|
|
71
|
+
export declare const dropQueuedAt: (line: number, index: number) => QueuedRequest | null;
|
|
72
|
+
/** Move the item at `from` to position `to` in `line`'s queue (both bounds
|
|
73
|
+
* explicit, same discipline as `dropQueuedAt`). Returns `false` — leaving
|
|
74
|
+
* the queue completely untouched — when either index is out of range;
|
|
75
|
+
* `true` on success, including the no-op case `from === to` (a valid index
|
|
76
|
+
* moved to itself: nothing changes, but the request was satisfied, not
|
|
77
|
+
* refused). `to` is a position in the queue AFTER `from` is removed — the
|
|
78
|
+
* standard "move to position N" reading: moving the last item to `to: 0`
|
|
79
|
+
* makes it the new front. */
|
|
80
|
+
export declare const moveQueued: (line: number, from: number, to: number) => boolean;
|
|
81
|
+
/** The hang-up flush: empty EVERY line's queue and return everything that
|
|
82
|
+
* was waiting, in line-then-position order (line ascending, FIFO within a
|
|
83
|
+
* line) — the order a normal end-of-call should hand work to the server in.
|
|
84
|
+
* Exhaustive by construction: every line the map holds is visited and
|
|
85
|
+
* cleared, so nothing can be left behind. See the module doc for why this
|
|
86
|
+
* is what makes the queue's in-memory-only durability trade-off safe for a
|
|
87
|
+
* normal call end. */
|
|
88
|
+
export declare const drainAllQueues: () => readonly {
|
|
89
|
+
readonly line: number;
|
|
90
|
+
readonly request: string;
|
|
91
|
+
}[];
|
|
92
|
+
/** The call-teardown clear: drop every line's queue without returning
|
|
93
|
+
* anything. Unlike `drainAllQueues`, this is for the paths that must not
|
|
94
|
+
* re-POST stale work — e.g. resetting state at the START of a fresh call. */
|
|
95
|
+
export declare const resetQueues: () => void;
|
|
96
|
+
/** Drop `line`'s held queue and report how many items were discarded — the
|
|
97
|
+
* ONE-line counterpart to `resetQueues()` (every line). For the paths that
|
|
98
|
+
* must not re-POST or re-dispatch one line's stale work without touching any
|
|
99
|
+
* other line's: `stop_agent` (voice-call.ts) clearing exactly the queue it
|
|
100
|
+
* just told the user was dropped, and `closeAgentLine` (voice-call-lines.ts)
|
|
101
|
+
* releasing a line whose held work must not resurrect onto the next
|
|
102
|
+
* occupant of the same number. An explicit `=== 0` check rather than
|
|
103
|
+
* truthiness (§10), same discipline as `dropQueuedAt`/`moveQueued`: a
|
|
104
|
+
* nothing-to-drop line reports `0` honestly instead of writing an empty
|
|
105
|
+
* array through `setQueue` for no reason. */
|
|
106
|
+
export declare const clearQueueForLine: (line: number) => number;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voice call's agent ROSTER — the browser-side twin of the tenant
|
|
3
|
+
* roster minted server-side (`buildCallRoster` in
|
|
4
|
+
* apps/belong-cloud/src/features/voice/call-roster.ts) and threaded through
|
|
5
|
+
* `POST /v1/voice/call-token`'s `agents` field (`callAgentSpecSchema`,
|
|
6
|
+
* voice.schemas.ts). This is what lets the dock's rail, the call screen's
|
|
7
|
+
* agent board, and every spoken announcement address an agent by ROLE
|
|
8
|
+
* ("cli", "consulting") instead of a bare line number — and do so from the
|
|
9
|
+
* moment the dock boots, before any call has ever been started: the rail
|
|
10
|
+
* renders dock-wide with no call active, so it needs the LAST-KNOWN roster,
|
|
11
|
+
* not only one handed to it live off a fresh mint.
|
|
12
|
+
*
|
|
13
|
+
* Persisted per workspace, exactly like `line-slots.ts` beside it
|
|
14
|
+
* (`belong:voice-call-roster`, suffixed `:ws:<id>` when a workspace is in
|
|
15
|
+
* effect via `peekWorkspaceId()`) — a host workspace switch must not hand
|
|
16
|
+
* tenant A's roster (its titles, its CLI agent) to a rail rendering for
|
|
17
|
+
* tenant B.
|
|
18
|
+
*
|
|
19
|
+
* localStorage is a perimeter: the persisted roster is zod-validated on read
|
|
20
|
+
* and any junk (corrupted JSON, wrong shape, storage disabled) degrades to
|
|
21
|
+
* an EMPTY roster — same as "no call minted yet". The schema below is a
|
|
22
|
+
* deliberately independent, by-hand copy of the server's
|
|
23
|
+
* `callAgentSpecSchema`: the two packages ship separately and share no type
|
|
24
|
+
* module, so this is what keeps them honest (mirrors the existing
|
|
25
|
+
* `CallAgentSpec`/`callAgentSpecSchema` split already living server-side in
|
|
26
|
+
* `call-roster.ts` / `voice.schemas.ts`).
|
|
27
|
+
*
|
|
28
|
+
* Two fallback rules, and they are NOT the same rule:
|
|
29
|
+
*
|
|
30
|
+
* - `lineForRole` / `roleForLine` on an EMPTY roster (nothing ever minted,
|
|
31
|
+
* or a mint whose body failed to parse) fall back to the POSITIONAL
|
|
32
|
+
* default that's been true since roles didn't exist: main=1, cli=2,
|
|
33
|
+
* consulting=3. This is what stops a parse failure from bricking role
|
|
34
|
+
* addressing mid-call — the switchboard still answers to
|
|
35
|
+
* main/cli/consulting by position.
|
|
36
|
+
* - On a NON-empty roster that simply doesn't have that role/line (a
|
|
37
|
+
* tenant with no CLI), the same lookups return `null` instead — the
|
|
38
|
+
* honest signal that THIS workspace has no such agent, which is what
|
|
39
|
+
* later produces an `unknown_role` tool response rather than silently
|
|
40
|
+
* addressing the wrong agent.
|
|
41
|
+
*
|
|
42
|
+
* `titleForRole` / `titleForLine` never return `null` — a title is always
|
|
43
|
+
* needed for display/announcement, so THEIR fallback is a string, applied
|
|
44
|
+
* uniformly regardless of whether the roster is empty or merely missing
|
|
45
|
+
* that entry. The two fallbacks are deliberately DIFFERENT shapes, not the
|
|
46
|
+
* same rule twice:
|
|
47
|
+
*
|
|
48
|
+
* - `titleForRole` falls back to a ROLE-derived phrase ("the main agent" /
|
|
49
|
+
* "the data agent" / "the analyst") — never a switchboard
|
|
50
|
+
* number. Every voice tool call carries a role (it is the operator's
|
|
51
|
+
* entire addressing vocabulary — see `voice-call.ts`'s
|
|
52
|
+
* `resolveRoleLine`), so this fallback is always reachable from a role
|
|
53
|
+
* the model already said out loud, and it feeds prose the model
|
|
54
|
+
* SPEAKS: an empty roster (nothing minted yet, or a pre-roster backend
|
|
55
|
+
* mid rolling deploy — `callTokenResponseSchema`'s `agents` field
|
|
56
|
+
* legitimately defaults to `[]`) must not leak a bare "Agent 2" into
|
|
57
|
+
* that prose.
|
|
58
|
+
* - `titleForLine` falls back to `"the main agent"` for line 1 and
|
|
59
|
+
* `` `Agent ${line}` `` otherwise — the "Agent N" convention the
|
|
60
|
+
* switchboard used before roles existed. This one serves the UI's
|
|
61
|
+
* numbered rail, which genuinely displays a switchboard slot number, so
|
|
62
|
+
* a line-derived fallback is correct there and stays as-is.
|
|
63
|
+
*/
|
|
64
|
+
export type CallAgentRole = "main" | "cli" | "consulting";
|
|
65
|
+
export type CallAgentSpec = {
|
|
66
|
+
readonly role: CallAgentRole;
|
|
67
|
+
/** The durable switchboard line underneath (`line-slots.ts`). */
|
|
68
|
+
readonly line: number;
|
|
69
|
+
/** What the voice calls this agent out loud. */
|
|
70
|
+
readonly title: string;
|
|
71
|
+
/** One sentence describing what this agent is for. */
|
|
72
|
+
readonly purpose: string;
|
|
73
|
+
};
|
|
74
|
+
/** Persist the roster minted for the current call. A whole-roster replace —
|
|
75
|
+
* never a per-entry patch — of whatever this workspace held before. */
|
|
76
|
+
export declare const setCallRoster: (roster: readonly CallAgentSpec[]) => void;
|
|
77
|
+
/** The current workspace's roster, or an empty array when nothing has been
|
|
78
|
+
* minted yet (or storage held junk). */
|
|
79
|
+
export declare const peekCallRoster: () => readonly CallAgentSpec[];
|
|
80
|
+
/** Persist the tenant's product name minted alongside the roster (U2) — a
|
|
81
|
+
* whole-value replace, same discipline as `setCallRoster`. */
|
|
82
|
+
export declare const setProductName: (productName: string | null) => void;
|
|
83
|
+
/** The current workspace's product name, or `null` when nothing has been
|
|
84
|
+
* minted yet, the tenant has none, or storage held junk. Used ONLY to
|
|
85
|
+
* localize the SDK's own UI copy (CallScreen's agent board, U2) — never fed
|
|
86
|
+
* into any string the model speaks or receives. */
|
|
87
|
+
export declare const peekProductName: () => string | null;
|
|
88
|
+
/** Resolve a role to its switchboard line. See the module doc: `null` only
|
|
89
|
+
* ever means "this NON-empty roster doesn't have that role" — an EMPTY
|
|
90
|
+
* roster falls back to the positional default instead. */
|
|
91
|
+
export declare const lineForRole: (role: CallAgentRole) => number | null;
|
|
92
|
+
/** Resolve a switchboard line to its role — the inverse of `lineForRole`,
|
|
93
|
+
* with the same two-rule fallback (EMPTY roster → positional; non-empty and
|
|
94
|
+
* missing → `null`). */
|
|
95
|
+
export declare const roleForLine: (line: number) => CallAgentRole | null;
|
|
96
|
+
/** What the voice calls this role out loud. Always a string — falls back to
|
|
97
|
+
* a ROLE-derived title (never a switchboard number, never `null`) when the
|
|
98
|
+
* roster has no entry for this role, whether the roster is empty or just
|
|
99
|
+
* missing it. */
|
|
100
|
+
export declare const titleForRole: (role: CallAgentRole) => string;
|
|
101
|
+
/** What the voice calls this line out loud. Always a string — falls back to
|
|
102
|
+
* `"the main agent"` (line 1) or `` `Agent ${line}` `` otherwise when the
|
|
103
|
+
* roster has no entry for this line, whether the roster is empty or just
|
|
104
|
+
* missing it. */
|
|
105
|
+
export declare const titleForLine: (line: number) => string;
|
|
106
|
+
/** Test-only: drop the persisted roster AND product name for the current
|
|
107
|
+
* workspace scope — they are minted together (the same call-token mint), so
|
|
108
|
+
* a test resetting one without the other would leak a stale product name
|
|
109
|
+
* (or roster) into the next case. */
|
|
110
|
+
export declare const __clearCallRosterForTest: () => void;
|
|
@@ -16,31 +16,70 @@
|
|
|
16
16
|
* Workspace isolation (§28): a background line opened while the host is inside
|
|
17
17
|
* a workspace must land in THAT workspace, not the shared legacy root — so the
|
|
18
18
|
* create body carries the effective `workspaceExternalId` (absent when the host
|
|
19
|
-
* runs unscoped). The line's registry label seeds the session title
|
|
20
|
-
*
|
|
19
|
+
* runs unscoped). The line's registry label seeds the session title when one
|
|
20
|
+
* exists; otherwise (U4 — the common case for a freshly materialized line,
|
|
21
|
+
* since `dispatchToBackgroundLine` no longer seeds the registry label with the
|
|
22
|
+
* agent's own name) the REQUEST itself does, so the sessions list still names
|
|
23
|
+
* the background conversation by what it's actually about ("Reconcile the
|
|
24
|
+
* July invoices"), not the agent that happened to run it.
|
|
21
25
|
*
|
|
22
26
|
* BOOT RE-ATTACH (F3/R4): `attachLineReader` below is the ONE seam that wires
|
|
23
|
-
* a line-run reader onto a line, shared by a fresh dispatch
|
|
24
|
-
* right after a POST accepts a run) and `reattachPendingLineRuns`
|
|
25
|
-
* once at boot, after `initAgentLines()` rehydrates the registry)
|
|
26
|
-
* run that was still going when the page reloaded. Both paths
|
|
27
|
-
* binding (`persistSlotRun`) and wire the identical phase/approval/
|
|
28
|
-
* artifact/finish/error callbacks — a reload must behave exactly like
|
|
29
|
-
* live dispatch it is standing in for, not a parallel, easier-to-drift copy.
|
|
27
|
+
* a VOICE-dispatched line-run reader onto a line, shared by a fresh dispatch
|
|
28
|
+
* (this module, right after a POST accepts a run) and `reattachPendingLineRuns`
|
|
29
|
+
* (called once at boot, after `initAgentLines()` rehydrates the registry)
|
|
30
|
+
* resuming a run that was still going when the page reloaded. Both paths
|
|
31
|
+
* persist the binding (`persistSlotRun`) and wire the identical phase/approval/
|
|
32
|
+
* navigate/artifact/finish/error callbacks — a reload must behave exactly like
|
|
33
|
+
* the live dispatch it is standing in for, not a parallel, easier-to-drift copy.
|
|
34
|
+
*
|
|
35
|
+
* TWO READER KINDS (BEL-90). This module now attaches two, and which callback
|
|
36
|
+
* set a caller gets is a correctness question, not a style one:
|
|
37
|
+
*
|
|
38
|
+
* - `attachLineReader` — the voice-NARRATION reader. Its callbacks are not
|
|
39
|
+
* plain setters: `deliverLineReply`/`deliverLineFailure` push
|
|
40
|
+
* `conversation.item.create` events into an active voice call and ask the
|
|
41
|
+
* model to respond, `notifyLinePhase` appends timeline rows and nudges
|
|
42
|
+
* spoken progress, `deliverLineArtifact` opens a preview panel. Correct
|
|
43
|
+
* for a run the operator dispatched BY VOICE.
|
|
44
|
+
* - `attachLineProgressReader` — the plain-progress reader, for a run the
|
|
45
|
+
* user started by TYPING that was still in flight at reload. It goes
|
|
46
|
+
* through `reportRunLineProgress` and touches only the registry, so a
|
|
47
|
+
* typed run can never make the agent start talking about background
|
|
48
|
+
* progress mid-call.
|
|
30
49
|
*/
|
|
31
50
|
/** Abort every live line reader and invalidate their callbacks (call teardown).
|
|
32
51
|
* Subscribed below to fire whenever a call leaves in_call/connecting. */
|
|
33
52
|
export declare const abortAllLineReaders: () => void;
|
|
53
|
+
/** Test-only: drop every plain-progress reader (and the active-session latch)
|
|
54
|
+
* so a case never inherits either from the previous case. The narration
|
|
55
|
+
* readers have `abortAllLineReaders`; these deliberately do NOT ride on it — a
|
|
56
|
+
* voice call ending has nothing to do with a typed run still streaming. */
|
|
57
|
+
export declare const __detachLineProgressReadersForTest: () => void;
|
|
34
58
|
/**
|
|
35
|
-
* BOOT RE-ATTACH
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
59
|
+
* BOOT RE-ATTACH: call this once, right after `initAgentLines()` rehydrates
|
|
60
|
+
* the registry. A line rehydrated as `status: "working"` has a run that was
|
|
61
|
+
* still going when the page went away (`rehydratedLines()` in
|
|
62
|
+
* voice-call-lines.ts) — this attaches the reader that actually converges it
|
|
63
|
+
* to ready/failed, instead of leaving an honest-looking but permanently stuck
|
|
64
|
+
* "working".
|
|
65
|
+
*
|
|
66
|
+
* TWO paths, because the two dispatch kinds have genuinely different
|
|
67
|
+
* contracts (BEL-90 / B1 — see `attachLineProgressReader`'s doc):
|
|
68
|
+
*
|
|
69
|
+
* - F3/R4, UNCHANGED — a line whose durable SLOT carries a runId was
|
|
70
|
+
* dispatched by voice. It gets the narration reader, on every line that has
|
|
71
|
+
* one, INCLUDING when its session is the one on screen. That last part is
|
|
72
|
+
* load-bearing: `persistSlotRun(line, null)` in that reader's terminal
|
|
73
|
+
* callbacks is the durable slot's clearer, and skipping the on-screen line
|
|
74
|
+
* would leave a stale slot runId that rehydrates as "working" on every
|
|
75
|
+
* future reload (§41). The transport's terminal now clears the slot too, so
|
|
76
|
+
* both routes converge.
|
|
77
|
+
* - BEL-90 — a line with NO slot runId but a fresh parked run cursor was
|
|
78
|
+
* started by typing. It gets the plain-progress reader, and it is SKIPPED
|
|
79
|
+
* when its session is the active one: ChatRoute is about to mount for that
|
|
80
|
+
* session and its transport will claim the single reader. Skipping is safe
|
|
81
|
+
* here precisely because a typed run writes no durable slot runId — there
|
|
82
|
+
* is nothing that could get stuck.
|
|
44
83
|
*/
|
|
45
84
|
export declare const reattachPendingLineRuns: (fetchFn?: typeof fetch) => void;
|
|
46
85
|
export declare const dispatchToLine: (req: {
|
|
@@ -1,60 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* Minimal SSE consumer for a BACKGROUND line's run stream — the Operator's
|
|
3
|
-
* ear on lines the chat surface isn't showing. Reads the AI-SDK data-stream
|
|
4
|
-
* protocol from `GET /v1/runs/:runId/stream` and surfaces only what the call
|
|
5
|
-
* needs: tool phases, approval-needed, the final text, errors. No `useChat`,
|
|
6
|
-
* no rendering, no reconnect/resume (that's `BelongChatTransport`'s job for
|
|
7
|
-
* the foreground chat surface — a background line reader just watches once).
|
|
8
|
-
* Fetch is injected for tests.
|
|
9
|
-
*
|
|
10
|
-
* Wire shapes mirror the real protocol pinned in `belong-chat-transport.ts` /
|
|
11
|
-
* `belong-data-parts.ts` — NOT the placeholder `text-delta` / `finish` shapes
|
|
12
|
-
* from the original task brief:
|
|
13
|
-
* - text is a DELTA part: `{ type: "text", id, text }` — accumulate `.text`.
|
|
14
|
-
* - `data-tool-call` carries a `state` ("started" | "completed" | "failed");
|
|
15
|
-
* only "started" is a phase change worth reporting.
|
|
16
|
-
* - `data-approval-request` carries a `state` ("pending" | "approved" |
|
|
17
|
-
* "rejected" | "amend_requested" | "expired"); only "pending" means an
|
|
18
|
-
* approval is newly needed.
|
|
19
|
-
* - `data-navigation` carries `{ routeName, params }` — a background agent
|
|
20
|
-
* asking the HOST to navigate. The foreground renderer executes these via
|
|
21
|
-
* `peekRouteHandler` (see `route-dispatch.ts`, the shared seam); a
|
|
22
|
-
* background line has no renderer to pick it up, so `onNavigate` here
|
|
23
|
-
* dispatches through that SAME seam instead. Unlike the foreground's
|
|
24
|
-
* reload-replay (guarded by its `historical` flag + `useFireOnce`), this
|
|
25
|
-
* reader has no reconnect/replay at all (see the module doc above) — each
|
|
26
|
-
* SSE line is parsed and applied exactly once by the read loop below, so
|
|
27
|
-
* no separate dedupe is needed here to keep a navigation from re-firing.
|
|
28
|
-
* - there is no `finish` / `error` part. The terminal signal is
|
|
29
|
-
* `data-status` with `status` ∈ "continuing" | "completed" | "cancelled" |
|
|
30
|
-
* "failed" | "throttled". "continuing" is the max-turns auto-continue —
|
|
31
|
-
* NOT terminal, keep reading the same stream. "completed" finishes with
|
|
32
|
-
* the accumulated text; "cancelled" | "failed" | "throttled" are errors.
|
|
33
|
-
*
|
|
34
|
-
* BOOT RE-ATTACH (F3/R4): this reader is also opened for a run that already
|
|
35
|
-
* finished before the read starts (a persisted `runId` re-attached after a
|
|
36
|
-
* reload — see `attachLineReader` in line-dispatch.ts). Two ways that shows
|
|
37
|
-
* up on the wire, both a CLEAN convergence rather than a failure:
|
|
38
|
-
* - the run is still known server-side and just replays its full history
|
|
39
|
-
* (§36 — `GET .../stream` with no cursor replays from the start): the
|
|
40
|
-
* already-written terminal `data-status` event arrives like any other
|
|
41
|
-
* part and is handled by the SAME "completed"/"cancelled"/"failed"
|
|
42
|
-
* branch below — no special-casing needed, this is the ordinary path.
|
|
43
|
-
* - the run (or its task) is truly gone server-side — 404/410 — which
|
|
44
|
-
* reads identically to "nothing left to report", not an error: `onFinish`
|
|
45
|
-
* fires with empty text instead of `onError`, so a boot re-attach never
|
|
46
|
-
* paints a stale run as failed just because its record has since expired
|
|
47
|
-
* or the session behind it was deleted.
|
|
48
|
-
*
|
|
49
|
-
* SSE parsing here is LINE-BY-LINE (split on "\n", not "\n\n") — the server
|
|
50
|
-
* sends exactly one `data:` line per event. `id:` lines carry the event
|
|
51
|
-
* cursor (ignored — v1 has no reconnect/resume) and `:`-prefixed lines are
|
|
52
|
-
* keepalive comments.
|
|
53
|
-
*/
|
|
1
|
+
import { RunStatusKind } from '../../stream/belong-data-parts.ts';
|
|
54
2
|
export type WatchLineRunInput = {
|
|
55
3
|
readonly runId: string;
|
|
56
4
|
readonly onPhase: (toolName: string) => void;
|
|
57
|
-
|
|
5
|
+
/** `reason` is the wire part's own `data.reason` — "" when the part omitted
|
|
6
|
+
* it (never `undefined`; the caller decides the fallback wording). */
|
|
7
|
+
readonly onApprovalNeeded: (reason: string) => void;
|
|
58
8
|
readonly onNavigate: (data: {
|
|
59
9
|
readonly routeName: string;
|
|
60
10
|
readonly params: Readonly<Record<string, string>>;
|
|
@@ -65,6 +15,36 @@ export type WatchLineRunInput = {
|
|
|
65
15
|
mimeType: string;
|
|
66
16
|
}) => void;
|
|
67
17
|
readonly onFinish: (finalText: string) => void;
|
|
68
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The run did not finish cleanly. `cause` is a discriminated shape rather
|
|
20
|
+
* than a bare `"terminal" | "transport"` string because the two carry
|
|
21
|
+
* genuinely different data (§21): a TERMINAL knows exactly which non-success
|
|
22
|
+
* status the wire reported, and callers must be able to tell a user-pressed
|
|
23
|
+
* Stop (`cancelled`) from a real breakage (`failed`) — the transport already
|
|
24
|
+
* renders the first as an ordinary finish and only the second as an error,
|
|
25
|
+
* and a rail badge that disagrees is the §42 mistake. A TRANSPORT failure
|
|
26
|
+
* knows nothing about the run's state at all: the server task may well still
|
|
27
|
+
* be running.
|
|
28
|
+
*/
|
|
29
|
+
readonly onError: (message: string, cause: {
|
|
30
|
+
readonly kind: "terminal";
|
|
31
|
+
readonly status: Exclude<RunStatusKind, "continuing">;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: "transport";
|
|
34
|
+
}) => void;
|
|
35
|
+
/**
|
|
36
|
+
* "The stream just delivered something" — fired once per chunk read off the
|
|
37
|
+
* socket, INCLUDING the server's `: keepalive` comment every 15s (which
|
|
38
|
+
* `parseSseLine` otherwise drops). It carries no payload because its only
|
|
39
|
+
* job is liveness: BEL-90's progress reader is the sole listener on a
|
|
40
|
+
* backgrounded typed run after a reload, and the parked run cursor's
|
|
41
|
+
* age-out gate (`peekLiveParkedRun`) needs a re-stamp from SOMEONE or a run
|
|
42
|
+
* doing two minutes of silent tool work reads as dead (§41).
|
|
43
|
+
*
|
|
44
|
+
* OPTIONAL because only that one caller owns a cursor. The narration reader
|
|
45
|
+
* (`attachLineReader`) rehydrates from the durable slot runId, which has no
|
|
46
|
+
* age gate, so there is nothing for it to keep fresh.
|
|
47
|
+
*/
|
|
48
|
+
readonly onTick?: () => void;
|
|
69
49
|
};
|
|
70
50
|
export declare const watchLineRun: (input: WatchLineRunInput, fetchFn?: typeof fetch) => (() => void);
|
|
@@ -34,8 +34,12 @@
|
|
|
34
34
|
* streaming server-side. Back-compatible the same way `line1` is: a bag
|
|
35
35
|
* written before this field existed has no `runId` key at all, and the
|
|
36
36
|
* schema defaults it to `null` (nothing was in flight when that bag was
|
|
37
|
-
* written). Line 1
|
|
38
|
-
*
|
|
37
|
+
* written). Line 1's slot `runId` stays `null` in practice — nothing dispatches
|
|
38
|
+
* to line 1 and `persistSlotRun` is the only writer — but do not read that as
|
|
39
|
+
* "line 1 has no run": the IN-MEMORY `AgentLine.runId` is no longer always null
|
|
40
|
+
* for line 1, since a reload seeds it from the parked run cursor (BEL-90 — see
|
|
41
|
+
* `AgentLine.runId`'s doc in voice-call-lines.ts). This field is the VOICE
|
|
42
|
+
* dispatch's durable binding specifically, not "the line's run".
|
|
39
43
|
*/
|
|
40
44
|
/** Every line number that gets a persisted slot — all three agent lines. */
|
|
41
45
|
declare const SLOT_LINES: readonly [1, 2, 3];
|
|
@@ -31,10 +31,16 @@ export type AgentLine = {
|
|
|
31
31
|
readonly lastText: string;
|
|
32
32
|
/** The line's current run, for `stop_agent` to target — bound by the
|
|
33
33
|
* line-run reader once a background dispatch's POST returns a runId, and
|
|
34
|
-
* cleared back to `null` when that run reaches a terminal state.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* cleared back to `null` when that run reaches a terminal state.
|
|
35
|
+
*
|
|
36
|
+
* Line 1 never has this WRITTEN by a dispatch (line-dispatch.ts never
|
|
37
|
+
* dispatches to it), and its stop-time runId is still resolved from the
|
|
38
|
+
* stream-state store — the same source the on-screen Stop button reads. It
|
|
39
|
+
* is no longer always `null` for line 1, though: a REHYDRATE seeds it from
|
|
40
|
+
* the parked run cursor on every line alike (BEL-90 — see
|
|
41
|
+
* `inFlightRehydration`), because a run started by typing keeps streaming
|
|
42
|
+
* across a reload and a chip that reads "idle" for it is a lie. Nothing
|
|
43
|
+
* about the stop path changed; only the reload seed did. */
|
|
38
44
|
readonly runId: string | null;
|
|
39
45
|
/** The live "what is it doing right now" label (e.g. "writing code"),
|
|
40
46
|
* written by `setLineStep` as phase notifications arrive while `status`
|
|
@@ -114,6 +120,20 @@ export declare const lineBoundToSession: (sessionId: string) => AgentLine | unde
|
|
|
114
120
|
* already holds must SWITCH to Agent 1, not adopt (duplicate) it onto whatever
|
|
115
121
|
* background line the user happens to be viewing. */
|
|
116
122
|
export declare const lineOwningSession: (sessionId: string) => AgentLine | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* EVERY line bound to `sessionId` — the plural form `markSessionSeen` and
|
|
125
|
+
* `releaseLinesForSession` below already work in, for the same reason they do:
|
|
126
|
+
* more than one line can hold one session (line 1 aliases whatever the chat is
|
|
127
|
+
* showing, so a background line the user opened is held by both), and a legacy
|
|
128
|
+
* `belong:voice-line-slots` bag written before the `isBoundToAnotherLine`
|
|
129
|
+
* guards landed can rehydrate two background lines onto one session.
|
|
130
|
+
*
|
|
131
|
+
* `lineOwningSession` answers a different question — "which ONE agent should a
|
|
132
|
+
* History reopen switch to" — and stays singular. Anything that WRITES per-line
|
|
133
|
+
* state for a session (BEL-90's `reportRunLineProgress`) must use this one, or
|
|
134
|
+
* the second holder freezes at whatever it last said.
|
|
135
|
+
*/
|
|
136
|
+
export declare const linesOwningSession: (sessionId: string) => readonly AgentLine[];
|
|
117
137
|
/** True when `sessionId` is already the session of some line OTHER than line
|
|
118
138
|
* 1 — chat.tsx's line-1 aliasing guard uses this to skip rebinding line 1
|
|
119
139
|
* onto a background line's session while the user is viewing it via
|
|
@@ -138,6 +158,26 @@ export declare const isBoundToAnotherLine: (sessionId: string) => boolean;
|
|
|
138
158
|
*/
|
|
139
159
|
export declare const foregroundLineNumber: (sessionId: string | null) => number;
|
|
140
160
|
export declare const openAgentLine: (label: string) => AgentLine | null;
|
|
161
|
+
/**
|
|
162
|
+
* Materialize a line AT A GIVEN NUMBER, unlike `openAgentLine`'s lowest-FREE
|
|
163
|
+
* allocation. F1: role→line is fixed and positional (main=1, cli=2,
|
|
164
|
+
* consulting=3 — `call-roster-store.ts`), so a background dispatch to a role
|
|
165
|
+
* whose line has never been touched must land EXACTLY on that role's number,
|
|
166
|
+
* never wherever happens to be free. `dispatchToBackgroundLine`
|
|
167
|
+
* (voice-call.ts) is the one production caller: "asking the CLI agent for
|
|
168
|
+
* something IS bringing it on" (design doc) means the line materializes on
|
|
169
|
+
* first use instead of reporting `unknown_line` for a line that simply
|
|
170
|
+
* hasn't been dispatched to yet.
|
|
171
|
+
*
|
|
172
|
+
* Returns `null` when `line` is outside the switchboard's
|
|
173
|
+
* 1..MAX_AGENT_LINES range, or when it is already occupied — the caller's
|
|
174
|
+
* own `getLine(line) === undefined` check is expected to make the second
|
|
175
|
+
* case unreachable in practice (this function is only ever called after
|
|
176
|
+
* that check fails), but the guard keeps the contract sound on its own
|
|
177
|
+
* terms rather than trusting every future caller to get the check right
|
|
178
|
+
* (§28).
|
|
179
|
+
*/
|
|
180
|
+
export declare const openAgentLineAt: (line: number, label: string) => AgentLine | null;
|
|
141
181
|
/**
|
|
142
182
|
* RELEASE a line — the mirror of `persistSlotBinding`'s adoption: clears the
|
|
143
183
|
* in-memory entry AND its durable slot together, so the freed number is
|
|
@@ -209,14 +249,21 @@ export declare const bindLineRun: (line: number, runId: string | null) => void;
|
|
|
209
249
|
* persisted slot together, so the ONE thing a page reload cannot observe by
|
|
210
250
|
* itself — "is this line's run still going?" — survives the reload instead
|
|
211
251
|
* of silently reverting to a lying "idle". `line-dispatch.ts`'s
|
|
212
|
-
* `attachLineReader` is the
|
|
213
|
-
*
|
|
214
|
-
*
|
|
252
|
+
* `attachLineReader` is the seam that SETS it, shared by a live dispatch and
|
|
253
|
+
* the boot re-attach (`reattachPendingLineRuns`) alike — same discipline as
|
|
254
|
+
* `persistSlotBinding`'s ADOPTION seam.
|
|
255
|
+
*
|
|
256
|
+
* There is a SECOND clearer (BEL-90): `run-line-progress.ts` calls this with
|
|
257
|
+
* `null` on every terminal, including for the session currently on screen.
|
|
258
|
+
* Without it, a background line whose run finished while the user was LOOKING
|
|
259
|
+
* at that line never reached `attachLineReader`'s terminal callbacks, so the
|
|
260
|
+
* slot kept a stale runId and every later reload rehydrated the chip as a
|
|
261
|
+
* permanently stuck "working" (§41).
|
|
215
262
|
*
|
|
216
|
-
* Line 1 never
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
263
|
+
* Line 1 never has this SET (line-dispatch.ts never dispatches to line 1 —
|
|
264
|
+
* see its module doc); its own run is resolved at stop-time from the
|
|
265
|
+
* stream-state store instead (`AgentLine.runId`'s doc). Clearing it for line 1
|
|
266
|
+
* is a harmless no-op, which is what keeps the terminal path above uniform.
|
|
220
267
|
*/
|
|
221
268
|
export declare const persistSlotRun: (line: number, runId: string | null) => void;
|
|
222
269
|
export declare const setLineStatus: (line: number, status: AgentLineStatus, lastText?: string) => void;
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Agent-initiated utterances (function acks, result relays, progress nudges,
|
|
10
10
|
* the greeting) are paced from AUDIO PLAYOUT END, not generation end: the next
|
|
11
|
-
* one may only dispatch once
|
|
12
|
-
* (`audioPlaying`)
|
|
13
|
-
*
|
|
11
|
+
* one may only dispatch once generation (`responseActive`), playback
|
|
12
|
+
* (`audioPlaying`), and the USER FLOOR (`userFloor` — the user is speaking, or
|
|
13
|
+
* only just stopped) are all idle, plus a breath (`UTTERANCE_GAP_MS`). User
|
|
14
|
+
* turns go through server VAD directly and never touch this logic.
|
|
14
15
|
*/
|
|
15
16
|
/**
|
|
16
17
|
* Breath between agent-initiated utterances, measured from AUDIO PLAYOUT END
|
|
@@ -29,13 +30,45 @@ export declare const UTTERANCE_GAP_MS = 700;
|
|
|
29
30
|
*/
|
|
30
31
|
export declare const AUDIO_IDLE_SAFETY_MS = 30000;
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* How long the user keeps the floor after `input_audio_buffer.speech_stopped`
|
|
34
|
+
* before an agent-initiated utterance may be released. Covers the case
|
|
35
|
+
* semantic VAD is deliberately patient about: a breath drawn mid-sentence,
|
|
36
|
+
* where the user has not actually finished. At 600ms (plus
|
|
37
|
+
* {@link UTTERANCE_GAP_MS}'s 700ms breath), an agent-initiated relay could
|
|
38
|
+
* start barely 1.3s after the user stopped — while semantic VAD is still
|
|
39
|
+
* deciding whether they are actually done. 1200ms buys the classifier more
|
|
40
|
+
* room before an agent-initiated utterance is allowed to land. This ONLY
|
|
41
|
+
* delays agent-*initiated* speech (progress nudges, result relays); the
|
|
42
|
+
* model's own response to the user's turn is untouched — that path never
|
|
43
|
+
* goes through this floor.
|
|
44
|
+
*/
|
|
45
|
+
export declare const USER_FLOOR_GRACE_MS = 1200;
|
|
46
|
+
/**
|
|
47
|
+
* Ceiling on how long the user floor may hold an utterance, measured from the
|
|
48
|
+
* moment the floor was TAKEN (`speech_started`). This is an ANTI-WEDGE
|
|
49
|
+
* fallback, not a tuning knob: {@link USER_FLOOR_GRACE_MS}'s grace timer
|
|
50
|
+
* already releases the floor shortly after every `speech_stopped`, so in
|
|
51
|
+
* ordinary speech the floor never survives to see this ceiling at all. Its
|
|
52
|
+
* only job is the case grace can't cover — a `speech_stopped` that never
|
|
53
|
+
* arrives (a genuinely stuck VAD) — which is why it must stay far above any
|
|
54
|
+
* real utterance: a tight ceiling here doesn't add safety, it just yanks the
|
|
55
|
+
* floor out from under a user who is still mid-sentence and dispatches a
|
|
56
|
+
* held relay over their voice, which is the exact bug this feature exists to
|
|
57
|
+
* prevent. Set at 45s — well above semantic VAD's own ceiling (8s at `low`
|
|
58
|
+
* eagerness, 4s at `medium`) — so it is reachable only when something has
|
|
59
|
+
* actually gone wrong, never in the course of normal speech. Same discipline
|
|
60
|
+
* as {@link AUDIO_IDLE_SAFETY_MS} — the pacer must never wedge.
|
|
61
|
+
*/
|
|
62
|
+
export declare const USER_FLOOR_MAX_HOLD_MS = 45000;
|
|
63
|
+
/**
|
|
64
|
+
* Hold a follow-up `response.create` while a response is still generating, its
|
|
65
|
+
* audio is still playing out, OR the user holds the floor. All three must be
|
|
66
|
+
* clear before the pause timer may run and the create may be sent.
|
|
35
67
|
*/
|
|
36
68
|
export declare const shouldHoldDispatch: (input: {
|
|
37
69
|
readonly responseActive: boolean;
|
|
38
70
|
readonly audioPlaying: boolean;
|
|
71
|
+
readonly userFloor: boolean;
|
|
39
72
|
}) => boolean;
|
|
40
73
|
/**
|
|
41
74
|
* On `response.done`, decide whether to release a queued follow-up immediately.
|
|
@@ -63,10 +96,15 @@ export type UtterancePacer = {
|
|
|
63
96
|
/** Audio playout ended (`output_audio_buffer.stopped`) — the anchor the next
|
|
64
97
|
* agent-initiated breath is measured from. */
|
|
65
98
|
readonly onAudioStopped: () => void;
|
|
66
|
-
/** The user took the floor (`input_audio_buffer.speech_started`).
|
|
67
|
-
*
|
|
68
|
-
*
|
|
99
|
+
/** The user took the floor (`input_audio_buffer.speech_started`). TAKES the
|
|
100
|
+
* user floor (arming the max-hold ceiling) and clears the playout gate and
|
|
101
|
+
* timers; a follow-up already counting down its breath is DEFERRED behind
|
|
102
|
+
* the user's turn (re-queued as pending), never dropped. */
|
|
69
103
|
readonly onBargeIn: () => void;
|
|
104
|
+
/** The user stopped talking (`input_audio_buffer.speech_stopped`). Starts
|
|
105
|
+
* the grace countdown; the floor is not released until it expires (or the
|
|
106
|
+
* model takes the turn, or the max-hold ceiling trips). */
|
|
107
|
+
readonly onUserSpeechStopped: () => void;
|
|
70
108
|
/** Call teardown: clear all flags and timers so nothing leaks forward. */
|
|
71
109
|
readonly reset: () => void;
|
|
72
110
|
};
|