@agentshouse/kit 0.1.0-alpha.27 → 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.27/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
@@ -446,7 +446,9 @@ kit setup
446
446
  ```
447
447
 
448
448
  `kit worker run` performs the same step at the start of a pass, before it
449
- hands out any work, so a managed Environment needs no terminal. The walk first
449
+ hands out any work, so a managed Environment needs no terminal; the resident
450
+ Kit performs it each time its control stream connects and on each `runs`
451
+ signal, which House also sends for every setup request. The walk first
450
452
  prepares the absolute `/agents/house` base, then visits every declared agent
451
453
  CLI and then every connector instance, in configuration order:
452
454
 
@@ -557,6 +559,43 @@ send one JSON line
557
559
  with `action: "authorization"` and `mode: "status"` or `mode: "login"`; the
558
560
  command returns exactly one JSON object such as `{ "authorized": true }`.
559
561
 
562
+ ## The Kit control stream
563
+
564
+ The resident Kit holds one `/kit/stream` WebSocket for its Environment,
565
+ authenticated with its Environment control lease and the Kit contract header,
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, 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.
573
+
574
+ Each time the socket connects, the resident acquires everything a signal could
575
+ have named, because a signal House sent while the socket was down is lost: its
576
+ Runs and resumptions with their setup and conversation files, the owner's
577
+ pending commands, the agent CLI list, the saved working copy choice and its
578
+ Google Sources. After that it acquires only what House names. A
579
+ `work_available` frame for `agents`, `working_copy` or `sources` acquires that
580
+ subject alone. `runs` takes a resumption or else a waiting Run, readying the
581
+ agent CLIs, a requested setup and conversation files first. `commands` hands
582
+ the supervised Session its stops, interrupts and answers and takes a
583
+ resumption the same way, without acquiring a Run. An `entries` frame, a change
584
+ in a Room the lease admits, delivers the working copies. A pass that took a Run
585
+ acquires again once that Run ends, and a pass that found nothing waits for the
586
+ next signal. A Run outcome House did not take is reported again every 30
587
+ seconds until House answers. The Kit has no idle acquisition cycle, no command
588
+ poll and no Session heartbeat: a Session is observed when its process starts
589
+ and when its supervisor sees it stop.
590
+
591
+ A socket that closes or never opens reconnects after a delay drawn between half
592
+ and all of one second, doubled for each consecutive failure up to 30 seconds.
593
+ An `authority_changed` close reconnects after a delay drawn between 0 and 2
594
+ seconds, with no error backoff. A `replaced` close, which means a newer
595
+ connection holds the Environment, ends the resident with `kit_stream_replaced`,
596
+ and an upgrade House refuses with `kit_upgrade_required` stops every House call
597
+ as [above](#connect-a-linux-computer) describes; neither is retried.
598
+
560
599
  ## The Worker runtime
561
600
 
562
601
  ```sh
@@ -831,37 +870,55 @@ what it has seen so far, and seals the turn itself. No Agent is asked to report
831
870
  text a deterministic observer already sees, and no agent-facing tool admits final
832
871
  text.
833
872
 
834
- While the adapter generates, the Kit posts the whole User-visible assistant text
835
- 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
836
876
  adapter-classified assistant message text enters that stream; reasoning, tool
837
- calls, plans, and terminal output never do. The Kit maps the structure it can
838
- read in that text - paragraphs, headings, fenced code, ordered and unordered
839
- lists, pipe tables, and `$$` formulas - into House's admitted blocks, and leaves
840
- anything it did not parse as the paragraph it observed. It invents no structure
841
- and renders nothing.
842
-
843
- The Kit coalesces and paces that stream rather than mirroring every chunk. It
844
- sends the first observed view at once, then at most one view every 500
845
- milliseconds, and one more the moment the adapter completes a message; one call
846
- is in flight at a time, so a newer view replaces the view it overtook and the
847
- view observed last is the view House sees last. A view whose text exceeds
848
- House's 4096-byte bound is not sent. A draft persists nowhere - not in this
849
- Kit's local state, not as House history - is never retried, and can neither
850
- delay nor prove turn completion: a refused, lost, or overtaken draft changes no
851
- Run, WorkerSession, or delivery state. Ending the stream waits for no draft the
852
- transport is still holding, so a slow or unanswered draft call cannot delay the
853
- 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.
854
910
 
855
911
  When the adapter completes the turn, the Kit seals it with one
