Repository navigation
Expand file tree
/
Copy pathsession.go
More file actions
3704 lines (3499 loc) · 202 KB
/
Copy pathsession.go
File metadata and controls
3704 lines (3499 loc) · 202 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Package session is the v3 conversational agent: a working loop
// you talk to, not a dispatcher. It owns one conversation against one
// workspace: the person submits messages, the agent works (read, bash, edit,
// write, grep, find, ls, todo) and streams what it does as events.
//
// The seams are deliberate and narrow. The agent talks to a provider through
// Completer (one method), and to the person through a channel of Events. The
// tasker does not exist here yet: when it attaches, it arrives as extra tools
// (task/change/stop → store.RequestCommand) registered beside the working
// ones, and nothing in this file changes.
//
// The loop's wire behavior — message assembly, stop condition, retry
// schedule, tool parallelism — follows internal/exec/bare, with
// three deliberate differences: it is interactive (Submit between turns, not
// one task to the end), interruptible (Interrupt cancels the in-flight turn
// and keeps the partial), and its compaction follows the implementation
// Decision 9 (omp's architecture: threshold = window − max(15%, 16k), keep
// 20k tokens verbatim, one LLM summary with omp's section contract, the pass
// journaled as a transcript marker).
package session
import (
"context"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/crewroute"
"github.com/Agent-Field/codeaf/internal/knowledge"
"sync"
"sync/atomic"
"time"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/codeaf/internal/approval"
"github.com/Agent-Field/codeaf/internal/connect"
"github.com/Agent-Field/codeaf/internal/delegate"
"github.com/Agent-Field/codeaf/internal/effort"
"github.com/Agent-Field/codeaf/internal/exec"
"github.com/Agent-Field/codeaf/internal/exec/bare"
procexec "github.com/Agent-Field/codeaf/internal/executor"
"github.com/Agent-Field/codeaf/internal/modelsource"
"github.com/Agent-Field/codeaf/internal/offpath"
"github.com/Agent-Field/codeaf/internal/provider"
"github.com/Agent-Field/codeaf/internal/roles"
"github.com/Agent-Field/codeaf/internal/search"
"github.com/Agent-Field/codeaf/internal/store"
"github.com/Agent-Field/codeaf/internal/subharness"
"github.com/Agent-Field/codeaf/internal/taxonomy"
)
// Completer is the narrow slice of provider.Client the loop needs. It is an
// interface so tests substitute a scripted completer.
type Completer interface {
CompleteWithMessages(ctx context.Context, messages []ai.Message, options ...ai.Option) (*ai.Response, error)
}
// modelChain is the OPTIONAL half of a [Completer]: which models it would move
// to when the one in hand can no longer answer, in order.
//
// It is a second interface rather than a second method on [Completer] because a
// chain is a thing only the real adapter has (internal/provider's
// FallbackModels). A completer that does not offer one — a test double, a build
// wired to no catalog and no `models.fallbacks` row — makes the hop ABSENT: the
// turn ends on the sentence it has always ended on, rather than on a capability
// that is present and fails.
type modelChain interface {
FallbackModels(model string) []string
}
// EventKind names one thing the person can see happening.
type EventKind int
const (
// EventTextDelta carries one streamed chunk of the assistant's reply in Text.
EventTextDelta EventKind = iota
// EventThinking says the model is reasoning; it carries no text.
EventThinking
// EventToolBegin carries the tool name in Tool and a person-readable gloss
// in Hint — "read internal/session/session.go", "bash go build ./…". Args
// carries the call's arguments for a surface that expands the row; Output
// is empty, the call has not run yet.
EventToolBegin
// EventToolEnd carries the tool name, a short result hint (often empty),
// and the call's Args and Output for expansion.
EventToolEnd
// EventToolFailed carries the tool name and why, with the same Args and
// Output as EventToolEnd — a failure is the one result worth reading in
// full, and the surface has it here without asking.
EventToolFailed
// EventTurnDone ends one Submit's stream; Usage is the turn's total.
EventTurnDone
// EventError ends the turn abnormally; Err says why.
EventError
// EventCompacting says a compaction pass has edited the transcript, which is
// work a surface should show rather than silence. Hint sizes the pass
// ("compacting ~84k tokens").
//
// EventCompacted always follows it — a surface opens a row on this one and
// settles it on that one. A pass that found nothing to do sends neither, so
// it opens no row that would need settling (loop.go's [Agent.compactWithPolicy]);
// `/compact` hears that from [ErrNothingToCompact] instead. Most passes are
// two mechanical walks over messages this session already holds, so what
// they cost is a lock; only a pass those walks cannot finish writes a summary
// with the conversation's model (compact_summary.go).
EventCompacting
// EventCompacted marks a compaction pass; Hint summarizes
// ("compacted from ~84k tokens, kept last ~20k"). [Event.Unchanged] is only
// ever set on it by a peer built before a no-op pass went silent.
EventCompacted
// EventReasoning carries one streamed chunk of the model's REASONING in
// Text, for the models that put their working on the wire (OpenRouter's
// "reasoning", the DeepSeek family's "reasoning_content").
//
// It follows the EventThinking that opened the run rather than replacing it:
// a surface that only draws "thinking…" ignores this kind and is unchanged,
// and a surface that shows the thought has the words and the boundary both.
// The text is NOT part of the answer and never enters the partial reply.
// Completed steps record it as hidden provider metadata and replay it under
// the same wire field, so the next tool step can continue the model's work
// without presenting that work as something the assistant said out loud.
EventReasoning
// EventConsentRequest asks the person whether one tool call may run
// (consent.go). It carries the call's ID, Tool, Args and gloss in Hint, and
// the policy's own phrasing of why it is asking in Rule. Wait is
// ConsentWaiting: silence is not a no.
//
// It is a QUESTION, not a report: the call is blocked inside the tool batch
// until [Agent.ResolveConsent] answers it or the turn's context dies, and a
// surface that ignores this kind leaves the turn waiting until the person
// interrupts. It arrives AFTER the batch's EventToolBegin rows, so a surface
// attaches the question to the row it already drew for that call.
EventConsentRequest
// EventTitleChanged carries the session's name in Text (title.go). It fires
// at most once per session — after the first completed turn, when the
// session had no name yet.
EventTitleChanged
// EventToolAnnounced says one tool call has finished ARRIVING — the model
// has sent the whole instruction — while the response it rides on is still
// streaming. It carries the same Tool, Hint and Args EventToolBegin will,
// and no Output: nothing has run.
//
// EventTaskProposal asks the person whether one groomed piece of work may
// become a task node (task.go). It carries the proposal in Task: title,
// the two-or-three-line summary, the full brief, and the auto-approve
// deadline.
//
// It is a QUESTION with a CLOCK, not a report: the propose_task call is
// blocked until [Agent.ResolveTask] answers it or the deadline passes, and
// the deadline passing means APPROVED — the surface is the person's chance
// to redirect, never a gate the work waits on forever. A surface with no
// answer box for this kind still works: the countdown approves.
EventTaskProposal
// EventTaskUpdate reports one task node's progress (task.go): Task carries
// the state (running, done, failed), the elapsed time, and on completion
// the report, the changed files, and the merge outcome. It is a report,
// never a question; the first update (running) arrives as the proposal
// resolves.
EventTaskUpdate
// EventToolAnnounced says one tool call has finished ARRIVING — the model
// has sent the whole instruction — while the response it rides on is still
// streaming. It carries the same Tool, Hint and Args EventToolBegin will,
// the CallID its forming events carried, and no Output: nothing has run.
//
// It is the difference between "asked for" and "started", and it exists
// because those two moments can be seconds apart. A mutating call is
// announced here and does not begin until the response completes and the
// batch starts (loop.go's safety law), so a surface that only had
// EventToolBegin had to choose between drawing nothing for that gap or
// drawing a spinner for work that had not started. Both are lies; this is
// the third option.
//
// EventToolBegin keeps its exact meaning: EXECUTION STARTED. Every call that
// is announced is also begun, in the same order, so a surface that ignores
// this kind is unchanged — and a provider that never announces (a
// non-streaming endpoint) simply sends no event of this kind.
EventToolAnnounced
// EventToolForming says one tool call is still ARRIVING — the model is
// spelling it out and has not finished. It is the phase BEFORE
// EventToolAnnounced, and it exists because that gap is not instant: a long
// write or a groomed propose_task takes seconds to stream, and a surface
// with only the announcement draws nothing at all for them.
//
// It carries CallID (the call's id once the wire has said one), Tool (the
// name once its delta has landed), Hint (a best-effort gloss built from the
// argument fields that have CLOSED so far — "write internal/foo.go" while the
// body of the file is still arriving), ArgsText (the raw partial arguments)
// and Bytes (how much of them has arrived).
//
// NOTHING HERE IS AN INSTRUCTION. ArgsText is half-sent JSON and is never
// parsed into Args; Hint is a scan, not an unmarshal; and forming NEVER
// implies execution — a formed call has not been announced, let alone begun,
// let alone consented to.
//
// ORDERING: forming (zero or more, per call) → EventToolAnnounced →
// EventToolBegin, keyed by CallID. Every call that forms is announced and
// begun in that order; calls in a parallel batch interleave with each other,
// but each call's own sequence holds. A non-streaming provider forms nothing,
// so a surface that ignores this kind is exactly what it was.
EventToolForming
// EventGuardianAllowed says a call the policy would have ASKED about ran
// because the guardian model vouched for it (guardian.go). It carries the
// Tool, the call's gloss in Hint and Args, and the rule that would have
// prompted in Rule.
//
// It is an ANNOTATION, not a question and not a result: the row it belongs to
// is the ordinary tool row, and this is the dim line beside it saying who
// answered instead of the person. A surface that ignores this kind shows a
// call that simply ran, which is what it did — but a gate that answers on
// somebody's behalf and says nothing about it is a gate nobody can audit, so
// the event exists whether or not a given surface draws it.
EventGuardianAllowed
// EventNudge says the turn has been caught going in circles (looped.go): Tool
// is the call that repeated, Count is how many times. A surface renders it as
// "stuck? nudged · <tool> ×N" for the warning rungs.
//
// The first two nudges are notes in the transcript, not errors or refusals.
// Past that ceiling the same event accompanies the checkpoint take-over rather
// than a third note. This event is how a person gets to SEE either happen.
EventNudge
// EventNotice carries one line in Text about what the turn's own machinery is
// doing to make the request land — not the model's words, and not a failure.
//
// Its one source today is the provider's endpoint-refusal chain
// (internal/provider's endpoints.go): "Retry 1/3: removed max_tokens",
// "Retry 3/3: Falling back to <model>". Those retries change the shape of the
// request a person asked for, so a surface that drew nothing for them would
// be showing an answer without showing what it cost to get one.
//
// It is a NOTE, like EventNudge: dim, one line, never an interruption. It can
// arrive before any text on the turn, and a turn may end in EventError with
// several of these already on screen — that sequence is the chain trying
// everything it had and saying so.
EventNotice
// EventConnectAsk asks the person whether one of their accounts may be
// connected (connect.go). It carries the id the answer is handed back with in
// ConnectID, and the account in Service and ServiceName — "google" and
// "Google", the word the tools use and the word a person reads.
//
// It is a QUESTION, and the same kind of question a consent prompt is: the
// use_service call is blocked inside the tool batch until
// [Agent.ResolveConnect] answers it, the five-minute clock runs out, or the
// turn's context dies. A surface that ignores this kind leaves the call
// waiting until one of those three happens, and a clock that runs out is a NO.
EventConnectAsk
// EventConnectAuth carries the page the person opens to say yes to the
// service named in Service: the address is in AuthURL.
//
// It is an INSTRUCTION to the surface — open this — and it arrives only after
// the person has already agreed to connect the account. It is followed by
// exactly one EventConnectDone, whatever happens next.
EventConnectAuth
// EventConnectDone ends one connect attempt for the service in Service:
// Account is the address it connected as, and Failed says it did not connect
// at all. The two are exclusive — a failure carries no account — and a
// person who simply walked away shows up here as a failure, because from
// this side an attempt nobody finished and an attempt that broke are the same
// fact: nothing is connected.
EventConnectDone
// EventHarnessOffer asks the person whether one sub-harness should take this
// turn (harness.go). It carries the id the answer is handed back with in ID,
// the harness's name in Text, and its one-sentence description in Hint.
//
// Model is the model the turn NAMED — "research this with opus" — resolved
// to an id this install has, and empty when nobody said. ModelNote is the
// other half of that: a word that named no model here, said in words a
// surface prints as it stands. Neither is a refusal; the offer is the same
// offer either way.
//
// It is a QUESTION, and the quietest kind on this list: the turn is held
// before its first request until [Agent.ResolveHarness] answers it or the
// turn's context dies, and NO is free — the turn the person typed runs
// exactly as it would have. A surface that ignores this kind would leave the
// turn waiting, which is why the offer is never raised unless somebody has
// said they are watching (Config.AskConsent).
EventHarnessOffer
// EventHarnessRun says the person said yes and the harness named in Text has
// the turn. Hint is its description, and Model is what it is running on when
// the turn named one.
//
// It is a REPORT, not a question, and it is what a surface draws instead of
// a model thinking: what follows is the harness's report as ordinary text
// and then EventTurnDone, or EventError if the run failed.
EventHarnessRun
// EventHarnessStep is one step of a running sub-harness, the instant it
// lands: Step is the walk's own trail entry (subharness.RunWatched) and ID is
// the run it belongs to — the id EventHarnessRun carried.
//
// It is a REPORT and it is DISPLAY-ONLY. A run takes minutes, and between the
// announcement and the report there was nothing on screen saying which part of
// it was happening. Nothing here is recorded: the report that follows carries
// the whole trail (subharness.RunCard), so a step kept in the transcript would
// be the same news written down twice.
EventHarnessStep
// EventHarnessDesign says a turn asked for a sub-harness to be BUILT — "make
// a harness for triaging flaky tests" — and the design has started
// (harness_build.go). Text is the goal, less the words that asked for it;
// Hint is "designing"; Model is what the design is thinking with.
//
// TASK NAMES THE NODE IT RUNS AS, and it is the only field on this kind a
// surface can act on. A design is a task now (harness_task.go): it has an id
// a person can say out loud, a room they can walk into, and a stop. So the
// one line this event draws names it — "harness · designing X — task 4" —
// and everything else about the design's life arrives on the task lane, not
// this one. Only ID is filled.
//
// It is a REPORT and it does not hold the turn: the turn is already over when
// it arrives, because designing takes a minute and a conversation held on one
// is a conversation nobody can use. Exactly one of EventHarnessDesignDone or
// an EventNotice saying why not follows it, on the standing lane
// ([Agent.HarnessDesigns]) as well as on the turn's stream.
EventHarnessDesign
// EventHarnessProgress reports one live snapshot of a harness design call.
// It is display-only: partial JSON and reasoning never enter the transcript.
// Goal names the request; Phase is designing or reviewing; Attempt and
// Attempts size the retry ladder. ThoughtTail is the recent reasoning, Hint
// is the best meaning recovered from partial JSON, Bytes is content received,
// and Stalled says no delta has arrived for ten seconds.
EventHarnessProgress
// EventHarnessDesignDone carries a finished design in Harness, with the id
// the answer goes back through in ID, the name in Text and the description in
// Hint.
//
// It is a QUESTION — the only one on this list that outlives the turn that
// raised it. A surface draws the page (subharness.CardLines is the renderer
// every surface shares) and answers through [Agent.ResolveHarness], the same
// method an offer is answered with: TRUE SAVES IT into the registry, false
// drops it. Nothing is written before that answer, and a surface that ignores
// this kind saves nothing — which is the same posture EventHarnessOffer
// keeps, one lane over.
EventHarnessDesignDone
// EventHarnessDesignRevising WITHDRAWS a design card the person asked to have
// changed. ID is the design it is about and Text is the change, in the
// person's own words as the design's thread passed them on (harness_task.go's
// revise_design).
//
// IT EXISTS BECAUSE A QUESTION CAN BE OVERTAKEN BY A THIRD ANSWER. A design
// card is one decision behind two doors — the card in the conversation and the
// approval row in the design's own room — and there is a third thing a person
// can do with a page, which is to say what is wrong with it. When they do, the
// page that card is about stops existing, so the card has to come down: left
// standing it would be a save key over a draft that has been replaced, and the
// answer it took would save the wrong page.
//
// A surface takes the card back to the LIVE form it wore while the page was
// first being written, because that is what is happening again — the designer
// is at work, EventHarnessProgress starts arriving, and exactly one
// EventHarnessDesignDone follows it with the rewritten page. The design's own
// ROW needs nothing from this kind: the node moves back to the "designing"
// phase on the task lane, and the approval row is drawn off that phase.
EventHarnessDesignRevising
// EventOrchestrateNote carries one planner note from an adaptive run
// (internal/orchestrate): Text is the note, ID the run. A REPORT; the room
// draws it as the thin thinking-row between completions.
EventOrchestrateNote
// EventOrchestrateFuel is the gauge and its early warning: Text is the
// spend summary ("$1.60 of $2.00"), Hint holds the cap. A REPORT at the
// 80% mark and whenever a surface asks; it never blocks anything.
EventOrchestrateFuel
// EventOrchestratePause says the run hit its fuel cap: in-flight nodes
// finished, nothing new launched, the frontier is frozen mid-shape. Text
// is the spend summary. It is a QUESTION answered through
// [Agent.ResolveOrchestrate] — top up, finish with what we have, or stop —
// and until that answer the run sits in its Paused state, resumable.
EventOrchestratePause
// EventToolFinished says ONE call's own work is over, the instant it is
// over, and carries how long that call took in Took.
//
// It is a CLOCK EVENT and nothing else: the result is not in it, and the row
// is not closed by it. The result still arrives as EventToolEnd or
// EventToolFailed, after the whole batch has finished, in call order — the
// order the transcript is written in.
//
// It exists because those two moments are not the same moment. A batch's
// calls run together and finish in any order, so a `cd` that took five
// milliseconds sat under a spinner and a climbing clock until the slowest
// call beside it returned, and then claimed that whole span as its own
// duration. The row was reading the BATCH's clock. This is the call's own,
// measured where it ran (loop.go's executeTool), so a surface can stop the
// row's clock and state the figure the call actually cost.
//
// A surface that ignores this kind is exactly what it was.
EventToolFinished
// EventRetrying says THIS STEP IS BEING ASKED AGAIN, and that whatever the
// dead attempt streamed is void. Text carries the one line explaining why —
// "nothing came back from the model — asking again", "the reply lost its
// thread — that text was dropped, asking again".
//
// It fires when the stream guard cut a request (internal/provider's
// streamguard.go): the endpoint went quiet, or the reply stopped being
// language. It also fires when a transport failure is about to be retried.
// A rescue taking over a visible answer on another machine serving the same
// model fires it too (internal/provider's hedge.go).
// The turn loop discards that attempt's partial text, its early reads and
// its half-arrived calls before the next request, so A SURFACE MUST THROW
// AWAY WHAT IT DREW FOR THEM TOO — everything after the last thing the person
// typed belongs to a response that will never exist, and leaving it on screen
// would show half a dead answer above the live one.
//
// AND IT FIRES WHEN THE STEP MOVES TO ANOTHER MODEL, which is the same news
// about the same attempt and a different thing to draw: the rest of the reply
// arrives in a different voice, at a different price. [Event.Retry] is what
// tells the two apart — its Next names the model being moved to and is empty
// on an ordinary retry (retrynews.go) — and Text carries the whole sentence
// either way, so a surface that reads only Text is exactly as correct as it
// has always been.
//
// It is also the one place a surface learns that a wait is a RETRY rather
// than a first attempt, which is the difference between "waiting for" and
// "trying again". It never ends a turn: either the next attempt streams, or
// EventError arrives with the sentence about giving up.
EventRetrying
// TWO KINDS WERE RETIRED HERE WITH STANDING ORDERS (the proposal and the
// update, replaced by [EventAutomationProposal] and [EventAutomationUpdate]
// at the end of this block). Their numbers are kept, never sent, so every
// kind after them keeps the integer it has on the wire and in anything a
// surface wrote down — this block's own law, stated over [EventRowNews].
_
_
// EventSubharnessAsk is a running subharness putting one question to the
// person (subharness_env.go, the Env's ask() door). ID is the run's task
// node, Text is the question in the program's own words, and Args carries
// the answers it offers as a JSON array when it offers a set.
//
// It is a QUESTION and it arrives IN THE RUN'S ROOM, which is where the run
// lives: its journal, its progress and its ✕ are all there already, and a
// question about the work belongs beside the work. A person who is not in the
// room learns about it from the ROSTER, because the node moves to the
// "awaiting your look" phase for exactly as long as the question stands —
// the same phase a design waiting on its card wears, for the same reason.
//
// It is answered through [Agent.AnswerSubharness], which takes the run's id,
// what they said, and whether they are TAKING OVER. A surface that ignores
// this kind leaves the run waiting until the node is stopped or the session
// closes, which is why the question is only ever put where somebody is
// watching (Config.AskConsent) — an unattended run answers from what the gate
// declared or stops incomplete, and never guesses.
EventSubharnessAsk
// EventSubharnessStep is one host call a running subharness just made
// (internal/exec's JournalEntry): ID is the run's node and Step carries the
// entry whole — which door, what it was about, what it cost.
//
// It is a REPORT and it is DISPLAY-ONLY, on EventHarnessStep's terms: the
// journal is the permanent record, this is how a person watches it being
// written. Nothing here is recorded in any transcript.
EventSubharnessStep
// EventSubharnessProposal asks whether one saved program should take this
// piece of work (tools_subharness.go). ID is the token an answer goes back
// through, Text is the program's name, Hint is what it is for, and
// Subharness carries the INTAKE CARD — every input field, what this
// conversation already answers, and which required blanks are left.
//
// It is a QUESTION and it is the one on this list with NO CLOCK THAT
// APPROVES. A task proposal's countdown ends in a yes because it is a window
// to redirect ordinary work; this one may not, because a program that ran
// because nobody answered would be exactly the silent auto-execution the
// whole path is built to prevent. It is answered
// through [Agent.ResolveSubharness] — true runs it, with the form as the
// person left it — and a surface that ignores this kind runs nothing at all,
// which is the correct behaviour rather than a degradation.
EventSubharnessProposal
// EventSubharnessProposalOff takes the card named by ID back down. Nothing
// ran, and nothing about the person's own intentions is being reported: the
// tool call that raised the card has let the turn go, either because its
// window expired or because the turn it belonged to was interrupted
// (tools_subharness.go).
//
// IT EXISTS SO THAT A CARD CANNOT OUTLIVE ITS LISTENER. The window is a
// bound on the TOOL CALL and not a deadline on a person, so it fires while
// the card is still on somebody's screen — and a card left standing after it
// would be a `run it` that resolves nothing, silently, which is the one
// ending a question is never allowed to have. A surface that ignores this
// kind leaves that dead card up; a surface that draws it takes the card down
// and says so.
EventSubharnessProposalOff
// EventTaskReplyTags names the finished tasks whose notes the next words
// answer. TaskReplyTags carries them in note order.
EventTaskReplyTags
// EventSteerAccepted says one sentence the person typed INTO the running
// turn is on the queue and will reach the model at the next step boundary
// (steer.go). Steer carries its identity, its words and the instant it was
// sent; nothing is in the transcript yet.
//
// It is a PROMISE AND NOT AN OUTCOME, which is why exactly one of the two
// kinds below always follows it on the same stream: the boundary it is
// waiting for may never come.
EventSteerAccepted
// EventSteerConsumed says the model HAS BEEN GIVEN that sentence: it is in
// the transcript as user content of the turn it was typed into, and the
// request carrying it is the next thing that goes out. Steer names which
// steer landed.
EventSteerConsumed
// EventSteerFellThrough says the turn ENDED FIRST — it answered, it faulted,
// or somebody stopped it — with that sentence still waiting, so no request of
// that turn ever carried it. The words are not lost and they did not steer
// anything: they move to the queue that holds a message waiting for a turn of
// its own, and the stream the steer was sent on carries that turn when it
// starts (steer.go states the whole law).
EventSteerFellThrough
// EventTaskPhase says one running node has moved between its three lives —
// its worker, the check that reads what the worker left, a repair round
// closing the gaps the check named (task_audit.go). TaskPhase carries the
// node's id, the word, the round numbers while a repair runs, and the
// check's one-line finding.
//
// IT RIDES THE TASK LANE beside EventTaskUpdate — the turn's hub AND the
// standing [Agent.TaskUpdates] subscription — because a check that takes
// four minutes takes them long after the turn that proposed the work ended.
//
// IT IS NEWS AND NEVER A ROW. The node's state does not move: it was
// running before the check and it is running after it, so a surface folds
// this into the row an update already gave it and never opens one from it.
// A surface that ignores this kind is what it was — which is what the
// evidence in #76 §5 describes: minutes of check and repair drawn as
// nothing at all, and a person concluding the work hung.
EventTaskPhase
// EventTakeover says another window on this machine has asked for this
// conversation and the turn it was in has ended (takeover.go). It rides the
// standing task lane and nothing else, and Text carries [TakeoverWord]. The
// surface that hears it lets go — detaches and closes the conversation the
// way /new does — and the window that asked resumes it from the checkpoint.
EventTakeover
// EventJobUpdate reports one BACKGROUND JOB's life (jobnotice.go): Job
// carries the job's own id, the short name it has been given, the command,
// the state, the log path and — once it is over — the exit code. It fires
// when a job starts, when its name arrives, and when it settles.
//
// IT IS ITS OWN KIND BECAUSE A JOB IS ITS OWN THING. A job used to ride
// EventTaskUpdate as a [TaskNotice], which left every surface downstream
// carrying a clause saying a job is not really a task — no room, no branch,
// no price, no stop, no card and no index row. What a job has is an id, a
// log and an exit code, and none of those is what a task row is drawn from,
// so it is published as what it is and the clauses go away.
EventJobUpdate
// EventCaption carries one line NAMING THE OPEN STEP (caption.go): the
// cheap narrator asked shortly after the tools begin. Text is the step
// title — a checklist item, not reasoning. It is news about the open step,
// never a new block of its own — the surface keys it onto the caption
// already drawn for that batch.
//
// A SURFACE THAT IGNORES THIS KIND IS UNCHANGED: the deterministic
// composite already stands in the caption slot, and this event only
// replaces that floor when a cheap model had something better to say.
//
// [Event.Category] rides with it and is the same news about the same step:
// which FAMILY of work the sentence is about (actioncategory.go). It is
// empty whenever the narrator did not name one, and a surface reads that
// emptiness as "ask the tools", never as "draw nothing".
EventCaption
// EventAssistantDone marks the journal boundary for one valid, non-empty,
// tool-free assistant response. Its content has already arrived through
// EventTextDelta and has been recorded before this event is published. It
// carries no prose of its own: a surface uses it to stop treating those
// streamed words as provisional while the end-of-turn checks still run.
//
// IT IS APPENDED TO PRESERVE EVERY EXISTING WIRE NUMBER. Hosts serialize
// EventKind as an integer, so inserting a kind above this point would make
// an older binary read every later event as a different fact.
EventAssistantDone
// EventMoved says a WINDOW SOMEWHERE ELSE HAS OPENED THIS CONVERSATION and
// is now the one in it. Text carries [MovedWord].
//
// IT IS NOT [EventTakeover] AND THE DIFFERENCE IS WHAT HAPPENS TO THE WORK.
// A takeover is asked for on this machine's disk and answered by a window
// that OWNS the engine: it interrupts, closes, and the work lands paused for
// the window that asked to resume. A move is announced by an engine that
// holds the conversation itself (internal/enginehost) to every other surface
// attached to it: nothing is interrupted and nothing pauses, because the
// engine goes on running the turn while the surfaces around it change. The
// window hearing this DETACHES — it does not close.
//
// IT RIDES THE STANDING TASK LANE for EventTakeover's reason exactly: it is
// the one subscription that outlives every turn, and a move happens most
// often in the middle of one.
EventMoved
// EventQuestion carries one whole [Question] in Question: a decision this
// engine is handing to the person, with its evidence, its answers, the
// asker's own pick, what is waiting on it and what an answer costs
// (question.go).
//
// IT ARRIVES AFTER THE ROWS IT IS ABOUT, exactly as EventConsentRequest
// already orders itself against its batch's EventToolBegin rows, and for
// the same reason: a question attaches to a row a surface has already
// drawn, and one that arrived first would be a question about nothing.
//
// IT IS A SECOND DESCRIPTION AND NEVER A REPLACEMENT. Every lane goes on
// emitting the event it always emitted — EventConsentRequest,
// EventTaskProposal, EventAutomationProposal and the rest — so a surface that
// ignores this kind is exactly what it was. A surface that draws it draws
// one object for every lane instead of thirteen cards.
//
// IT RIDES THE TURN IT WAS RAISED IN, AND [Agent.WatchQuestions] BESIDE IT —
// never the standing TASK lane, which is the roster's and whose readers walk
// a strict sequence of rows.
EventQuestion
// EventQuestionWithdrawn says a question stopped being one: the subject
// settled, the clock took it, the plan changed, another answer made it
// moot. Question carries the same object with [Question.Withdrawn] filled
// in, so a surface has the head it drew and the sentence to retire it with.
//
// A QUESTION IS NEVER SIMPLY GONE. A count that drops for no reason a
// person can see is a count they stop believing, so the reason travels with
// the withdrawal and is drawn once, dim.
EventQuestionWithdrawn
// EventQuestionAnswered carries the whole [Answer] in Answer: what was
// picked, what was said beside it, who decided and how long it lasts.
//
// THIS ONE IS KEPT. It is written to the session's own decisions.jsonl as
// it is emitted ([Agent.Decisions] reads it back), because it is the
// DECISION RECORD — the first rung of the ladder, the thing an asker reads
// before it puts anything to anybody. Consent is deliberately not journaled
// (a question about work that has not happened yet); an ANSWER is the
// opposite of that: it is the one thing about a question that stays true
// afterwards.
EventQuestionAnswered
// EventRowNews carries one line in Text about A ROW THE PERSON WROTE that
// this build has stopped acting on — a pinned machine the router refuses to
// serve a model from, a base that will not carry a lane choice at all
// (internal/provider's lanepin.go and prefcarry.go).
//
// IT IS NOT [EventNotice] AND THE DIFFERENCE IS WHO THE SENTENCE IS FOR. A
// notice is the adapter saying what it did to a request to get it accepted,
// and it is over once the answer lands — a surface may fold it away with
// the rest of the machinery. This is the only account a person will get of
// why the machine they named has stopped appearing, and there is nothing to
// fold it into: it asks them to do something (pin again, or leave it on
// auto). Measured on 2026-09-13, riding the wrong kind: the pin was
// retired, `@deepseek` came off the model word, another machine answered,
// and the chat's work chip had swallowed the sentence that said so.
//
// IT IS LAST IN THIS BLOCK AND EVERY NEW KIND BELONGS HERE, because a kind
// is an integer on the remote wire (internal/remote's EventWire): one added
// in the middle renumbers every kind under it, and a window and an engine
// on two builds would then disagree about what each other's events were.
EventRowNews
// EventQuestionDiscussion carries a reply beside a pending decision.
EventQuestionDiscussion
// EventToolOutput carries literal stdout/stderr in Text while a user shell
// command is running. CallID owns the bytes; its result still ends the call.
EventToolOutput
// EventAutomationProposal asks the person whether to save one automation
// (automation_tool.go); Automation carries the card. Nothing is saved until
// it is answered.
EventAutomationProposal
// EventAutomationUpdate reports an automation changing because of a call
// in this turn — saved, paused, resumed, deleted, asked to run now. It is a
// report and never a question.
EventAutomationUpdate
// EventChatStart asks the attached terminal to open an independent chat.
EventChatStart
)
// TaskReplyTag is the task identity a surface places beside the answer its
// completion prompted. Request is the person's original text, not the brief.
type TaskReplyTag struct {
ID uint64 `json:"id"`
Title string `json:"title"`
Request string `json:"request,omitempty"`
// Obligation is WHAT THIS RESULT WAS ACTUALLY OWED when it was delivered,
// and it is set only when the person moved the goal while the work ran: the
// admitted ask as history, their applied directions in order, and the
// deliverable and done-condition as they then stood (wakecause.go's
// [obligationText], from the assignment's own snapshot). Revision is the
// assignment version it was taken at, and 0 on an unrevised task.
//
// A REVISED TASK IS JUDGED BY THIS AND CITED BY Request. Request stays the
// person's original words for the row a surface draws beside the answer;
// judging a CSV result against the JSON that was first asked for is the
// failure these two fields exist to prevent ([Agent.turnAsk] reads them).
// Both are optional: an old tag, or one from an unrevised task, carries
// neither and is read exactly as it always was.
Obligation string `json:"obligation,omitempty"`
Revision uint64 `json:"revision,omitempty"`
// State and Stopped belong to the result's own attempt. A delayed
// delivery must not borrow a re-armed node's new cancellation state. Empty
// State identifies older tags without positive stop provenance.
State TaskState `json:"state,omitempty"`
Stopped bool `json:"stopped,omitempty"`
}
// Event is one observable thing in a turn. A Submit returns a channel of
// them, closed after EventTurnDone or EventError.
//
// A STEER'S CHANNEL IS THE ONE EXCEPTION, and it is exact: [Agent.Steer] hands
// back a stream that outlives the turn it was sent into when the steer falls
// through, so EventSteerFellThrough arrives AFTER that turn's EventTurnDone or
// EventError, and the turn the words then start speaks on the same channel
// (steer.go says why). A caller that reads to close — which is every caller
// today — sees all of it in order and needs no second rule; a caller that stops
// at the terminal event stops at the terminal event of the FIRST turn.
type Event struct {
ChatStart *ChatStart `json:",omitempty"`
ReplayCursor ReplayCursor `json:",omitempty"`
ReplayObserved bool `json:"-"`
Discussion *QuestionDiscussion `json:",omitempty"`
Kind EventKind
// Addressed is a producer's declaration that streamed text is for the person.
// It does not imply a completed response and survives interruption.
Addressed bool `json:"Addressed,omitempty"`
// Told marks an [EventNotice] as addressed to the person: a surface draws it
// as a line to read, and never folds it into the turn's work group with the
// dim facts. It is omitted when false, so an older peer sees what it always
// saw.
Told bool `json:"Told,omitempty"`
Text string
ShortTitle string `json:"ShortTitle,omitempty"`
Tool string
Hint string
Err error
// Discard says an EventError ended a cut attempt whose streamed text was
// never a reply. The surface withdraws that attempt before drawing the
// error, as it does for EventRetrying, without counting another retry.
Discard bool `json:"Discard,omitempty"`
// Unrecorded says an EventNotice is in no record a returning surface
// redraws: memory's notices ([Agent.sayMemory]) are said to the person and
// never written into the transcript. A surface holding this conversation
// out of sight keeps such a line and draws it when the person brings the
// conversation back (tui3's keeper.go). Every other notice is either in the
// transcript already or said for the moment it is said in.
Unrecorded bool `json:"Unrecorded,omitempty"`
Usage Usage
TaskReplyTags []TaskReplyTag
// Skills is the ordered list of skill names this turn carried, on the
// notice that announces them (skillturn.go). IT IS THE FIELD AND NOT THE
// SENTENCE a surface reads: [Event.Text] says the same thing in words for
// a reader who draws notices as prose, and a surface that took the names
// back out of that sentence would break the first time somebody improved
// the wording or a skill name held a comma, and would break silently,
// because a test written against the same sentence agrees with it.
//
// AN ABSENT LIST MEANS UNKNOWN AND NOT NONE. The tag is omitempty because
// an event with no skills has to serialise as it did before this field
// existed, which is what keeps a new session and an older peer talking
// (internal/remote's wire tests). The cost is that a turn that carried
// nothing and a peer too old to send the field put the same bytes on the
// wire, so a surface may draw a non-empty list and must say nothing at all
// otherwise — a sentence like "no skills used" is a claim this field
// cannot support.
Skills []string `json:"Skills,omitempty"`
// Summarized is how many messages a compaction pass replaced with a
// summary, on [EventCompacted]; zero for a pass that only stubbed and
// folded. It is the field a surface decides by, never the hint's words:
// a pass that rewrote the person's own messages is the one whose line
// stays standing (internal/tui3's workfold.go), and a free pass folds
// with the rest of the turn's machinery. Omitted when zero, so an event
// with no summary serialises exactly as it did before this field, and a
// surface talking to an older engine folds every pass.
Summarized int `json:"Summarized,omitempty"`
// Category is the FAMILY OF WORK an EventCaption's sentence is about — one
// word from the closed list in actioncategory.go — and it is zero on every
// other kind.
//
// EMPTY IS THE NORMAL MISSING CASE AND NOT AN ERROR. The narrator is a cheap
// model asked for a prefix it may ignore, and a surface that receives none
// derives the family from the batch's own tool names
// ([ActionCategoryForTools]), which is deterministic and cannot be wrong
// about which hands were used. So this field REFINES a mark that is already
// correct; it never supplies one that would otherwise be missing.
//
// It rides the wire behind a json tag of its own so a peer built before it
// existed simply does not see it (internal/remote's [EventWire] embeds this
// struct whole), and a caption saved by an older build replays with an empty
// one and derives the same mark it always drew.
Category ActionCategory `json:"Category,omitempty"`
// Unchanged says an [EventCompacted] pass left the transcript exactly as it
// found it: nothing was old enough to stub and nothing was foldable, so the
// region above the conversation did not move and neither did the floor
// beneath it. It is false on every other kind and on every pass that really
// edited something.
//
// THE ZERO VALUE IS "A PASS HAPPENED", and that polarity is the whole reason
// this is a field rather than a reading of Hint. EventCompacted is sent on
// BOTH paths by promise, because a surface opens a row on EventCompacting
// and has to be able to settle it whatever the pass found. So one value
// carried two meanings and the failing one was silent: a surface handed its
// scrollback over to a replacement that had not happened, and declared the
// conversation finished with a good part of it undrawn and unreachable.
//
// It rides the wire behind a json tag of its own, so a peer built before it
// existed does not send it, reads false, and behaves exactly as it always
// did (internal/remote embeds this struct whole).
//
// NO PASS IN THIS BUILD SETS IT. A pass that finds nothing now sends neither
// EventCompacting nor EventCompacted, so there is no row to settle. The field
// stays, and a surface still honours it, because a remote engine built
// between the two changes announces every pass and settles a refused one
// with it.
Unchanged bool `json:"Unchanged,omitempty"`
// Args is the tool call's arguments rendered for display: the JSON the
// model sent, compacted to one line and capped. It is set on
// EventToolBegin, EventToolEnd and EventToolFailed. Arguments that do not
// parse as JSON pass through as the raw text — a malformed call is still a
// call the person should be able to look at.
//
// THE CONTRACT IS THAT THIS STAYS PARSEABLE WHENEVER THE WIRE ARGUMENTS
// WERE, at every size. The cap ([argsLimit]) is spent INSIDE the oversized
// string values, each of which then ends in the marker [capBytes] writes —
// `… (12345 more bytes)` — rather than by cutting the text, which would end
// a 20k write's payload in the middle of a string literal and leave every
// reader downstream calling a well-formed call malformed.
//
// So a field a surface reads back out of this may be SHORTER than the one
// the model sent, and says so in its own last bytes. Anything derived from
// one is a floor rather than a figure: a capped write's line count is "at
// least this many", and internal/tui3 spells that with a trailing `+`.
Args string
// BeltStepHandled is present only for a run worker that opted into the
// step boundary handshake. Its owner closes it after recording this end
// event and applying the run's limits and notes. Cancellation releases a
// belt whose reader failed, and this local handshake never goes on wire.
BeltStepHandled chan<- struct{} `json:"-"`
// Output is the tool's result text on EventToolEnd and EventToolFailed,
// verbatim up to a cap and then marked "… (N more bytes)".
//
// CONTRACT: Output is FOR DISPLAY EXPANSION ONLY. It is not the result.
// The wire result — what the model reads, what the transcript records — is
// unchanged and complete; this field is a capped copy for a surface that
// wants to show more than Hint. A surface must never treat it as the tool's
// output for any purpose other than showing it to a person.
Output string
// HarnessMade says this step's failure was written by the HARNESS and not by
// the world the model reached for: a hand that was withdrawn (withdrawn.go),
// a door that refused the call (consent.go and the rest of the pre-action
// chain). It is set on EventToolFailed and on nothing else.
//
// IT EXISTS FOR THE COUNTERS. A stuck detector's whole claim is that a step
// which taught nothing was a step the model had no business taking, and that
// claim is false when the harness wrote the answer itself — measured in
// SWE-Marathon s4, where the harness withdrew `bash`, answered eight retries
// with "Unknown tool", and then injected three [stuck] notes blaming the
// model for the retries (withdrawn.go states the whole failure). A surface
// may show it or ignore it; the runner reads it to keep the harness's own
// steps out of the model's ledger ([runTaskChild]).
HarnessMade bool
// Refused says a DOOR said no to an action the model attempted: the call was
// well formed, it named a tool on the belt, and a pre-action citizen (the
// approval gate, a write or ground guard) refused it before it ran. It is set
// on EventToolFailed, only together with HarnessMade, and only at the one
// place every veto passes through ([episode.preAction] names who refused).
//
// IT SEPARATES TWO ANSWERS THE HARNESS WRITES. A correction about the FORM of
// a reply (one call per response, a malformed call, a withdrawn tool, a held
// process rule) is addressed to the worker and nothing was attempted on the
// world: HarnessMade alone. A refused door is something the worker TRIED, and
// a person steering a run wants to see that it was tried and refused. The
// fact is kept here, where the attempted action is known, so no reader has to
// tell the two apart by the words of the answer.
Refused bool
// ID names one EventConsentRequest, and is the token a surface hands back
// to [Agent.ResolveConsent]. It is zero on every other kind but
// EventHarnessOffer, whose own id goes back through
// [Agent.ResolveHarness] — two lanes, two counters, and one field, because
// "which question" is the same question for both of them.
ID uint64
// CallID is the PROVIDER's id for the tool call a tool lifecycle event
// (forming, announced, begin, finished, end or failed), an
// EventConsentRequest or an EventCaption is about — the same string the tool
// result carries — and is empty on every other kind. It is empty on a forming
// event too until the wire has sent one, which is the first fragment in
// practice and nothing the consumer may assume.
//
// ON A CAPTION IT IS THE BATCH'S ANCHOR: the id of the call the batch opened
// with, which is how a surface knows WHICH STEP the sentence is about. The
// narrator answers on a goroutine that can be descheduled between checking
// that its batch is still open and reaching the hub, so a caption can arrive
// after its batch ended and the next one began. A surface keying on "the
// newest tool row" then retitles the running step with a sentence about the
// finished one; keyed by this id it drops news about work it is no longer
// holding. An event with no anchor is an engine built before this, and a
// surface may serve it by recency exactly as it always did.
//
// ON A CONSENT REQUEST IT IS WHICH CALL IS BEING ASKED ABOUT. A surface pairs
// the question to the row it draws the question under, and the card reads the
// command it is about to remember off that row — so a question paired by tool
// name alone can, with two bash calls in flight, show one command and bank a
// standing rule for the other (internal/session's consent.go).
//
// It is on BOTH ends of that pair on purpose: forming and announced are two
// states of one call, and the id is what lets a surface say so. Without it
// the announcement can only be paired by tool name, and a batch of parallel
// calls of the same tool has no name to tell its rows apart by.
//
// It is not [Event.ID] because that field is the consent lane's own token, a
// uint64 this session mints; these are two different names for two different
// things and folding them would make "which call" and "which question"
// the same field with two answers.
CallID string
// ArgsText is the RAW, PARTIAL arguments text of a forming call: exactly what
// the provider has streamed so far, uncompacted and unparsed. It is set on
// EventToolForming and empty everywhere else — Args is the display JSON of a
// WHOLE call, and half of a JSON object is not that.
//
// It is CUMULATIVE: every fragment carries the whole text that has arrived so
// far, not the piece that just landed, so a surface keeping it replaces what
// it held rather than appending to it. It is capped at [formingArgsLimit]
// from the FRONT, and Bytes beside it is the honest size of the whole.
//
// A surface may show it, cut it, or ignore it. NOTHING MAY UNMARSHAL IT — and
// nothing needs to: [PartialString] is the tolerant read of one field's
// streamed text, and it is one scanner in one place rather than a second
// parser per surface.
ArgsText string
// Took is how long ONE tool call's own work took, on EventToolFinished and
// zero on every other kind. It is measured around the tool's execution and
// around nothing else: not the wait for a consent question, and not the wait
// for the rest of the batch.
Took time.Duration
// Bytes is how much of a forming call's arguments has arrived. It is the
// length of ArgsText, carried as its own field so a surface can show progress
// ("write · 4.2 KB") without measuring text it may have chosen not to keep.
Bytes int
// Harness progress fields ride on EventHarnessProgress alone. They are flat
// because the event is already the transport envelope and every field is a
// short fact a surface may independently omit.
Goal string
Phase string
Attempt int
Attempts int
ThoughtTail string
Stalled bool
// Step is one finished step of a RUNNING sub-harness, on EventHarnessStep
// alone and nil on every other kind. It is the walk's own trail entry rather
// than a copy of the parts of it a surface might want, so the row drawn while
// the run happens and the row on the card read back afterwards are rendered
// from one fact (subharness.StepLine).
Step *subharness.Trail
// Entry is one host call a running SUBHARNESS just made, on
// EventSubharnessStep alone and nil on every other kind
// (internal/exec's JournalEntry). It is the journal's own entry rather than a
// copy of the parts of it a surface might want, for the reason Step above is
// the trail's: the row drawn while the run happens and the row read back out
// of the journal afterwards are one fact rendered twice.
Entry *exec.JournalEntry
// Task carries one EventTaskProposal or EventTaskUpdate's payload
// (task_contract.go). It is nil on every other kind, and the ID inside it
// is the token a surface hands back to [Agent.ResolveTask].
Task *TaskNotice
// Job carries one EventJobUpdate's payload (jobnotice.go). It is nil on every
// other kind, and the Id inside it is the job's OWN number — the one
// `jobs output 3` and `jobs kill 3` already take — rather than a second id
// minted somewhere else to keep it from colliding with a task's.
Job *JobNotice
// TaskPhase carries one EventTaskPhase's payload (task_contract.go): which
// running node moved into which of its three lives. It is nil on every
// other kind, and it is its OWN payload rather than more fields on
// TaskNotice because a phase is not a row — it names no state, no elapsed
// and no cost, and a surface that mistook one for an update would redraw a
// card from a value that never carried those.
TaskPhase *TaskPhaseNotice
// Automation carries one EventAutomationProposal or EventAutomationUpdate's
// payload (automation_tool.go). It is nil on every other kind.
Automation *AutomationNotice `json:",omitempty"`
// Subharness carries one EventSubharnessProposal's intake card
// (subharness_contract.go). It is nil on every other kind, and the ID beside
// it is the token a surface hands back to [Agent.ResolveSubharness].
Subharness *SubharnessCard
// Retry carries one [EventRetrying]'s payload in parts (retrynews.go): which
// model was being asked, how far into its patience the step is, why the
// attempt is void, and — when the step is moving — which model the rest of
// the reply will come from. It is nil on every other kind.
//
// It rides behind a json tag of its own so a peer built before it existed
// simply does not see it (internal/remote's [EventWire] embeds this struct
// whole), and an older engine's retry arrives with none — which is the same
// thing this build's surface must already handle, because [Event.Text] is
// still the whole line and always has been.
Retry *RetryNews `json:"Retry,omitempty"`
// Steer carries one sentence spliced into a running turn, on
// EventSteerAccepted, EventSteerConsumed and EventSteerFellThrough alone; it
// is nil on every other kind (steer.go). The same [SteerNote] value rides
// all three, so a surface pairs the outcome with the row it drew on the
// acceptance by [SteerNote.ID] and never by matching the words.
Steer *SteerNote
// Rule is the approval policy's own phrasing of why a call is being asked
// about — `bash pattern "rm -rf *"`, `tool "edit"`, `default`. It is set on
// EventConsentRequest and empty elsewhere. The wording is the policy's
// (internal/approval) so that every surface says the same sentence about the