@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 +136 -49
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +11 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +23 -13
- package/src/contract.js +6 -9
- package/src/draft.js +4 -82
- package/src/house.js +63 -58
- package/src/kit-work-stream.js +57 -27
- package/src/launcher.js +21 -6
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/tasks.js +21 -18
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +124 -53
- package/src/turn-frames.js +69 -0
- package/src/worker.js +144 -91
- package/src/working-copy.js +18 -9
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.
|
|
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
|
|
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
|
|
835
|
-
of the turn so far
|
|
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,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
and
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
|
857
|
-
UUIDv7 `operation_id`. The exact request - that identity
|
|
858
|
-
written into the WorkerSession binding before the
|
|
859
|
-
call returns the outcome House recorded and never
|
|
860
|
-
House's answer names the turn it closed and the
|
|
861
|
-
the Kit never received leaves the Run unsettled
|
|
862
|
-
next `kit worker run` replays exactly that
|
|
863
|
-
|
|
864
|
-
House did decide is the Run's
|
|
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.
|
|
984
|
-
WorkerSession
|
|
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,
|
|
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
|
|
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`
|
|
1282
|
-
|
|
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.
|
|
1312
|
-
it reads the saved choice, one
|
|
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
|
|
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.
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
the
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
-
|
|
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 !== ""
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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?.(
|
|
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 = "
|
|
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 {
|
|
2
|
-
|
|
3
|
-
const DRAFT_INTERVAL_MS = 500;
|
|
1
|
+
import { pacedLatest } from "./pacing.js";
|
|
4
2
|
|
|
5
3
|
export function observedAssistantDrafts(options) {
|
|
6
|
-
const
|
|
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
|
|
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
|
}
|