Repository navigation
Expand file tree
/
Copy pathchat.go
More file actions
6547 lines (6319 loc) · 315 KB
/
Copy pathchat.go
File metadata and controls
6547 lines (6319 loc) · 315 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 main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/codeaf/internal/catalog"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/consent"
"github.com/Agent-Field/codeaf/internal/craft"
"github.com/Agent-Field/codeaf/internal/exec"
"github.com/Agent-Field/codeaf/internal/guard"
"github.com/Agent-Field/codeaf/internal/head"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/lane"
"github.com/Agent-Field/codeaf/internal/lease"
"github.com/Agent-Field/codeaf/internal/plan"
"github.com/Agent-Field/codeaf/internal/profile"
"github.com/Agent-Field/codeaf/internal/provider"
"github.com/Agent-Field/codeaf/internal/provider/pool"
"github.com/Agent-Field/codeaf/internal/resident"
"github.com/Agent-Field/codeaf/internal/revision"
"github.com/Agent-Field/codeaf/internal/router"
"github.com/Agent-Field/codeaf/internal/rtk"
"github.com/Agent-Field/codeaf/internal/session"
"github.com/Agent-Field/codeaf/internal/store"
"github.com/Agent-Field/codeaf/internal/thread"
)
// summaryFileList is the header a leaf writes over the absolute paths it
// produced, before handing its summary on. The spelling is a constant because
// two readers depend on it byte for byte: the person who opens the files, and
// `codeaf do`, which strips the block back off the deliverable so its own
// `files:` footer is the only list on stdout.
const summaryFileList = "\n\nFiles:\n"
// brainOptions is what one driver says about the errand it is building a brain
// for. There is one brain and one construction of it; these are the few things
// the construction cannot work out for itself.
//
// THEY DESCRIBE THE WORK, NEVER A SURFACE. A brain has no conversational half
// to switch off any more: `codeaf chat` is internal/tui3 over internal/session
// and reaches none of this, and the entry point that once built a talking brain
// here went with the surface it served (#329). The compiler, the reconciler,
// the runner, the contracts, the gate, the extensions and the replans are what
// is left, which is the whole of what an errand needs.
type brainOptions struct {
// ephemeral says the store evaporates when this process exits, so the
// loops whose whole product is a durable record — craft, self-practice —
// would be paying for something nobody can ever read.
ephemeral bool
// workspaceRoot overrides where jobs work. Empty is the store's own
// workspace directory, which is what a caller that names none gets.
workspaceRoot string
// sharedWorkspace makes every node work directly in workspaceRoot instead
// of in a per-job subdirectory beneath it, and moves the harness's own
// scratch out of it entirely.
//
// It is what an errand selects, and the reason the other layout exists is
// that a driver hosting many unrelated jobs must give each one its own
// directory so they cannot trample each other. An errand IS one job, aimed
// at a directory a person named, and the only
// correct reading of "fix the bug in intervals.py -w ~/project" is that the
// agent works in ~/project — opening the file that is there, editing it in
// place, the way every other CLI agent does. Under the per-job layout it
// started in an empty ~/project/task-2 instead, could not see the file it
// was sent to fix, and invented one.
sharedWorkspace bool
// model and planModel are the per-run slot overrides. They outrank both the
// environment and the persisted picker, because a flag is the most recent
// thing the person said.
model, planModel string
// seats is a headless door's answer from the one resolution ladder — flag,
// environment, crew, default (config.ResolveSeats) — handed down rather
// than resolved again here, so the models the door printed on its receipt
// are the models this brain builds clients for.
//
// Nil falls back to the order this construction has always kept on its own:
// the flag, then the picker the person last used, then what config.Load
// resolved. Every door that has a ladder to read hands the answer down.
seats *config.Seats
// wall is how long this brain's work has to finish in. Zero is no wall at
// all; an errand's is its own timeout, and it is handed on so the work can
// see the clock it is being judged against.
wall time.Duration
// produced is the job's record of what its work left behind: told, as each
// leaf lands, the absolute paths that leaf's workspace recorded it writing.
// It is the registry's own list rather than anything read back out of prose,
// and it exists because the workspace dies with the worker goroutine:
// nothing downstream of the run can ask it what was written unless somebody
// catches the answer on the way past.
//
// Only an errand wires it — `codeaf do` is one process around one job, so
// the paths a leaf recorded here are the paths its footer prints and its
// --json carries. Nil is every other driver, which reads files off the graph
// like any other reader.
//
// IT READS AS WELL AS WRITES, AND THAT IS THE WHOLE OF THE CHANGE. It was a
// write-only sink for a while — a func the wiring could tell things and
// could never ask anything — so the delivery gate was handed a second,
// narrower record instead: the artifacts of the ONE LEAF in front of it. A
// repair round writes nothing, because the work landed under its parent, so
// the gate judging one was told the run had left nothing behind and refused
// a delivery whose files were on disk and in this registry the whole time
// (2026-08-29, bench/deepswe textual s5 and igel s5;
// the implementation §5).
produced artifactRecord
// consent answers the price question for a desk with nobody at it. Nil is
// the ordinary desk: it asks, and the job waits.
consent func(store.Node, planEstimate) bool
// newClient builds provider clients. Nil is the real one; a test scripts a
// provider through it and drives the same brain every other caller drives.
newClient func(config.Config, string) (*liveClient, error)
// callWall is how long one completion on the two structuring slots may run.
// Zero is pool.DefaultCallWall, which is every caller but a test: a test
// names a wall it can reach in a second rather than sitting out four
// minutes of a model that never stops thinking (issue #927).
callWall time.Duration
}
// remainingWall reads what the errand's own context still leaves.
//
// WHAT THE ERRAND'S WALL LEAVES IS THE ONLY FIGURE THAT CARRIES THE PERSON'S
// OWN NUMBER. The runner passes the same context settings.ExecContext preserves
// and the leaf actually runs under; a surface with no wall has no deadline here
// and keeps the token-sized room it has always granted.
func remainingWall(ctx context.Context) time.Duration {
if at, ok := ctx.Deadline(); ok {
return time.Until(at)
}
return 0
}
// buildBrain assembles the brain: every provider client, the reconciler, the
// runner and the consent desk. It is a function rather than the body of the
// errand because a test scripts a provider through brainOptions.newClient and
// drives the same construction every errand drives. Nothing here starts a
// goroutine or holds the terminal; start does that, once, on a brain that has
// already been built.
func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, error) {
brain := &chatBrain{window: w, session: session, wall: opts.wall}
path, graph := w.path, w.graph
newClient := opts.newClient
if newClient == nil {
newClient = newLiveClient
}
settings, err := config.Load()
if err != nil {
// The sentence about a missing key is said at the one exit every
// command leaves through (main.go), not here: this used to print it and
// then let the machine form print underneath, which said one fact twice.
return nil, err
}
// The crew router picks its seats from this catalog (config.CrewCatalog),
// so it is wired BEFORE the seats handed in are applied — a door whose
// ladder routed before this line would have routed from nothing. The read
// is the same non-blocking one, never a fetch.
modelCatalog := catalog.LoadLazy(context.Background(), catalog.Options{
BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir,
})
seatCrewRows(modelCatalog.ModelsNow)
wirePoolIndex(settings.ProfileDir)
if opts.seats != nil {
applySeats(&settings, *opts.seats)
} else {
applyModelFlags(&settings, opts.model, opts.planModel)
}
prefs := loadChatPrefs(filepath.Dir(path))
if strings.TrimSpace(prefs.VoiceModel) == "" {
prefs.VoiceModel = settings.VoiceModel
}
// Discovery starts here and is waited for nowhere on this path. On a cold
// cache it is a network round-trip, and every question it answers belongs
// to a tool the user has not been able to reach yet — the surface has not
// been drawn. Asking it in front of the first frame buys nothing and can
// cost fifteen seconds of dead terminal.
// Every client built below shapes its requests against these rows: which
// knobs a model accepts is the catalog's answer, not a guess.
settings.Models = modelCatalog
mediaClient, err := settings.MediaClient()
if err != nil {
return nil, err
}
visionClient, err := settings.VisionClient()
if err != nil {
return nil, err
}
documentClient, err := settings.DocumentClient()
if err != nil {
return nil, err
}
// A flag names the model for this run and outranks the picker, which is a
// standing preference; the picker outranks the environment, which is a
// default. With no flag this is the picker exactly as before.
talkModel := firstNonEmptyString(opts.model, prefs.ChatModel, settings.Model)
workModel := firstNonEmptyString(opts.model, prefs.TaskModel, settings.Model)
baseMedia := exec.MediaTools{
Provider: mediaClient, Catalog: modelCatalog, VisionClient: visionClient,
DocumentClient: documentClient, DocumentEngine: settings.DocumentEngine,
ImageModel: prefs.ImageModel, SpeechModel: prefs.SpeechModel,
MusicModel: prefs.MusicModel, VideoModel: prefs.VideoModel,
// "best" and a named model are resolved at call time, so the strongest
// advertised model is whatever the catalog says now, not at startup.
ResolveModel: func(modality, word string) (string, error) {
return config.ResolveMediaModel(modelCatalog, modality, word)
},
}
// The slot defaults the catalog owns are filled the first time a leaf asks
// for them, which is the first time they can possibly matter. Everything
// here is the same resolution in the same order as before; only the moment
// moves, from in front of the first frame to behind it.
mediaModels := NewMediaModels(baseMedia, func(tools *exec.MediaTools) {
tools.ImageModel = firstNonEmptyString(tools.ImageModel, settings.ResolveImageModel(modelCatalog))
tools.SpeechModel = firstNonEmptyString(tools.SpeechModel, settings.ResolveSpeechModel(modelCatalog))
tools.MusicModel = firstNonEmptyString(tools.MusicModel, settings.ResolveMusicModel(modelCatalog))
tools.VideoModel = firstNonEmptyString(tools.VideoModel, settings.ResolveVideoModel(modelCatalog))
tools.VisionModel = settings.ResolveVisionModel(modelCatalog, talkModel, workModel)
if video, ok := modelCatalog.Model(tools.VideoModel); ok {
tools.VideoPrice = video.RequestPrice
}
})
chatClient, err := newClient(settings, talkModel)
if err != nil {
return nil, err
}
brain.closing(chatClient.Close)
taskClient, err := newClient(settings, workModel)
if err != nil {
return nil, brain.abandon(err)
}
brain.closing(taskClient.Close)
// The plan slot structures work — the task graph, replans, contracts, the
// delivery gate. Empty follows the work model live, so by default this is
// the same model behind a second hot-swappable handle; a picked plan model
// or CODEAF_PLAN_MODEL splits structuring from execution, and a /model
// change lands on the very next planning call.
planModel := firstNonEmptyString(opts.planModel, prefs.PlanModel, settings.PlanModel, workModel)
// How much the structuring model can hold, read once for the session. Every
// budget on the planning side — what a reviser is shown of what happened,
// what a claim-time division is shown of what landed, what the completion
// gate is shown of the table it judges — is a share of this. Zero is the
// catalog saying it cannot place the model, and each of those falls back to
// the literal it carried before this number existed.
planContextTokens := modelCatalog.ContextLength(planModel)
planClient, err := newClient(settings, planModel)
if err != nil {
return nil, brain.abandon(err)
}
brain.closing(planClient.Close)
// Who named the plan model, for the seed's own record: the flag if one was
// typed, the rung the ladder climbed to when a headless door resolved the
// seats, and otherwise the variable that has always seeded it.
planOrigin := config.PlanModelEnv
if strings.TrimSpace(opts.planModel) != "" {
planOrigin = "--plan-model"
}
if opts.seats != nil {
planOrigin = opts.seats.Plan.Rung()
}
installRoleLadder(graph, talkModel, planModel, workModel, settings.PlanModel, planOrigin)
boostClients := newMessageClientPool(settings)
brain.closing(boostClients.Close)
// The ruler stays keyed to the work model even when a different model
// plans: the anchors measure how the executor spends turns, and the plan
// model only reads them to size work for that executor.
measured := installMeasuredRulers(settings, taskClient.Model())
// Every structuring call this surface makes now bills the same rail its
// leaves bill. A journal write that fails is not a reason to fail the call
// it is describing, so this is best-effort by construction — but it fails
// loudly enough into the log to be findable.
journalSpend := func(usage store.NodeUsage) {
if err := graph.RecordUsage(usage); err != nil {
log.Printf("note: could not journal structuring spend for %s: %v", usage.NodeID, err)
}
}
chatClient.WithUsageJournal(journalSpend)
taskClient.WithUsageJournal(journalSpend)
planClient.WithUsageJournal(journalSpend)
boostClients.WithUsageJournal(journalSpend)
// The two structuring slots get a wall on a single completion; the work slot
// deliberately does not. Everything the talk and plan slots do is one
// round-trip that either answers or has stopped answering — compiling an ask,
// titling it, writing briefs, setting contracts, judging a deliverable — and
// none of it has an honest duration measured in minutes. A leaf is the other
// kind of thing: an agent loop with the executor's own deadline over it, where
// a long silence is often just a long tool call.
callWall := opts.callWall
if callWall <= 0 {
callWall = pool.DefaultCallWall
}
chatClient.WithCallWall(callWall)
planClient.WithCallWall(callWall)
// The positive stopping condition, installed once for every path that can
// grow a running job. It is asked last, after rounds, nodes and the daily
// rail have all passed, so on the common path it is never asked at all; the
// paths that ask it reach it through a package seam because they are
// reached through signatures that carry a plan function and a budget and
// have no client to give it. CODEAF_GROWTH_GATE=0 turns it off.
resident.SetGrowthSatisfier(resident.SatisfierFor(planClient, planContextTokens))
workspaceRoot := strings.TrimSpace(opts.workspaceRoot)
if workspaceRoot == "" {
workspaceRoot = home.StoreDir(path, "workspace")
}
if err := os.MkdirAll(workspaceRoot, 0o700); err != nil {
return nil, brain.abandon(fmt.Errorf("create chat workspace: %w", err))
}
// The machinery goes to the store's own directory on EVERY layout, not just
// the shared one. It used to move only when the workspace belonged to a
// person, on the reading that a per-job directory is the harness's to litter;
// what actually sat in it was the worker's own flight recorder, its raw event
// stream and its spilled observations, in the one directory the worker was
// told to work in. A measured leaf spent five of eleven turns listing that and
// reading its own trace back. See [exec.Workspace] on the scratch field.
scratchRoot := home.StoreDir(path, "scratch")
if err := os.MkdirAll(scratchRoot, 0o700); err != nil {
return nil, brain.abandon(fmt.Errorf("create chat scratch: %w", err))
}
// What the planner is allowed to see of the world before it plans. Only a
// shared workspace holds the person's own material; in the per-job layout the
// directory a job will work in does not exist yet, and the root above it holds
// nothing but the other jobs' folders — which says nothing about this goal and
// would spend prompt budget saying it. Empty renders nothing, so every
// planning prompt in an ordinary chat session stays byte for byte what it is.
terrainRoot := ""
if opts.sharedWorkspace {
terrainRoot = workspaceRoot
}
// Where this surface's jobs work, so a leaf whose stored row names a worker
// this build does not have can say so once in its own flight recorder rather
// than degrading in silence.
seatLeafWorkerNotes(workspaceRoot, scratchRoot, graph)
// The lease proves that no live resident can still own a claim in this DB.
// A closed terminal mid-run can otherwise leave leaves stranded as
// "running" forever. Release them back to pending before the
// reconciler starts, and say so once: recovered work resumes rather than
// haunting the rail.
//
// What it does NOT do is continue a transcript. The executor is rebuilt from
// nothing and the worker starts at turn zero. What it does now do is hand
// that fresh worker its predecessor's bank: the lines the interrupted attempt
// posted to its own record, and the files it left in the job directory,
// arriving as one more input under the headers a re-decomposed leaf already
// gets (see resident.Bank, and the node.Attempt read in the runner). So the
// sentence changed with the behaviour. It used to say "each starts again from
// the beginning", which was true and was the bug.
if released, err := graph.ReleaseOrphans(); err == nil && len(released) > 0 {
_, _ = thread.Post(graph, store.Message{
SessionID: session,
Role: store.RoleSystem,
Body: pickedUpMessage(len(released)),
})
}
if err := resident.ReAdoptServices(graph, session, nil); err != nil {
return nil, brain.abandon(fmt.Errorf("re-adopt services: %w", err))
}
// plans retains each planned job's graph so execution can be the headless
// mechanism exactly: call shapes for the router, escalation verdicts, and
// the profile records that calibrate the planner's ruler all read from the
// same plan nodes the scheduler would have read.
// They also outlive this process: a job that was planned in the session you
// closed last night is still running this morning, and its remaining plan
// is still editable.
plans := newJobPlans(graph)
brain.plans, brain.ledger = plans, graph
// The craft repository lives beside the graph it serves. Without git or
// with a broken repo, craft is dormant — never a startup failure.
// An ephemeral store has no craft: the repository would be born in a
// directory that is deleted a minute later, so recognising a learned
// workflow could never find one and forging a new one would throw it away.
var craftRunner *resident.CraftRunner
var craftShelf resident.CraftShelf
craftDir := ""
if !opts.ephemeral {
if craftRepo, craftErr := craft.Open(filepath.Join(filepath.Dir(path), "craft")); craftErr == nil {
craftRunner = resident.NewCraftRunner(graph, craftRepo, craftRepo.Dir())
craftShelf, craftDir = craftRepo, craftRepo.Dir()
} else {
log.Printf("note: craft repository unavailable: %v", craftErr)
}
}
reconciler := newResidentReconciler(settings, graph, chatClient, taskClient, planClient, plans, terrainRoot,
func(words head.ModelWords) head.WorkModelChoice {
return resolveWorkModelWords(words, modelCatalog, taskClient.Model)
}, true)
if craftRunner != nil {
reconciler = reconciler.WithCraftRunner(craftRunner)
}
// A restart that names a model resolves it through the same catalog the
// compiler's model words use; without this seam every "rerun that on the
// better model" honestly reports falling back to the default.
reconciler = reconciler.WithModelResolver(func(names []string, boost bool) (string, bool) {
choice := resolveWorkModelWords(head.ModelWords{Names: names, Boost: boost},
modelCatalog, taskClient.Model)
return choice.Model, choice.Model != ""
})
// The lease's flock proves the process exists; the heartbeat proves it is
// doing the work. Without this stamp a wedged process holds the resident
// role while every other one defers to it, and nothing it owes is done.
reconciler = reconciler.WithHeartbeat(func(at time.Time) {
_ = lease.NoteResidentTick(path, at)
})
// The moment this process took the resident role, which every rule about
// what happened before it is measured against.
reconciler = reconciler.WithResidentSince(time.Now())
// Recognition and forging ride the resident's own talk client, like every
// other small verdict it makes about itself.
if craftShelf != nil {
reconciler = reconciler.WithCraftMind(resident.NewCraftMind(craftShelf, craftDir,
fillCraftParams(settings, chatClient), repairCraft(settings, chatClient)))
}
if err := reconciler.AttachSession(session); err != nil {
return nil, brain.abandon(err)
}
web := exec.NewWeb()
// chatWorkerCeiling is not a concurrency policy — the provider's adaptive
// rate limiter is the real throttle. This is a local sanity bound on
// goroutines/file handles, far above any realistic graph width.
const chatWorkerCeiling = 32
// The price before the purchase. It is built here so the runner can consult
// it on the very first tick of a claimed leaf, which is the last moment at
// which not starting the work is still free.
desk := newConsentDesk(graph).WithHeadless(opts.consent)
desk.Rehydrate()
// Declared before it is built so the leaf builder inside can reach it. The
// one thing it says back to the runner is how long this leaf's own watchdog
// is, which is what keeps the claim reaper above every deadline actually
// granted (resident.Runner.RaiseStaleAge).
var runner *resident.Runner
runner = resident.NewRunner(graph, func(ctx context.Context, node store.Node) (resident.ExecResult, error) {
isReflex := node.Group == resident.ReflexGroup
// Nothing above this line spends anything, which is the whole point of
// its position: a job whose estimate crosses the threshold is held and
// asked about before its first worker has said a word. The runner's
// landing path reads the hold and releases the claim, so this returns
// nothing and the node goes back to the queue rather than lying about
// having finished or failed.
if desk.Gate(settings, measured, node) {
return resident.ExecResult{}, nil
}
// Each top-level job works in its own directory: one thread hosts
// many unrelated jobs, and continuity between them travels through
// the graph as digests and absolute paths, never through a shared
// folder they could trample.
//
// An errand is the one case with nothing to trample. It is a single job
// pointed at a directory somebody named, every node of it — leaves,
// gate-bought extensions, overrun continuations — shares that directory,
// and the files already in it are the work.
jobDir := workspaceRoot
// Whether the directory is the job's own. It matters to one reader: the
// bank a dead attempt hands to its retry names the files already there,
// and in a job's own folder those files are that attempt's work, while in
// an errand's they are the person's project — which nobody produced, and
// which must never be announced to a leaf as "files already produced" it
// is free to skip.
ownWorkspace := !opts.sharedWorkspace
if ownWorkspace {
jobDir = filepath.Join(workspaceRoot, jobIDOf(graph, node))
}
jobSpace, err := exec.NewWorkspace(jobDir)
if err != nil {
return resident.ExecResult{}, err
}
// WHERE THIS JOB'S WORK HAPPENS IS A PROPERTY OF THE JOB, said once,
// here, by the only code that decides it. Four paths grow a running job
// and every one of them has to weigh what the last round changed
// against what the job is about — which cannot be read without knowing
// which tree to read it in. Threading it through four signatures owned
// by four waves is how a fact comes to be true on whichever caller
// somebody remembered. See resident.RememberJobWorkspace.
resident.RememberJobWorkspace(graph, node, jobDir)
jobSpace = jobSpace.WithScratch(scratchRoot)
if opts.sharedWorkspace {
// Whose directory this is, said as its own fact. The engine reads it
// to decide it may never check the root out from under somebody's
// work; it used to read "the machinery went elsewhere" instead, which
// stopped being the same question the moment the machinery always did.
jobSpace = jobSpace.OwnedByPerson()
}
staged, err := exec.StageAttachments(jobSpace, attachmentStoreRoot(path), node.Provenance.Attachments)
if err != nil {
return resident.ExecResult{}, err
}
documentPaths := staged.Documents
// A planned job keeps one executor for every leaf so its profile key names
// the model that actually produced all measured turns. A picker change
// applies to the next job rather than relabeling work already in flight.
planPrefix, planGraph, planNode, workingModel, workingClient := plans.lookup(node.ID)
if workingClient == nil {
// A job rehydrated from the journal after a restart carries its
// structure but no live client, and its leaves genuinely do run on
// the current work model — so the profile key must name that one
// rather than the model that planned the job in a process that is
// no longer here.
workingModel, workingClient = taskClient.Snapshot()
}
// A model the user named for this job outranks both, and only for this
// job: the request rides the node's provenance, so every leaf under it
// is served by that model however the work slot moves afterwards.
escalatable := taskClient.Escalatable()
if pinned, ok := pinnedWorkClient(boostClients, node); ok {
workingModel, workingClient = pinned.Snapshot()
escalatable = pinned.Escalatable()
}
// What runs this leaf was settled when the job was admitted, and the
// node's row carries the answer. Reading it here rather than deciding
// it here is the whole point of journaling it: a leaf claimed an hour
// after its splice runs on what it was promised.
subharness := leafSubharness(node)
// What actually fed this leaf, counted and weighed once, here, at the
// moment it is claimed. Every budget below is arithmetic over these two
// numbers, and they are read once precisely so that they cannot move: a
// grant recomputed between turns would move the prompt prefix with it,
// under a cache that is counting on it not moving.
fanIn, fanInErr := graph.DependencyFanIn(node.ID)
if fanInErr != nil {
// A budget that could not be measured falls back to the flat one,
// which is what every leaf had before this existed. Zero is the
// correct value for "nothing fed this", and it is also the safe
// value for "nobody could say".
log.Printf("note: could not measure what fed %s: %v", node.ID, fanInErr)
fanIn = store.DependencyFanIn{}
}
// Ordinary leaves retain the byte-identical headless envelope, raised by
// exactly what their own fan-in measures — see gatheringGrant. A leaf
// re-dispatched after running out is granted more than the attempt that
// ran out, or it would buy the same truncated ending again — see
// regrantAfterRunningOut; the wall below follows the larger grant because
// it is arithmetic over the same number. Reflexes use the deliberately
// tiny rung budget and a seconds-scale watchdog.
turns, tokens := gatheringGrant(chatLeafTurns, chatLeafTokens, fanIn)
tokens = regrantAfterRunningOut(tokens, int(node.Attempt))
leafRoom := exec.SubharnessFor(subharness)
wallLeft := remainingWall(ctx)
deadline := leafRoom.DeadlineWithin(tokens, wallLeft)
watchdog := exec.WatchdogAbove(deadline)
if isReflex {
turns, tokens = reflexTurns, reflexTokens
deadline = reflexDeadline
watchdog = deadline + 15*time.Second
}
// The open envelope, kept whole, because a fold may hand it back. A node
// run as an assembly that could not assemble has had its own premise
// refused by the attempt, and the retry gets the shape and the room this
// leaf would have had if nothing had judged it — so a predicate that is
// wrong about a node costs one short call rather than the node.
openTurns, openTokens, openDeadline := turns, tokens, deadline
leafMedia := mediaModels.Snapshot()
leafMedia.WorkingModel = workingModel
leafMedia.VisionModel = settings.ResolveVisionModel(modelCatalog, chatClient.Model(), workingModel)
// A media purchase is the one spend in the system that is quoted before
// it is made, so it is the one place a rail can refuse rather than
// discover. Both rails are asked, in the order they bind: the day's
// ceiling governs the machine, and a task ceiling governs one subtree —
// a leaf inside a governed job must not spend the job's last dollars on
// a video while the rest of the job is being held for the same reason.
// An ungoverned graph pays nothing for the second question: the store's
// first act is to ask whether any ceiling exists at all.
leafMedia.BeforeSpend = func(_ context.Context, additional float64) error {
if settings.DailyBudgetUSD > 0 {
rail, _, gateErr := graph.PauseDailyRailWithAdditionalSpend(settings.DailyBudgetUSD, node.Provenance.SessionID, additional)
if gateErr != nil {
return gateErr
}
if rail.Reached {
return fmt.Errorf("daily budget reached")
}
}
task, _, gateErr := graph.PauseTaskRail(node.ID, node.Provenance.SessionID, additional)
if gateErr != nil {
return gateErr
}
if task.Reached {
return fmt.Errorf("this task's budget is reached")
}
return nil
}
// The wiring one worker is built from, kept rather than spent, because a
// second attempt may be given to a different kind of worker: everything
// here is a fact about the leaf and none of it is a fact about who runs
// it, so the same build serves whichever one does.
build := leafBuild{
settings: settings, client: workingClient, workspace: jobSpace, web: web,
graph: graph, media: &leafMedia, model: workingModel, models: modelCatalog,
maxTurns: turns, maxTokens: tokens, deadline: deadline, fanIn: fanIn,
// This surface settles its leaves through the path that grows the
// graph from a division request, so the verb has somewhere to go.
// See leafBuild.swarm.
swarm: true,
}
shape := "atomic"
if isReflex {
shape = "reflex"
}
if planNode != nil {
shape = exec.LeafShape(planNode)
// Frozen means frozen everywhere: the sentinel may not edit a
// node whose transcript is already being written. The two facts
// settled a few lines above go on with it, because this is the
// moment both are known and the plan node is what the profile
// record is later written from.
plans.markClaimed(planGraph, planNode, subharness, fanIn.Count)
}
// Every input is named. Untitled, they render as `=== from "" ===`
// under a header that says the results are prior work the leaf already
// has and must not gather again — so a standing lesson reading "check X
// before Y" arrived as a claim that X had been checked. The notebook is
// not a prior result and says so; a dependency says whose it is.
inputs := leafNotebookInputs(graph, node)
// What the fan-in actually delivered, measured here because the fold
// decision below turns on it: a node may only be run as an assembly if
// every result it assembles arrived whole. Handles are the count of the
// ones that did not.
pushed, handles, carried := 0, 0, 0
dependencies, err := graph.DependencyInputs(node.ID, build.dependencyPot())
if err == nil {
for _, dependency := range dependencies {
// The files are what make the 300-word cap survivable: a
// producer keeps its answer in the message and its working in a
// file, and its consumer can only honour that split if it is
// told where the file is. Without this the pointer was written
// and never delivered.
files := dependency.Artifacts
pushed += len(dependency.Digest)
if dependency.NodeID != "" {
carried++
}
if dependency.Handle != "" {
handles++
// Digest in the prompt, whole result on the end of a path.
// The handle joins the artifacts because that is already the
// list the brief renders as "read them if you need the full
// detail" — which is exactly the sentence a handle wants said
// about it, and the leaf already reads paths with sh.
//
// This is the pull side of the policy, and with the product
// on the edge it is finally the exception rather than the
// rule: a dependency comes back on a handle when its material
// genuinely did not fit, and not merely because its producer
// summarised itself in one sentence.
files = append(append([]string(nil), files...), dependency.Handle)
}
inputs = append(inputs, exec.Input{
Title: dependency.NodeID,
Result: dependency.Digest,
Artifacts: files,
// True exactly when the text above is the material and not an
// account of it. A clipped input is an account of it again,
// and the brief must not tell the leaf otherwise.
Whole: dependency.Handle == "",
})
}
}
// The fold decision, taken here because here is the only place both
// halves of it are knowable: the plan says whether this node has
// anything to go and find, and the fan-in that just landed says whether
// it is holding what it was promised.
//
// Neither half alone is safe. A plan that names no sources describes a
// node that should be able to assemble in one pass, and a node whose
// inputs came back on handles cannot, whatever its plan says — it has
// files to open, and opening them is turns. The conjunction is what makes
// the cap honest.
//
// carried must equal what was measured, not merely reach two: a fan-in
// too wide for the pot arrives with its tail named in one sentence
// instead of carried, and a node assembling from a list of the things it
// was not shown is a node with fetching to do.
fold := !isReflex && planNode != nil && plan.Folds(planNode) &&
fanIn.Count >= 2 && carried == fanIn.Count && handles == 0
if fold {
turns, tokens = foldGrant(modelCatalog.ContextLength(workingModel),
exec.FoldTurns, pushed, tokens)
wallLeft = remainingWall(ctx)
deadline = leafRoom.DeadlineWithin(tokens, wallLeft)
watchdog = exec.WatchdogAbove(deadline)
build.maxTurns, build.maxTokens, build.deadline = turns, tokens, deadline
}
// Journaled either way, so a benchmark can tell a fold that fired from a
// node that merely finished quickly, and so a fold that did not fire can
// be argued with rather than guessed at.
leafMode := store.LeafMode{
Mode: store.LeafModeOpen, Deps: carried, Pushed: pushed, Handles: handles,
Turns: turns, Tokens: tokens,
}
if fold {
leafMode.Mode = store.LeafModeFold
}
if modeErr := graph.RecordLeafMode(node.ID, leafMode); modeErr != nil {
log.Printf("note: could not journal the dispatch shape of %s: %v", node.ID, modeErr)
}
// The worker, and the record that it is this one. Both come out of the
// same call because the record is worthless if a dispatch path can get
// its executor without leaving it — see runningWorker.
worker := runningWorker(node.ID, subharness, build, "")
// The steering mailbox: user messages anchored to this node land in
// the worker's transcript before its next turn. The cursor starts at
// zero so guidance sent while the node was still queued applies too.
var steerCursor int64
steer := func() []string {
messages, err := graph.NodeMessages(node.ID, steerCursor, 20)
if err != nil {
return nil
}
var lines []string
for _, message := range messages {
steerCursor = message.Seq
if message.Role == store.RoleUser {
lines = append(lines, message.Body)
}
}
return lines
}
// The contract is the working method and belongs in the system message
// beside the harness's own invariants, where a headless run already
// puts it: together they are what a specialised harness for this domain
// would have been hand-written to say, and they are the frozen prefix
// every turn of the leaf is billed against. Folded into the brief it
// still arrived, but as user text below the churn.
leafTitle := firstLine(node.Brief)
if title := strings.TrimSpace(node.Title); title != "" {
leafTitle = title
}
outputHint, intermediate := leafOutputHint(node, leafTitle, jobSpace)
// The job blackboard: what one worker learns mid-flight that changes how
// the others must act — a discovery about the material, a pitfall, a
// decision to match. A note is an agent message anchored to the job's
// root, and every leaf of the job (the sink included) reads the root at
// the same between-turn boundary that already reads user steering. A
// leaf that starts late reads the whole board on its first poll because
// its cursor starts at zero; a single-leaf job carries neither the tool
// nor the poll, so the atomic path pays nothing.
jobRoot := jobIDOf(graph, node)
// Which board rows this leaf wrote. A leaf must not read its own notes
// back, and it used to know which were its own by finding its own TITLE
// glued to the front of them — so every note carried a clipped node title
// colon-joined to a sentence, and the room showed lines like
// "⚑ Assemble the three country sections into a: Brief saved to …": a
// name cut mid-phrase, wearing a colon it never earned, in front of the
// one part of the line that says anything. The row's own sequence answers
// the same question exactly, so the note is just the note.
mine := map[int64]bool{}
// The same lines, kept where a retry can reach them. A note anchors to
// the JOB root, so a leaf that is not its own job root cannot find its
// own notes again by reading its own record, and the journal cannot tell
// a reader which of a job's notes came from which leaf. What this leaf
// said, this leaf remembers. See resident.Bank.
banked := &sharedLines{}
var share func(string) error
if node.Parent != store.RootID {
share = func(line string) error {
// The board is workers talking to workers. It is read off the job
// root by node, never by session, so it belongs to the record and
// nothing is lost by keeping the conversation out of it — a person
// watching a job saw one ⚑ line per note per leaf, which is the
// machinery's internal correspondence delivered to their inbox.
posted, postErr := thread.Record(graph, store.Message{
Role: store.RoleAgent,
NodeID: jobRoot,
Body: jobNoteBody(line),
})
if postErr == nil {
mine[posted.Seq] = true
banked.add(line)
}
return postErr
}
}
// Reading the board costs one indexed query per turn boundary and zero
// tokens when it is empty, so every leaf reads it — including the sink,
// whose merge is exactly where a sibling's warning matters most.
var boardCursor int64
board := func() []string {
messages, boardErr := graph.NodeMessages(jobRoot, boardCursor, 20)
if boardErr != nil {
return nil
}
var lines []string
for _, message := range messages {
boardCursor = message.Seq
note, isNote := jobNoteLine(message)
if !isNote || mine[message.Seq] {
continue
}
lines = append(lines, note)
}
return lines
}
// The pickup bank. A node whose attempt counter is already above zero has
// been claimed before, which means a previous run of this leaf ended
// without settling it — its process went away, the whole surface did, or
// the claim reaper took it back — and it is on the queue again. It used to
// come back to an empty context and a directory full of its own work, and
// the surface said so out loud: "each starts again from the beginning". It
// does not have to. What that attempt reached is on its own record and its
// files are still on disk, and both ride in as one more input under the
// same headers a re-decomposed leaf already gets. See resident.Bank.
//
// AND THE RESUMPTION IS JOURNALED, because the promise above was made on
// 2026-08-29 and quietly not kept: the seed was assembled from a turn-number
// window read across every attempt in the record at once, so a leaf on its
// third claim was handed an interleaving of two earlier attempts and opened
// by exploring the repository it had spent forty minutes in. Nothing in the
// store said whether a claim had resumed or started over, which is why it
// took a transcript autopsy to find out. Now the row says so and the
// headless stream reads it (store.EventLeafResumed).
// carriedFiles is what an earlier attempt at this same leaf left on disk,
// kept because it is half of the reading below: the tree this attempt is
// starting from is the job's record plus whatever the attempt before it
// wrote, and a before-and-after that forgot the second half would read a
// resumed leaf's inherited files as this attempt's own work.
var carriedFiles []string
if node.Attempt > 0 {
if bank, recorded := leafBank(graph, node, jobSpace, jobDir, ownWorkspace, nil, nil); !bank.Empty() {
inputs = append(inputs, bank.Input())
carriedFiles = bank.Artifacts
if recorded > 0 {
if resumeErr := graph.RecordLeafResumed(node.ID, store.LeafResumed{
Turns: recorded, Files: bank.Artifacts,
}); resumeErr != nil {
log.Printf("note: could not journal that %s resumed: %v", node.ID, resumeErr)
}
// AND IT IS A ROUND. A leaf resumed in place spends a body
// of work exactly as a spliced repair does, and the growth
// ledger never saw one — so eight exhaustions of one lineage
// cost ink s10 its whole wall while the standstill rule,
// which counts rounds out of that ledger, weighed five.
resident.NoteResumedRound(graph, node, bank.Artifacts)
}
}
}
// THE WORLD AS THIS LEAF FOUND IT, read once, here, before the worker is
// given anything to do. It is the earlier half of the only honest answer
// to "did this attempt change anything" — the later half is taken where
// the delivery gate is asked, and two equal stamps are an attempt that
// moved nothing at all. Reading it any later reads a tree the worker has
// already been in. See revision.TreeStamp and nothingChanged.
startedWorldAs := revision.TreeStamp(jobArtifacts(opts.produced, carriedFiles))
task := exec.Task{
Reflex: isReflex,
Fold: fold,
Subharness: subharness,
NodeID: int(node.CreatedSeq),
// The identity everything this leaf writes is filed under, and it is
// the node's own id for the same reason its output path is (see
// leafOutputHint): the creation sequence belongs to the whole splice.
// Under it, four siblings of one job shared one artifact bucket, one
// flight recorder and one set of .obs spill names — so each was told
// the others' files were its own, their recorders interleaved into a
// single document, and a spilled observation was overwritten by a
// sibling's while the stub in its context still pointed at the file.
NodeKey: node.ID,
StoreNodeID: node.ID,
Title: firstLine(node.Brief),
Goal: node.Provenance.Intent,
Brief: withDocumentAttachmentBrief(residentDeliveryBrief(graph, node), documentPaths),
Contract: leafContract(plans, planNode, node),
// The whole object, beside the two halves of it the executor
// already reads. Nothing in the generic loop renders it today; it
// is here so a worker that speaks a spec is handed one rather than
// having it reassembled from prose at the boundary (W3).
Spec: leafSpec(plans, planNode, node),
Skills: leafSkills(planNode),
OutputHint: outputHint,
Intermediate: intermediate,
Inputs: inputs,
Steer: steer,
Share: share,
Board: board,
Control: func() exec.ControlAction {
control, err := graph.Control(node.ID)
if err != nil {
return exec.ControlNone
}
if control.CancelRequested {
return exec.ControlCancel
}
if control.Held {
return exec.ControlPause
}
return exec.ControlNone
},
ImagePaths: append([]string(nil), staged.ImageFiles...),
DocumentPaths: append([]string(nil), documentPaths...),
// Within-node progress, through the one channel the thread already
// has for "this is still happening": the same replaceable rows a
// compile posts, anchored to the job rather than typed into it.
// A leaf that runs for the better part of an hour would otherwise
// be a spinner, and the two alternatives — splicing its insides
// into the graph, or narrating them as messages — are the two
// things the leaf contract forbids by name.
Progress: leafProgress(graph, resident.PlanAnchor{
NodeID: jobRoot, SessionID: node.Provenance.SessionID,
}),
// A caught fault is the other within-node fact a person needs and
// the one that used to reach nothing but a log file. It is filed
// against this node rather than the job root, because what a reader
// wants to know is which piece of work was interrupted.
Fault: leafFault(graph, node.ID),
}
// The scheduler's quality loop, inline: each attempt is one routable
// unit carrying its call shape, a watchdog sits above the leaf's own
// deadline so a wedged executor becomes a recorded failure rather
// than a silent hang, and a leaf whose verdict says a stronger model
// might fix it gets exactly one escalation when a panel offers one.
//
// A STRONGER MODEL IS THE ONLY SECOND RUNG THERE IS. There is one
// worker, so a retry is the same worker with whatever the model ladder
// can offer it, and a leaf that still cannot do the work ends with its
// failure named rather than being handed somewhere else.
//
// The acceptance checklist, journaled against the work it will be used
// to judge, before that work starts. It is written here rather than at
// plan time because a node has to exist for an event to hang on, and
// because this is the one place that knows which spec this leaf is
// actually running with — a spliced repair inherits its predecessor's
// checklist and must be seen to.
//
// It is also the only place a person watching a headless run learns the
// checklist exists at all: the stream reads this event (see narrateOne
// in do.go). A fail-safe that does not reach the person reading the
// verdict is decoration — FAILSAFE clause 3.
journalAcceptance(graph, node, task.Spec)
attempts := 1
if !isReflex && escalatable {
attempts = 2
}
// One job is one cache lineage, exactly as one headless run is: the
// affinity key rides every leaf of the job so a prefix cache warmed
// by one worker serves its siblings.
ctx = provider.WithCacheKey(ctx, provider.RunCacheKey(node.Provenance.Intent, workingModel))
var outcome *exec.Outcome
// Every call this leaf makes, banked under its node as the provider
// answers. See leafBanker.
banker := newLeafBanker(graph, node.ID)
var spent exec.Usage
// The same spend with its per-turn shape kept, accumulated across every
// attempt this node makes. See leafShape.
var spentShape []exec.TurnUsage
spentTurns := 0
workerModel := taskClient.Model()
// Every gathering node runs. There was once one that did not: the sink
// of a declared bundle, whose parts were the person's own requests
// verbatim and therefore self-contained deliveries, so joining them in
// the asked order was assembly rather than judgment and skipped a model
// pass worth 121 of a 212-second job. That shortcut was the bundle
// route's to give — it rested on the route's guarantee about what its
// leaves were — and it went out with the route. A planned subtree makes
// no such promise: its leaves are as often fragments of one deliverable
// as they are whole answers, and concatenating those would ship a seam
// where the delivery should be.
for attempt := 0; attempt < attempts; attempt++ {
// An escalation that repeats the task verbatim buys a stronger model
// and then pays it to rediscover everything the first attempt found —
// including files sitting in the shared workspace it is about to
// write again. The gate's revision pass has always been sighted this
// way; the escalation was the one retry that was not.
// outcome is deliberately not required. It used to be, and that
// condition was the whole of the incident: a leaf abandoned by its
// watchdog returns no outcome at all, so the one retry that most
// needed sighting — the one whose first attempt had been working for
// an hour — was the one that skipped this block entirely and re-ran
// the assignment verbatim. What that attempt reached is in the
// journal and on disk either way. See resident.Bank.
if attempt > 0 {
attempted := task
if bank, _ := leafBank(graph, node, jobSpace, jobDir, ownWorkspace, outcome, banked.lines()); !bank.Empty() {
attempted.Inputs = append(append([]exec.Input{}, inputs...), bank.Input())
}
// The fold is released here and nowhere else. Whatever brought
// this leaf to a second attempt, it is evidence that this node
// did not assemble in one pass, and running the retry under the
// same two-call cap would buy the same ending twice.
if attempted.Fold {
attempted.Fold = false
build.maxTurns, build.maxTokens = openTurns, openTokens
build.deadline = openDeadline
watchdog = exec.WatchdogAbove(build.deadline)
worker = runningWorker(node.ID, subharness, build, "")