856
- `sessions/<session>/assistant-messages` call carrying the same blocks and a
857
- UUIDv7 `operation_id`. The exact request - that identity and those blocks - is
858
- written into the WorkerSession binding before the call leaves, so a recovered
859
- call returns the outcome House recorded and never seals a turn twice, and
860
- House's answer names the turn it closed and the delivery it queued. An answer
861
- the Kit never received leaves the Run unsettled with the request retained: the
862
- next `kit worker run` replays exactly that operation and those blocks before
863
- settling anything, rather than reporting a failure it cannot prove. A refusal
864
- 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.
865
922
 
866
923
  That seal is the one durable record of the turn, and only a seal House accepted
867
924
  completes a turn: the Kit reports whole-turn completion to House after the seal
@@ -980,8 +1037,9 @@ A save that fails leaves one note in `state/conversation-originals/` naming the
980
1037
  attachment, its Run, the local file and the descriptor it declared. Each
981
1038
  later `kit worker run` retries it from that file without materializing or
982
1039
  acknowledging anything again, and reports `source_unavailable` when
983
- the file is gone or changed rather than saving different bytes. While a
984
- WorkerSession stays supervised the same retry runs at the heartbeat interval. The note leaves once
1040
+ the file is gone or changed rather than saving different bytes. When a
1041
+ supervised WorkerSession ends, the same retry runs once more before its Run is
1042
+ reported. The note leaves once
985
1043
  House records the save, once House answers that the Run is over, or when House
986
1044
  authorizes that Run's cache cleanup; House accepts no save for a Run that has
987
1045
  settled, so an unsaved original keeps its failed outcome from then on. The owner
@@ -1089,7 +1147,7 @@ A restore of House's database starts a new log epoch. Until this Environment
1089
1147
  has reported in it, House refuses every act that touches a Run, acquisition and
1090
1148
  lease release included, as `epoch_report_required` naming the epoch. The first
1091
1149
  such refusal ends whatever the pass was doing. One raised inside a supervised
1092
- 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
1093
1151
  replaces Session authority for a forwarded call, never becomes an answer to the
1094
1152
  Agent: it ends the bridge and the pass at once. The Kit then reports, under the
1095
1153
  lease it holds and at `POST /kit/sessions/report`, every Run it holds a spawn
@@ -1117,7 +1175,10 @@ written into the durable binding. Reconciliation must obtain current House
1117
1175
  authority before the live process can make further calls.
1118
1176
 
1119
1177
  An owner stop requested through House reaches this Kit as a durable command,
1120
- both at startup and while a Session is supervised. The Kit destroys the exact
1178
+ both at startup and while a Session is supervised: House signals `commands` on
1179
+ the Kit control stream and the supervisor acquires the pending stops,
1180
+ interrupts and answers at once, so a stop reaches the process within a second.
1181
+ The Kit destroys the exact
1121
1182
  tmux session, confirms that it is gone, and reports the stopped Session. It
1122
1183
  reports no continuation for a Run stopped by its owner and never reopens the
1123
1184
  local channel for that Session.
@@ -1278,8 +1339,8 @@ The Environment owner chooses in House which Rooms this Environment keeps
1278
1339
  locally, and whether it keeps `/private`. An Environment whose owner never chose
1279
1340
  copies nothing, and a Room joined later stays uncopied until the owner selects
1280
1341
  it. The resident Kit reads the saved choice through
1281
- `POST /kit/working-copy/selection` when it starts, whenever its control stream
1282
- connects or reconnects, and whenever House points it there after a save;
1342
+ `POST /kit/working-copy/selection` whenever its control stream connects or
1343
+ reconnects and whenever House points it there after a save;
1283
1344
  `kit worker run` reads it once before its pass.
1284
1345
 
1285
1346
  Each selected Room becomes one ordinary Git repository at
@@ -1308,8 +1369,9 @@ state. The House revision of every file content Kit received, keyed by path,
1308
1369
  Git blob and the House state that received it, stays in Kit home under
1309
1370
  `state/working-copies/`, outside the repository. A local commit is a local tree, never a House revision.
1310
1371
 
1311
- The resident Kit keeps every active copy current. On each pass, and whenever
1312
- it reads the saved choice, one `POST /kit/door/discover` tells it where each
1372
+ The resident Kit keeps every active copy current. Whenever its control stream
1373
+ carries a Room change, and whenever it reads the saved choice, one
1374
+ `POST /kit/door/discover` tells it where each
1313
1375
  copied Room stands, and for a Room that moved on it reads the changes after the
1314
1376
  copy's recorded position through `POST /kit/door/enumerate`. A changed, new or
1315
1377
  removed authored file becomes a commit on `refs/house/received`, its House
