@agentshouse/kit 0.1.0-alpha.28 → 0.1.0-alpha.29

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/README.md CHANGED
@@ -12,7 +12,7 @@ The Skill set Kit installs is Matt Pocock's
12
12
  On Linux `amd64` or `arm64`, run the published, versioned bootstrap once:
13
13
 
14
14
  ```sh
15
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.28/connect-linux.sh)
15
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh)
16
16
  ```
17
17
 
18
18
  The bootstrap first establishes Docker Engine from what it observes on the
@@ -564,8 +564,12 @@ command returns exactly one JSON object such as `{ "authorized": true }`.
564
564
  The resident Kit holds one `/kit/stream` WebSocket for its Environment,
565
565
  authenticated with its Environment control lease and the Kit contract header,
566
566
  for as long as it holds that lease; `kit worker run` holds one for the length
567
- of its pass to receive its owner's commands. The Kit sends nothing on it and
568
- accepts every frame House sends.
567
+ of its pass to receive its owner's commands, and `kit telegram serve` holds one
568
+ while it serves the gateway under a lease of its own. The Kit sends no
569
+ subscribe frame and accepts every frame House sends. What it writes there is
570
+ the live view of the turns it runs, as draft and plan frames the
571
+ [Worker runtime](#the-worker-runtime) describes, and a `gap` frame when the
572
+ [Telegram gateway](#the-telegram-gateway) needs a turn's whole view again.
569
573
 
570
574
  Each time the socket connects, the resident acquires everything a signal could
571
575
  have named, because a signal House sent while the socket was down is lost: its
@@ -866,37 +870,55 @@ what it has seen so far, and seals the turn itself. No Agent is asked to report
866
870
  text a deterministic observer already sees, and no agent-facing tool admits final
867
871
  text.
868
872
 
869
- While the adapter generates, the Kit posts the whole User-visible assistant text
870
- of the turn so far to `sessions/<session>/drafts` as an ephemeral draft. Only
873
+ While the adapter generates, the supervisor writes the whole User-visible
874
+ assistant text of the turn so far as `draft` frames on the Environment's
875
+ [control stream](#the-kit-control-stream); it calls no draft route. Only
871
876
  adapter-classified assistant message text enters that stream; reasoning, tool
872
- calls, plans, and terminal output never do. The Kit maps the structure it can
873
- read in that text - paragraphs, headings, fenced code, ordered and unordered
874
- lists, pipe tables, and `$$` formulas - into House's admitted blocks, and leaves
875
- anything it did not parse as the paragraph it observed. It invents no structure
876
- and renders nothing.
877
-
878
- The Kit coalesces and paces that stream rather than mirroring every chunk. It
879
- sends the first observed view at once, then at most one view every 500
880
- milliseconds, and one more the moment the adapter completes a message; one call
881
- is in flight at a time, so a newer view replaces the view it overtook and the
882
- view observed last is the view House sees last. A view whose text exceeds
883
- House's 4096-byte bound is not sent. A draft persists nowhere - not in this
884
- Kit's local state, not as House history - is never retried, and can neither
885
- delay nor prove turn completion: a refused, lost, or overtaken draft changes no
886
- Run, WorkerSession, or delivery state. Ending the stream waits for no draft the
887
- transport is still holding, so a slow or unanswered draft call cannot delay the
888
- seal or the Run behind it.
877
+ calls, and terminal output never do. The Kit maps the structure it can read in
878
+ that text - paragraphs, headings, fenced code, ordered and unordered lists, pipe
879
+ tables, and `$$` formulas - into House's blocks, and leaves anything it did not
880
+ parse as the paragraph it observed. It invents no structure, renders nothing,
881
+ and holds back or refuses no draft for its length: a frame carries every block
882
+ of the text however long it is.
883
+
884
+ A frame names the turn it belongs to, as House answered the Session's `started`
885
+ observation with the turn's `conversation_id` and `turn_id`, together with the
886
+ Execution attempt as `attempt_id`. It carries a `sequence` that grows by one
887
+ with each paced view of the turn, and replaces the turn's view from the block
888
+ index `from` onward with the `blocks` it carries, so a frame for text that only
889
+ grew carries the blocks from the first one that changed, and a view that
890
+ changed no block is a frame that replaces nothing. The launcher numbers the
891
+ views, so a Kit that restarts mid-turn continues the turn's sequence. The Kit
892
+ coalesces and paces that stream rather than mirroring every chunk: it writes
893
+ the first observed view at once and then at most one view in each
894
+ 500-millisecond pacing window, the latest one, even when the adapter completes
895
+ a message inside a window. A frame written while the socket is down is lost,
896
+ so the first frame after each reconnect, and after any frame the socket could not carry, replaces the view
897
+ from block 0. A Session House names no conversation turn for gets no frames. A
898
+ draft persists nowhere - not in this Kit's local state, not as House history -
899
+ is never retried, and can neither delay nor prove turn completion.
900
+
901
+ A `plan` session update is one snapshot of the agent's steps. The Kit writes
902
+ each snapshot as a `plan` frame paced by the same rule, carrying every step's
903
+ `label`, `priority` and `status` exactly as the adapter gave them, its own
904
+ `sequence`, and the step index `from` it replaces the plan onward. The act that
905
+ ends the turn carries the last snapshot the Kit saw as its final `plan`: the
906
+ seal, the observed stop, the Run outcome and an `interrupted` interrupt outcome
907
+ each name the provider turn and every step's `label` and `status`, however many
908
+ steps and however long their text, and a turn that never planned carries none.
909
+ There is no per-snapshot plan route.
889
910
 
890
911
  When the adapter completes the turn, the Kit seals it with one
891
- `sessions/<session>/assistant-messages` call carrying the same blocks and a
892
- UUIDv7 `operation_id`. The exact request - that identity and those blocks - is
893
- written into the WorkerSession binding before the call leaves, so a recovered
894
- call returns the outcome House recorded and never seals a turn twice, and
895
- House's answer names the turn it closed and the delivery it queued. An answer
896
- the Kit never received leaves the Run unsettled with the request retained: the
897
- next `kit worker run` replays exactly that operation and those blocks before
898
- settling anything, rather than reporting a failure it cannot prove. A refusal
899
- House did decide is the Run's reported failure.
912
+ `sessions/<session>/assistant-messages` call carrying the same blocks, the final
913
+ plan, and a UUIDv7 `operation_id`. The exact request - that identity, those
914
+ blocks and that plan - is written into the WorkerSession binding before the
915
+ call leaves, so a recovered call returns the outcome House recorded and never
916
+ seals a turn twice, and House's answer names the turn it closed and the
917
+ delivery it queued. An answer the Kit never received leaves the Run unsettled
918
+ with the request retained: the next `kit worker run` replays exactly that
919
+ operation, those blocks and that plan before settling anything, rather than
920
+ reporting a failure it cannot prove. A refusal House did decide is the Run's
921
+ reported failure.
900
922
 
901
923
  That seal is the one durable record of the turn, and only a seal House accepted
902
924
  completes a turn: the Kit reports whole-turn completion to House after the seal
@@ -1125,7 +1147,7 @@ A restore of House's database starts a new log epoch. Until this Environment
1125
1147
  has reported in it, House refuses every act that touches a Run, acquisition and
1126
1148
  lease release included, as `epoch_report_required` naming the epoch. The first
1127
1149
  such refusal ends whatever the pass was doing. One raised inside a supervised
1128
- Run's bridge, while it admits or settles a question, streams a draft, or
1150
+ Run's bridge, while it admits or settles a question, reports an interrupt, or
1129
1151
  replaces Session authority for a forwarded call, never becomes an answer to the
1130
1152
  Agent: it ends the bridge and the pass at once. The Kit then reports, under the
1131
1153
  lease it holds and at `POST /kit/sessions/report`, every Run it holds a spawn
@@ -1683,7 +1705,7 @@ kit telegram serve
1683
1705
 
1684
1706
  The command holds one Environment control lease for the whole run and performs
1685
1707
  three activities inside it, each paced on its own so none of them blocks
1686
- another:
1708
+ another, beside the live drafts described below:
1687
1709
 
1688
1710
  - **ingress** reads updates from the stored offset, normalizes each one into the
1689
1711
  update House accepts, submits it, and advances the stored offset only after
@@ -1704,15 +1726,39 @@ another:
1704
1726
  original, and reported the same way; a pull that fails reports
1705
1727
  `transfer_failed` and an ambiguous upload answer reports `delivery_unknown`.
1706
1728
  Only an image ever becomes a file item, because House offers every other file
1707
- as a `conversation_file_link` message this queue delivers like any other. Live drafts ride that same queue as their own item and are
1708
- never reported back, because a draft has no settlement: House has already
1709
- coalesced each conversation to its latest view, and the gateway sends the
1710
- drafts of a pass after its durable deliveries with the Bot API's native
1711
- `sendRichMessageDraft` on the bound forum topic thread, under the same bot-wide
1712
- `retry_after` gate. The gateway coalesces nothing again and holds no draft for
1713
- a later pass: a draft taken in a pass that gate closed is dropped, and the next
1714
- view House coalesces takes its place. Pacing lives here because this is where
1715
- the Bot API is called.
1729
+ as a `conversation_file_link` message this queue delivers like any other. The
1730
+ queue carries no draft: an answer holding a draft item is refused as
1731
+ `kit_telegram_response_invalid`. House caps no message, so the gateway alone
1732
+ fits one to Telegram's rich message limits, 32,768 characters and 500 blocks:
1733
+ a rich message past them is sent as consecutive messages in order, split
1734
+ between blocks and inside a block only when that block alone exceeds them,
1735
+ and reported once, `delivered` with the first message's id when every part
1736
+ was delivered and otherwise with the answer of the first part that was not.
1737
+ Each answer also names the `threads` the bot's topics carry, a
1738
+ `conversation_id` with its `chat_id` and `topic_id`, which is where live
1739
+ drafts go.
1740
+
1741
+ Live drafts come from the control stream, not from House's queue. Run on its
1742
+ own, the command holds the Environment's [control stream](#the-kit-control-stream)
1743
+ for its lease; served by `kit resident`, it receives the draft and plan frames
1744
+ the resident's socket receives, through the resident, and never opens a second
1745
+ socket. The gateway Environment's socket carries every conversation bound to a
1746
+ topic of the bot, with no subscribe frame. The gateway keeps the latest view of
1747
+ each such conversation's turn: a frame that starts at block 0 replaces a view,
1748
+ the next frame in sequence applies from its `from`, and a frame after a gap in
1749
+ the sequence, or one for a turn it holds no view of, is applied nowhere and
1750
+ answered with one `gap` frame naming the conversation and turn, so House sends
1751
+ the whole view again. It renders each view as one Telegram draft per turn with
1752
+ `sendRichMessageDraft` on the bound topic, the live plan above the assistant
1753
+ text, under one draft id derived from the turn, because Telegram shows one bot
1754
+ draft per topic and a second id would replace the first. A view past the rich
1755
+ message limits is shortened inside that one draft, never split into a second.
1756
+ A live plan is part of the draft and never a sent message; the frozen plan
1757
+ House queues at the turn's end is the turn's one plan message. Drafts coalesce
1758
+ to the latest view, one Bot API call at a time, and are never reported back:
1759
+ the same bot-wide `retry_after` gate drops a draft while it is closed, and a
1760
+ draft answered with `retry_after` closes it. A closed gate drops a preview,
1761
+ never a durable delivery.
1716
1762
 
1717
1763
  Each serve run first asks House to settle the attempts an earlier run abandoned,
1718
1764
  so a restarted gateway never leaves a delivery attempted forever. A Telegram
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.29",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.28/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.28/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.29",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/binding.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdir, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { finalPlan, planSnapshot } from "./plan.js";
3
4
  import { KitRefusal } from "./refusal.js";
4
5
  import { readNote, writeNote } from "./state.js";
5
6
  import { reportedNote, retainedReport } from "./terminal.js";
@@ -43,6 +44,7 @@ export async function writeBinding(directory, binding) {
43
44
  : { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
44
45
  stopped: stoppedNote(binding.stopped),
45
46
  reported: reported === null ? null : reportedNote(reported),
47
+ plan: binding.plan ?? null,
46
48
  });
47
49
  }
48
50
 
@@ -58,6 +60,12 @@ function retainedSeal(stored) {
58
60
  return { operation: stored.operation, blocks: stored.blocks };
59
61
  }
60
62
 
63
+ function retainedPlan(stored) {
64
+ if (stored === null || stored === undefined) return null;
65
+ const plan = finalPlan(planSnapshot(stored));
66
+ return plan === null || JSON.stringify(plan) !== JSON.stringify(stored) ? undefined : plan;
67
+ }
68
+
61
69
  function retainedFailure(stored) {
62
70
  if (stored === null) return null;
63
71
  if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
@@ -96,6 +104,8 @@ function completeBinding(directory, stored) {
96
104
  if (stopped === undefined) return null;
97
105
  const reported = retainedReport(stored.reported);
98
106
  if (reported === undefined) return null;
107
+ const plan = retainedPlan(stored.plan);
108
+ if (plan === undefined) return null;
99
109
  return {
100
110
  run: stored.run,
101
111
  session: stored.session,
@@ -107,6 +117,7 @@ function completeBinding(directory, stored) {
107
117
  sealed,
108
118
  stopped,
109
119
  reported,
120
+ plan,
110
121
  };
111
122
  }
112
123
 
package/src/blocks.js CHANGED
@@ -1,8 +1,4 @@
1
- import {
2
- ASSISTANT_BLOCKS_MAX,
3
- ASSISTANT_TABLE_COLUMNS_MAX,
4
- ASSISTANT_TEXT_MAX_BYTES,
5
- } from "./contract.js";
1
+ import { ASSISTANT_TABLE_COLUMNS_MAX } from "./contract.js";
6
2
 
7
3
  const HEADING = /^(#{1,6})\s+(.+)$/;
8
4
  const BULLET = /^ {0,3}[-*+]\s+(.+)$/;
@@ -132,25 +128,5 @@ export function assistantBlocks(text) {
132
128
  index += 1;
133
129
  }
134
130
  endParagraph();
135
- if (blocks.length === 0) return null;
136
- if (blocks.length <= ASSISTANT_BLOCKS_MAX) return blocks;
137
- const whole = text.trim();
138
- return whole === "" ? null : [{ type: "paragraph", text: whole }];
139
- }
140
-
141
- export function blocksText(blocks) {
142
- return blocks
143
- .map((block) => {
144
- if (block.type === "formula") return block.expression;
145
- if (block.type === "list") return block.items.join("\n");
146
- if (block.type === "table") {
147
- return [block.headers, ...block.rows].map((row) => row.join("\t")).join("\n");
148
- }
149
- return block.text;
150
- })
151
- .join("\n\n");
152
- }
153
-
154
- export function withinAssistantBound(text) {
155
- return Buffer.byteLength(text, "utf8") <= ASSISTANT_TEXT_MAX_BYTES;
131
+ return blocks.length === 0 ? null : blocks;
156
132
  }
package/src/bridge.js CHANGED
@@ -16,8 +16,13 @@ import {
16
16
  TURN_OUTCOMES,
17
17
  } from "./contract.js";
18
18
  import { operationId } from "./operation.js";
19
+ import { planSnapshot } from "./plan.js";
19
20
  import { houseFailed } from "./refusal.js";
20
21
 
22
+ function sequenced(value) {
23
+ return Number.isSafeInteger(value) && value > 0;
24
+ }
25
+
21
26
  export function bridgedFrame(request, operation) {
22
27
  if (typeof request.method !== "string") return request;
23
28
  const params = typeof request.params === "object" && request.params !== null ? request.params : {};
@@ -50,6 +55,7 @@ export async function openBridge(options) {
50
55
  closed: false,
51
56
  questions: new Map(),
52
57
  interrupts: new Set(),
58
+ plan: null,
53
59
  };
54
60
  const connections = new Set();
55
61
  const clients = new Map();
@@ -201,29 +207,31 @@ export async function openBridge(options) {
201
207
  return;
202
208
  }
203
209
  if (frame.type === "draft") {
204
- if (typeof frame.text === "string" && frame.text !== "") options.draft?.(frame.text);
210
+ if (typeof frame.text === "string" && frame.text !== "" && sequenced(frame.sequence)) {
211
+ options.draft?.({ text: frame.text, sequence: frame.sequence });
212
+ }
205
213
  return;
206
214
  }
207
- if (
208
- frame.type === "plan" &&
209
- typeof frame.provider_turn === "string" &&
210
- Array.isArray(frame.steps) &&
211
- typeof frame.terminal === "boolean"
212
- ) {
213
- void options.plan?.({
214
- provider_turn: frame.provider_turn,
215
- steps: frame.steps,
216
- terminal: frame.terminal,
217
- });
215
+ if (frame.type === "plan") {
216
+ const snapshot = planSnapshot(frame);
217
+ if (snapshot === null || !sequenced(frame.sequence)) return;
218
+ state.plan = snapshot;
219
+ options.plan?.({ ...snapshot, sequence: frame.sequence });
218
220
  return;
219
221
  }
222
+ const carried = planSnapshot(frame.plan);
223
+ if (carried !== null) state.plan = carried;
220
224
  if (
221
225
  frame.type === "interrupt_applied" &&
222
226
  typeof frame.interrupt === "string" &&
223
227
  ["interrupted", "nothing_to_interrupt"].includes(frame.outcome) &&
224
228
  state.interrupts.has(frame.interrupt)
225
229
  ) {
226
- const reported = options.interrupt?.(frame.interrupt, frame.outcome);
230
+ const reported = options.interrupt?.(
231
+ frame.interrupt,
232
+ frame.outcome,
233
+ frame.outcome === "interrupted" ? state.plan : null,
234
+ );
227
235
  if (reported === undefined) state.interrupts.delete(frame.interrupt);
228
236
  else void Promise.resolve(reported).finally(() => state.interrupts.delete(frame.interrupt));
229
237
  return;
@@ -237,6 +245,7 @@ export async function openBridge(options) {
237
245
  providerContinuation: frame.provider_continuation === true,
238
246
  providerSessionId: typeof frame.provider_session_id === "string" && frame.provider_session_id.length > 0 && frame.provider_session_id.length <= 512 ? frame.provider_session_id : null,
239
247
  stopOutcome: TURN_OUTCOMES.includes(frame.stop_outcome) ? frame.stop_outcome : null,
248
+ plan: carried,
240
249
  };
241
250
  return;
242
251
  }
@@ -436,6 +445,7 @@ export async function openBridge(options) {
436
445
  .filter((interaction) => interaction !== null),
437
446
  assistant: () => [...state.assistant],
438
447
  stop: () => state.stop,
448
+ plan: () => state.plan,
439
449
  failure: () => state.failure,
440
450
  stopRecovery: () => lifetime.abort(),
441
451
  async close() {
package/src/contract.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
2
2
 
3
- export const KIT_CONTRACT_VERSION = "2";
3
+ export const KIT_CONTRACT_VERSION = "3";
4
4
  export const KIT_CAPTURE_BATCH_MAX = 100;
5
5
  export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
6
6
  export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
@@ -48,14 +48,8 @@ export function runRequestPath(run) {
48
48
  export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
49
49
  export const KIT_SESSIONS_REPORT_MAX = 256;
50
50
 
51
- export const ASSISTANT_TEXT_MAX_BYTES = 4096;
52
- export const ASSISTANT_BLOCKS_MAX = 500;
53
51
  export const ASSISTANT_TABLE_COLUMNS_MAX = 20;
54
52
 
55
- export function assistantDraftsPath(workerSession) {
56
- return `/kit/sessions/${encodeURIComponent(workerSession)}/drafts`;
57
- }
58
-
59
53
  export function assistantTextPath(workerSession) {
60
54
  return `/kit/sessions/${encodeURIComponent(workerSession)}/assistant-messages`;
61
55
  }
@@ -66,6 +60,8 @@ export const TELEGRAM_DELIVERY_BATCH_MAX = 10;
66
60
  export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
67
61
  export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
68
62
  export const TELEGRAM_GATEWAY_POLL_MS = 2_000;
63
+ export const TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX = 32_768;
64
+ export const TELEGRAM_RICH_MESSAGE_BLOCKS_MAX = 500;
69
65
  export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
70
66
  export const TELEGRAM_MEDIA_KINDS = [
71
67
  "document",
package/src/draft.js CHANGED
@@ -1,89 +1,11 @@
1
- import { assistantBlocks, blocksText, withinAssistantBound } from "./blocks.js";
2
-
3
- const DRAFT_INTERVAL_MS = 500;
1
+ import { pacedLatest } from "./pacing.js";
4
2
 
5
3
  export function observedAssistantDrafts(options) {
6
- const intervalMs = options.intervalMs ?? DRAFT_INTERVAL_MS;
7
- const now = options.now ?? (() => Date.now());
8
- let timer = null;
9
- let sentAt = null;
10
- let sent = null;
11
- let latest = null;
12
-
13
- function write() {
14
- timer = null;
15
- if (latest === null || latest === sent) return;
16
- sent = latest;
17
- sentAt = now();
18
- options.send(sent);
19
- }
20
-
21
- function cancel() {
22
- if (timer === null) return;
23
- clearTimeout(timer);
24
- timer = null;
25
- }
26
-
4
+ const paced = pacedLatest(options);
27
5
  return {
28
6
  observe(view) {
29
- if (typeof view !== "string" || view === "") return;
30
- latest = view;
31
- if (timer !== null) return;
32
- const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
33
- if (wait <= 0) {
34
- write();
35
- return;
36
- }
37
- timer = setTimeout(write, wait);
38
- timer.unref?.();
39
- },
40
- complete(view) {
41
- if (typeof view !== "string" || view === "") return;
42
- latest = view;
43
- cancel();
44
- write();
45
- },
46
- close: cancel,
47
- };
48
- }
49
-
50
- export function openAssistantDrafts(options) {
51
- let pending = null;
52
- let sending = null;
53
- let sent = null;
54
- let closed = false;
55
-
56
- function pump() {
57
- if (sending !== null || pending === null) return;
58
- const view = pending;
59
- pending = null;
60
- sent = view.text;
61
- let started;
62
- try {
63
- started = Promise.resolve(options.send(view.blocks));
64
- } catch {
65
- started = Promise.resolve();
66
- }
67
- sending = started
68
- .catch(() => undefined)
69
- .finally(() => {
70
- sending = null;
71
- pump();
72
- });
73
- }
74
-
75
- return {
76
- update(text) {
77
- if (closed || typeof text !== "string" || text === "") return;
78
- if (text === sent || text === pending?.text) return;
79
- const blocks = assistantBlocks(text);
80
- if (blocks === null || !withinAssistantBound(blocksText(blocks))) return;
81
- pending = { text, blocks };
82
- pump();
83
- },
84
- close() {
85
- closed = true;
86
- pump();
7
+ if (typeof view === "string" && view !== "") paced.observe(view);
87
8
  },
9
+ close: paced.close,
88
10
  };
89
11
  }