@@ -1328,7 +1390,8 @@ or protected paths House declares for the Room change, Kit reads the Room again
1328
1390
  as a reselection does. A position older than House
1329
1391
  retains, or from before House's database was restored, makes Kit read the Room
1330
1392
  again as a reselection does; while House's replica has not reached the position
1331
- the copy waits for the next pass. A Room House no longer answers under its
1393
+ the copy waits, and the resident delivers again 30 seconds later. A Room House
1394
+ no longer answers under its
1332
1395
  recorded handle makes Kit read the saved choice again, which follows a handle
1333
1396
  change or marks the copy inactive, and a Room that lost sync is named on
1334
1397
  standard error as `operation_denied` and keeps what it holds. `kit worker run`
@@ -1642,7 +1705,7 @@ kit telegram serve
1642
1705
 
1643
1706
  The command holds one Environment control lease for the whole run and performs
1644
1707
  three activities inside it, each paced on its own so none of them blocks
1645
- another:
1708
+ another, beside the live drafts described below:
1646
1709
 
1647
1710
  - **ingress** reads updates from the stored offset, normalizes each one into the
1648
1711
  update House accepts, submits it, and advances the stored offset only after
@@ -1663,15 +1726,39 @@ another:
1663
1726
  original, and reported the same way; a pull that fails reports
1664
1727
  `transfer_failed` and an ambiguous upload answer reports `delivery_unknown`.
1665
1728
  Only an image ever becomes a file item, because House offers every other file
1666
- as a `conversation_file_link` message this queue delivers like any other. Live drafts ride that same queue as their own item and are
1667
- never reported back, because a draft has no settlement: House has already
1668
- coalesced each conversation to its latest view, and the gateway sends the
1669
- drafts of a pass after its durable deliveries with the Bot API's native
1670
- `sendRichMessageDraft` on the bound forum topic thread, under the same bot-wide
1671
- `retry_after` gate. The gateway coalesces nothing again and holds no draft for
1672
- a later pass: a draft taken in a pass that gate closed is dropped, and the next
1673
- view House coalesces takes its place. Pacing lives here because this is where
1674
- 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.
1675
1762
 
1676
1763
  Each serve run first asks House to settle the attempts an earlier run abandoned,
1677
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.27",
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.27/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.27/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.27",
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 = "1";
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;
@@ -21,10 +21,11 @@ export const COMMAND_ADAPTER_SESSION_TIMEOUT_MS = 30_000;
21
21
  export const WORKER_EXIT_GRACE_MS = 5_000;
22
22
  export const WORKER_EXIT_POLL_MS = 100;
23
23
  export const WORKER_SUPERVISION_POLL_MS = 2_000;
24
- export const WORKER_COMMAND_POLL_MS = 2_000;
25
- export const WORKER_HEARTBEAT_MS = 30_000;
26
24
  export const WORKER_COMMAND_BATCH_MAX = 10;
27
25
  export const WORKER_RUN_TOKEN_MARGIN_MS = 60_000;
26
+ export const KIT_STREAM_RECONNECT_MS = 1_000;
27
+ export const KIT_STREAM_RECONNECT_MAX_MS = 30_000;
28
+ export const KIT_STREAM_REJOIN_MS = 2_000;
28
29
  export const TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES = 10 * 1024 * 1024;
29
30
  export const CONVERSATION_FILE_TRANSFER_TIMEOUT_MS = 30_000;
30
31
  export const WORKER_INHERITED_ENVIRONMENT = ["PATH", "HOME"];
@@ -47,14 +48,8 @@ export function runRequestPath(run) {
47
48
  export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
48
49
  export const KIT_SESSIONS_REPORT_MAX = 256;
49
50
 
50
- export const ASSISTANT_TEXT_MAX_BYTES = 4096;
51
- export const ASSISTANT_BLOCKS_MAX = 500;
52
51
  export const ASSISTANT_TABLE_COLUMNS_MAX = 20;
53
52
 
54
- export function assistantDraftsPath(workerSession) {
55
- return `/kit/sessions/${encodeURIComponent(workerSession)}/drafts`;
56
- }
57
-
58
53
  export function assistantTextPath(workerSession) {
59
54
  return `/kit/sessions/${encodeURIComponent(workerSession)}/assistant-messages`;
60
55
  }
@@ -65,6 +60,8 @@ export const TELEGRAM_DELIVERY_BATCH_MAX = 10;
65
60
  export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
66
61
  export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
67
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;
68
65
  export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
69
66
  export const TELEGRAM_MEDIA_KINDS = [
70
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
  }