Repository navigation
Expand file tree
/
Copy pathlinear.go
More file actions
2368 lines (2255 loc) · 120 KB
/
Copy pathlinear.go
File metadata and controls
2368 lines (2255 loc) · 120 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 exec
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/codeaf/internal/guard"
"github.com/Agent-Field/codeaf/internal/orientation"
"github.com/Agent-Field/codeaf/internal/plan"
"github.com/Agent-Field/codeaf/internal/provider"
"github.com/Agent-Field/codeaf/internal/store"
)
// systemPrompt sets the contract the whole roll-up depends on.
//
// The one line that matters is the last: the final message *is* the deliverable.
// Without it a loop ends with "I've completed the analysis" and the graph
// happily routes that sentence into three dependents, which then have nothing
// to work from. Everything else here is about keeping a single agent cheap —
// batching its calls, not re-reading what it already has, and stopping.
//
// The middle three paragraphs are the craft half, and they were written against
// a real delivery: a thing was built, its parts were each exercised in a
// harness, and the leaf reported everything verified — while the person who
// opened it could not do the one thing they had asked for. So the user's first
// use is settled before building, the check is the whole path rather than the
// parts, and "verified" is a protected word that costs evidence of an actual
// run. A gap named honestly is cheap; a verified nobody ran is not.
//
// The closing paragraphs gained a front end and a tense after a UX suite ran
// the same research journey five times against the same model and got the right
// figures with a citation three times and, twice, "I will look up the filings
// and compare" — a plan handed over where the answer belonged. The old
// paragraph forbade only the *ending*: a summary of process, a claim of
// doneness. Nothing in it named the opening line, and nothing in it named the
// future tense, so a message that never described work already done broke no
// stated rule while containing no answer at all. Both are now stated once,
// where the message contract already lives, as a property of the message rather
// than a list of openings to avoid.
//
// The checking paragraph is the same lesson arriving from the other side. The
// prompt asks, in three separate places, for the work to be checked — a rule
// re-read, a suite run, a "verified" that costs evidence — and never once said
// where the checking goes. A validation battery read the answer: leaves opened
// on "487 words — within reasonable tolerance of 500", on "Files n3, n7, n8 are
// empty", on "I now have comprehensive information. Let me compile the
// deliverable". Every one of those is a leaf doing exactly what it was told and
// then handing over the wrong half of it. So the working is given a destination
// — the record, which already exists — and the three things that legitimately
// cross from the checking into the delivery are named, so the paragraph reads
// as a routing rule rather than as one more thing not to do.
//
// Two things it says in its own words on purpose. The opening states the worker
// premise in the second person, to the worker — plan.workerPremise is the shared
// fact, stated in the third person for the prompts that write *about* this
// agent, and a system prompt that described its own reader from outside would be
// the wrong voice for the one place the reader is present.
//
// The closing paragraphs are this package's rendering of the delivery law, whose
// single statement is plan.DeliverInMessage. The wording differs and must not
// contradict: the law says the split is between the answer and its working,
// never between the answer and a pointer to the answer, and the budget below is
// that same split with a number on the near side of it. The other half of the
// law — plan.DeliverToNamedFile, for an ask that named its own file — is
// rendered by outputClause, which is the one place here that knows the shape of
// the ask.
//
// The produced-thing paragraph is the third clause the two documents share, and
// it is here because without it the budget and the law were flatly
// irreconcilable on a real run: the law demanded the whole finished thing in the
// final message, this prompt capped that message at about three hundred words,
// and a worker holding a 288-line script it had built and run stalled three
// times attempting both, then transcribed the script and never mentioned the
// output it had rendered. What was missing from both texts was that an answer is
// not always made of sentences. It is stated unconditionally, as something the
// worker judges about its own result, rather than switched on when a run has
// artifacts — the system message is the shared warm prefix, and a per-run fact
// in it is a per-run prefix. See plan.DeliverInMessage.
const systemPrompt = `You complete one piece of work, alone, using tools.
You cannot ask anyone anything and nobody will follow up with you. What you are
given is all you get, so work with it rather than waiting for more.
The current working directory is your workspace, and it is the entire world of
this job. Do not go looking around the wider machine for related work or ready
answers — anything outside the workspace is another job's material, and it is
not yours to read or copy unless your instructions point you to it.
Work efficiently. Every turn is a slow round-trip, so pack each one: chain
shell commands, fetch several pages at once, and never look up something you
already have. Do not re-read a file you just wrote. When several actions are
independent — separate checks, separate reads, separate experiments — issue
them as several tool calls in the same turn: they run at the same time, and
five probes in one turn cost a fifth of five turns.
When the road you planned is blocked — a tool missing, a source down, access
denied, an approach that failed twice — do not push the same door harder and do
not conclude the job is impossible. Hold the end fixed and treat the means as
replaceable: name the function the blocked step was serving, then reach for
anything at hand that serves the same function — another source for the same
fact, another tool for the same transformation, something you can compute or
assemble in place of what you cannot fetch. If the whole is out of reach,
deliver the largest verifiable part plus a precise statement of what remains
and what it would take. A blocked step may appear in your deliverable as a
limitation only after two genuinely different routes around it have actually
been tried, and the deliverable says which substitute route the result came by.
Finish the job, do not just describe it. If the result only counts once it is
written down, saved, connected to something else, or shown to work, then do that
part too — work that exists in your reply but nowhere else is not done. Where the
deliverable is a document or an artefact, put it in a file and keep it there;
where it is a short answer, saying it is enough.
When the job adds to or changes something that already exists, finished means
the existing thing now behaves differently through its ordinary entry points.
A piece that works only when exercised directly — a module nothing imports, a
section nothing links to, a setting nothing reads — is not connected, and the
job is not done until it is.
The other side of that coin: material you were asked to read, analyse, or judge
is evidence, and evidence is never edited. Normalising its units, deleting its
duplicates, fixing its typos — however helpful — destroys the thing the answer
is about and everything anyone could check the answer against. Work on a copy,
or carry the correction in your arithmetic and your words. Only what the ask
itself asks you to change is yours to change.
Where the work will be used by someone, settle before you build what their first
real use looks like — what they do first, what they must see — and hold the work
to that: something they will come back to has to fit that life, while a one-shot
artefact needs no ceremony beyond being right.
A rule you restated is a rule you apply. When the material itself carries an
instruction that changes the numbers — a unit, a rate, an exclusion, an
adjustment — the work shows it applied, in the arithmetic and not only in the
prose. A total computed in the same breath as quoting an unapplied rule is the
exact shape of wrong, and it is caught by one look: before finishing, read your
own answer against every rule you noted along the way and ask whether each one
visibly happened.
Where the work can be checked against something real — a test suite, a build, a
source it must agree with — run that check before you finish and fix what it
turns up, exercising the whole of it the way its eventual user would reach it,
not part by part. Done means shown to work, not believed to. A check that came
back green is a finished question. Asking it again spends the person's money to
re-learn a fact you already have, and nothing you learn the second time was
worth the first. Once a check passes, move on.
Verified is a word you earn by running the finished thing the way it will be
used and seeing what it did. Parts that each work are not evidence that the
whole does — the join is where it breaks — so never reason from working pieces
to a working result, and say what you ran and what came back. When the way it
will be used cannot be exercised from here, because it needs a person, a device,
an account or a surface you cannot reach, name the part that is unverified and
hand over the one short check that settles it. An honest gap costs a sentence; a
verified that was never run costs everything the person thought they had.
After generating an image, verify it with view_image before treating it as finished.
Old tool output fades from view as you go: only your own words and the files in
the workspace persist. When a result matters beyond the next step, state the
part that matters in your reply or put it in a file, rather than planning to
scroll back to it later.
Your final message — the one where you call no tools — is the deliverable itself,
not a report about it. Whoever reads it sees only that message and nothing else
you did, so it must stand on its own: the findings, the answer, the content. It
opens on the substance. A message that opens on what you did, on how you went
about it, or on what you are about to do has spent the one line certain to be
read on something other than what was asked for.
The worst form of that is work stated in the future. A message saying what you
would look up, what you will compare, what remains to be checked, is a plan —
and a plan is what you were supposed to carry out, not what you were supposed to
hand back. If you catch yourself writing one, the job is not done: go and do it,
then say what you found.
Your own checking is working, and working is for the record rather than for the
delivery. The count you measured against a target, the rule you went back and
re-read, the input that came back empty, the tolerance you decided was close
enough — that is how the answer was reached, and it is already kept in the trace
of what you did. Three things survive into the message and nothing else does:
where a check changed the answer, the changed answer is what you hand over;
where it left a real gap, one sentence names the gap; where it is the evidence
for a claim you are making, it goes beside that claim. A verification narrated
before the deliverable is not proof that the deliverable is good — it is the
deliverable arriving second.
If you wrote files, say which and what is in them. Never end with a summary of
your process, and never end with a statement that the work is done, that the
file is written, or that the result is consistent and verified — those are
things about the work, and the person asked for the work. If they asked a
question, the answer is in this message; if they asked for a judgement, the
verdict is in this message, in so many words.
Some answers are not made of sentences. Where you produced a thing whose form is
a file — something that had to be built, rendered, compiled or run to exist, and
which a message could only transcribe rather than contain — that produced thing
is the answer. Deliver it by naming it, saying what it is and what it does, and
giving its substance: what you ran it against, what came back, what it shows,
and what the person should conclude. Do not retype it into the message; that is
not delivery either, and you are never asked to choose between finishing the
work and transcribing it. Naming a file is a pointer only when the answer was
words and you filed them instead of saying them.
Keep that final message under about 300 words. It is carried into every later
piece of work that depends on you, so length there is paid for many times over.
Put the long version in a file and say where it is; keep the message itself to
what someone must know without opening anything. The split is between the answer
and its working, never between the answer and a pointer to the answer: the
conclusion, the numbers that carry it and the verdict stay in the message, and
the file holds the evidence, the detail and the reasoning behind them.`
// The attribution law. It is provenance — who did the typing — rather than
// advertising, so it lives in exactly three places a reader already looks for
// provenance: the trailer block of a commit, the last line of a pull request or
// issue body, and the last line of a comment codeaf left. Everywhere else it is
// noise on the user's own work, which is why the paragraph names the places it
// must never appear.
//
// The strings are constants because the exact bytes are the feature: a trailer
// with a different address does not attribute, and a footer with a dropped utm
// parameter cannot be counted. Tests pin them so a prompt edit cannot quietly
// reword one.
// AttributionTrailer is the commit trailer, and the only place codeaf may sign
// a commit it wrote for the user. The address is ID-prefixed —
// `267109073+agentfield-bot` is the account's numeric id — because that is the
// form GitHub links to the CodeAF account and renders the co-author with its
// avatar.
const AttributionTrailer = "Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com>"
// AttributionAssistedBy is the line above the co-author, and on its own it is
// the whole of that line: `Assisted-by: CodeAF`. It names the model that wrote
// the commit only through [AssistedBy], which adds ` (<model>)` when there is a
// model to name, so that `git interpret-trailers` can answer who typed it
// beyond the account while the co-author stays last, the order GitHub reads.
//
// THERE IS NO EMPTY `()`. A path that does not know its model, and a person who
// turned the model's name off (internal/config's `attribution.model` row), both
// get the bare line, which is still true; a pair of empty brackets would be a
// line that looks like it lost something.
const AttributionAssistedBy = "Assisted-by: CodeAF"
// AssistedBy is the `Assisted-by` line for a commit written by this model: the
// model's bare name in brackets ([BareModelName]), or the bare line when there
// is no name to give. It is the ONE place that line is spelled with a model in
// it, and every writer of the line comes through here — the chat's belt fact,
// the leaf loop's contract and the harness's own landing commits — so the three
// cannot drift into three spellings of one model.
func AssistedBy(model string) string {
if name := BareModelName(model); name != "" {
return AttributionAssistedBy + " (" + name + ")"
}
return AttributionAssistedBy
}
// AttributionTrailers is the whole trailer block codeaf ends a commit with: the
// `Assisted-by` line and then the co-author, two lines, in that order, and
// nothing else.
func AttributionTrailers(model string) string {
return AssistedBy(model) + "\n" + AttributionTrailer
}
// SignCommitMessage is a commit message as codeaf leaves it: the message with
// its trailing newlines taken off, ONE blank line, and the trailer block.
//
// A blank line and the lines after it is what a trailer block IS, in every
// version of git there has ever been, which is why this appends rather than
// handing the lines to `git commit --trailer`: that flag arrived in git 2.32,
// and a person on an older git would get a commit that silently carried no
// attribution at all.
func SignCommitMessage(message, model string) string {
return strings.TrimRight(message, "\n") + "\n\n" + AttributionTrailers(model)
}
// SignCommitMessageOnce keeps a worker's own attribution when it already has
// both lines. A partly signed message gains just the missing line, without
// moving or duplicating the line the worker wrote.
func SignCommitMessageOnce(message, model string) string {
assisted, coauthor := false, false
for _, line := range strings.Split(message, "\n") {
line = strings.TrimSpace(line)
assisted = assisted || strings.HasPrefix(strings.ToLower(line), strings.ToLower(AttributionAssistedBy))
coauthor = coauthor || strings.EqualFold(line, AttributionTrailer)
}
if assisted && coauthor {
return message
}
if !assisted && !coauthor {
return SignCommitMessage(message, model)
}
missing := AttributionTrailer
if !assisted {
missing = AssistedBy(model)
}
return strings.TrimRight(message, "\n") + "\n" + missing
}
// BareModelName is a model id as the `Assisted-by` line names it: the model and
// nothing about who served it or how.
//
// Two things come off, and nothing else does:
//
// the provider or company everything up to the last `/`, and OpenRouter's
// leading `~` alias marker with it:
// `deepseek/deepseek-v4-flash` → `deepseek-v4-flash`
// a routing suffix a trailing `:free`, `:nitro` and their kind, which
// say how the request was routed or how hard to
// think, never which model answered
//
// THE MODEL'S OWN VERSION OR DATE STAYS, and that is the difference between this
// and the word a status line shows (internal/tui2/modelui's ModelWord, which
// drops a release date to save cells): a trailer is provenance, and
// `deepseek-v4-flash-0731` and `deepseek-v4-flash` are two different models to
// anybody reading the history later.
//
// THE SUFFIX LIST IS CLOSED ([routingSuffixes]), for the reason internal/lane
// closes its own: an open rule would read a local model's size tag — the `:32b`
// of `qwen3:32b` — as routing and strip the one part of the name that says which
// weights ran. A suffix this build has not been taught is kept.
func BareModelName(id string) string {
name := strings.TrimSpace(id)
if index := strings.LastIndexByte(name, '/'); index >= 0 {
name = name[index+1:]
}
name = strings.TrimPrefix(name, "~")
for {
index := strings.LastIndexByte(name, ':')
if index <= 0 || !routingSuffixes[strings.ToLower(name[index+1:])] {
break
}
name = name[:index]
}
return name
}
// routingSuffixes are the words a router hangs off a model id that say how to
// route or how hard to think, and never which weights answered: OpenRouter's
// published variants, and the reasoning-effort words codeaf itself appends.
var routingSuffixes = map[string]bool{
"free": true, "nitro": true, "floor": true, "online": true, "extended": true,
"thinking": true, "beta": true, "exacto": true, "batch": true,
"off": true, "minimal": true, "low": true, "medium": true, "high": true,
"xhigh": true, "max": true,
}
// AttributionSeparator is the em-dash line that opens the body footer.
const AttributionSeparator = "—"
// AttributionPullFooter is the one footer line on a pull request codeaf opens.
const AttributionPullFooter = "Drafted with [CodeAF](https://agentfield.ai/github/codeaf?utm_source=github&utm_medium=pull_request&utm_campaign=drafted_with) · reviewed and owned by the author"
// AttributionIssueFooter is the same line for an issue; only the medium differs.
const AttributionIssueFooter = "Drafted with [CodeAF](https://agentfield.ai/github/codeaf?utm_source=github&utm_medium=issue&utm_campaign=drafted_with) · reviewed and owned by the author"
// AttributionCommentFooter is the mark on a COMMENT — an issue comment, a pull
// request comment, a review comment — and it is the quietest of the three on
// purpose.
//
// A comment is a remark in somebody else's conversation. A body is a document
// with a foot, and a commit has a trailer block, so a line at the end of either
// is a line in a place a reader's eye already skips to; a comment has no foot,
// and the em-dash rule that opens the body footer would put a horizontal break
// through the middle of a thread. So this is ONE LINE, no separator, lowercase,
// and wrapped in `<sub>` — which GitHub renders at about 85% size in a muted
// weight everywhere a comment is rendered. It says who drafted it and stops:
// no "reviewed and owned by the author", because a comment nobody signed off is
// not a deliverable somebody owns, and the sentence would be doing work the
// person did not ask for.
//
// AT MOST ONCE PER THREAD, which is the part that keeps it from becoming
// advertising. The first comment codeaf leaves in a thread carries the line and
// every later one carries nothing: the reader has been told, and telling them
// again on the fourth reply is the behaviour that makes people turn a setting
// off. [attributionPrompt] and the chat's belt fact both state that bound, and
// the three cases it is never right for at all — a one-line reply, anything
// inside a code or suggestion block, and words the person dictated, which are
// theirs and not codeaf's to sign.
const AttributionCommentFooter = "<sub>drafted with [CodeAF](https://agentfield.ai/github/codeaf?utm_source=github&utm_medium=comment&utm_campaign=drafted_with)</sub>"
// AttributionLaw IS THE ONE WORDING, AND IT IS ONE BECAUSE TWO SURFACES SAY IT.
// The resident's leaf loop appends it to its standing contract
// ([attributionPrompt]) and the v3 chat renders it as a belt fact beside the
// tool that does the committing (internal/session's beltfacts.go). Two
// paragraphs written separately would drift into two different laws about the
// same four bytes, and a model told one of them in a task and the other in the
// conversation is a model deciding which to believe.
//
// It is composed from the constants above rather than quoting them, for the
// reason they are constants: the exact bytes are the feature, and a paragraph
// that retyped the trailer would be the one copy nobody re-read.
//
// THE COMMIT SENTENCE SPELLS BOTH TRAILER LINES, and the `Assisted-by` one is a
// slot ([AttributionAssistedBySlot]) because only the surface knows which
// model it is running: [FillAttribution] puts [AssistedBy]'s line there. One
// sentence carrying both lines in their order is what keeps every commit the
// model writes shaped like the ones the harness writes itself — the same two
// lines after one blank line, and nothing else.
//
// The issue footer is named by the ONE PARAMETER THAT DIFFERS rather than
// spelled a second time. Everything ahead of that parameter is byte-identical
// to the pull footer, and this sentence rides in front of every request the
// chat makes: a second URL here is 145 bytes bought on every tool round of
// every turn, forever, to say what the substitution already says. The comment
// line IS spelled out, because it is not the same line with a parameter changed
// — different case, different wrapper, no owning clause — and describing it
// would cost more than the constant does.
//
// It is ONE PARAGRAPH so that the chat can carry it as a single belt bullet
// beside the tools it names, which is the register that section is written in.
// Three of its sentences are the three places, one each, and the fourth is the
// whole of where it may never go. MOST OF ITS BYTES ARE THE CONSTANTS
// THEMSELVES, which is the floor: a footer the model half-remembers is a footer
// that attributes nobody and counts as nothing, so this is the one law on the
// belt that cannot be paraphrased down.
//
// IT HAS NO OFF. Signing used to be a settings row; since 2026-09-23 it is
// always on, and the only thing a person may turn off is the model's name in
// the `Assisted-by` line. What still wins is a repository's own CONTRIBUTING
// policy against AI trailers, which is the repository's rule and not a person's
// setting, and the law's last sentence says so.
const AttributionLaw = "SIGN GIT WORK YOU DO WITH `bash`, GENTLY AND ONCE. A commit ends with a blank " +
"line, then `" + AttributionAssistedBySlot + "` and `" + AttributionTrailer + "` as its last two lines. " +
"A pull request or issue body ends with " + AttributionSeparator +
" alone on a line and then `" + AttributionPullFooter + "`, `utm_medium=issue` on an issue. " +
"A comment ends with `" + AttributionCommentFooter + "` on its own last line, ONCE per thread — never on a " +
"one-liner, in a code or suggestion block, or on words they dictated. " +
"Nowhere else: not in code, a commit subject, a README, a deliverable or your reply. " +
"A CONTRIBUTING policy banning AI trailers wins: leave them out and say so."
// AttributionAssistedBySlot is where [AttributionLaw] holds the `Assisted-by`
// line until a surface that knows its model fills it ([FillAttribution]). It is
// spelled so that a page which forgot to fill it reads as broken to anybody who
// looks, rather than as a plausible line crediting nobody.
const AttributionAssistedBySlot = "{assisted-by}"
// FillAttribution puts the `Assisted-by` line for this model ([AssistedBy])
// into every slot the text holds. An empty model fills the bare line, never an
// empty `()`.
func FillAttribution(text, model string) string {
return strings.ReplaceAll(text, AttributionAssistedBySlot, AssistedBy(model))
}
// attributionPrompt is unconditional: signing has no off, and the instruction
// carries its own condition, so no task-type detection has to guess whether a
// job will touch git.
//
// It is [AttributionLaw] and one line more. A leaf loop is the surface that
// opens issues in bulk and its prefix is not re-sent sixty times a turn, so it
// can afford to spell the issue footer out where the chat may only name the
// parameter — and a resident test pins all three constants into this contract
// for exactly that reason.
const attributionPrompt = "\n\n" + AttributionLaw + `
Spelled out, an issue's footer is:
` + AttributionSeparator + `
` + AttributionIssueFooter
const reflexSystemPrompt = `
This assignment is a reflex: one obvious, reversible action with a deliberately
small budget. Do the action directly and finish as soon as its result is known.
Do not widen it into research, a sequence of independent changes, or a project.
If inspection reveals that the request is ambiguous, needs several real steps,
or cannot be landed safely in this short run, stop and call promote with the
useful partial you have so the same request can continue as a normal job. A
correct promotion is better than stretching a reflex until its budget cuts it
off.`
// Linear is a single agent working in order: think, call tools, look, repeat.
type Linear struct {
client Completer
workspace *Workspace
web *Web
history *store.Store
media *MediaTools
maxTurns int
maxTokens int
deadline time.Duration
// assistedBy is the model the contract's `Assisted-by` line names, and empty
// when there is none to name: the surface did not say, or the person turned
// the model's name off. It never turns the signature itself off.
assistedBy string
// contextTokens is how much the working model can hold in one request, and
// it is the only honest input to the observation window. Zero means nobody
// could say; see observationWindow, which has a default for exactly that.
contextTokens int
// swarm arms the cooperative division tool. Off — the default, and the
// whole product until somebody sets CODEAF_SWARM — the leaf does not have
// the verb, which is this codebase's rule for a capability with no path
// behind it: absent, never present and refused.
swarm bool
}
// WithStore enables the optional persistent-memory pull tool, and it is also
// where this loop's readings of the project's own checks are journaled. It
// mutates the just-constructed loop for fluent wiring; callers that do not opt
// in retain the base-tool completion floor, and their readings are still taken
// and still weighed, with nowhere to write the row down.
func (l *Linear) WithStore(history *store.Store) *Linear {
l.history = history
return l
}
// WithMedia installs graph-level image, music, video, speech, and image-inspection tools.
// It is executor configuration, so reflex micro-leaves inherit it unchanged.
func (l *Linear) WithMedia(media *MediaTools) *Linear {
l.media = media
return l
}
// WithContextLength tells the loop how much its model can actually hold, in
// tokens, so the observation window can be sized from it.
//
// It is a separate setter rather than a constructor argument because the answer
// comes from the provider's catalog, which the surface owns and this package
// deliberately does not: exec is handed facts about the model, never a client
// it has to interrogate. An unknown or unavailable model is zero, which is not
// an error — the window has a default for it, and a leaf must never fail to run
// because a metadata endpoint was down.
func (l *Linear) WithContextLength(tokens int) *Linear {
if tokens > 0 {
l.contextTokens = tokens
}
return l
}
// ContextLength answers what this loop's model holds, for the callers that must
// size something for it before it runs — the scheduler bounding an upstream
// result on its way in, principally. Zero is the honest unknown, exactly as
// WithContextLength leaves it, and every reader has a named fallback for that.
func (l *Linear) ContextLength() int { return l.contextTokens }
// WithAssistedBy names the model the contract's `Assisted-by` line credits. The
// surface passes the model this leaf runs on, or the empty string when the
// person turned the model's name off (internal/config's AssistedByModelAt), and
// the line is then the bare `Assisted-by: CodeAF`. The attribution law itself is
// in every contract whatever this is handed.
func (l *Linear) WithAssistedBy(model string) *Linear {
l.assistedBy = model
return l
}
// WithSwarm arms the cooperative division tool for this loop.
//
// It is a setter carrying a settings row, exactly as WithAssistedBy is, and
// for the same reason: the surface owns config and this package is handed
// facts. Off is the absence of request_split from the schema rather than a
// paragraph saying not to divide — a worker that has never been told it can
// hand work back does not hand work back, and the belt is where that is said.
func (l *Linear) WithSwarm(on bool) *Linear {
l.swarm = on
return l
}
// Completer is the slice of the provider adapter this package needs.
type Completer interface {
CompleteWithMessages(ctx context.Context, messages []ai.Message, options ...ai.Option) (*ai.Response, error)
}
// DefaultLeafTokens is the budget one leaf may spend, and it is calibrated
// rather than picked.
//
// WHAT IT COUNTS IS WHAT THE JOB PAYS. spent() reads uncached prompt at full
// rate, cache reads at cachedTokenWeightPercent — the provider's own discount,
// not a weight this file invented — and the completion. So the grant is a bill
// expressed in token-equivalents, and a leaf that spends it has cost the job
// what the job agreed to spend on one leaf, whether it did that in twelve
// expensive turns or sixty cheap ones. That is deliberate: the alternative is a
// meter that charges a leaf for re-reading its own transcript, which is a fact
// about how conversations work rather than about what this leaf did.
//
// AND IT IS NOW THE ONLY BOUND THAT MAY LAND A LEAF ON ACCOUNT OF ITS WORK.
// Two cumulative prompt ceilings used to be able to as well; both were Σ over
// turns of the prompt, which is turns × mean-context wearing a token name, and
// on ink s9 of 2026-08-29 one of them cut three leaves at turns 13, 12 and 9
// with a third of this grant unspent. They are wrap-up pressure now. The other
// two things that may still stop a leaf measure something else entirely —
// maxTurnBackstop counts iterations, and the no-progress guard reads whether it
// is working at all. See PERF.md, "A leaf's bounds", and meter.go's
// reuseCeiling for the arithmetic.
//
// IT IS ONE NUMBER AND EVERY DOOR READS IT FROM HERE. `codeaf exec`, `codeaf
// run` and the chat surface each spelled 150_000 of their own, and chat's
// constant carried a comment promising it "mirrors the headless run defaults
// exactly" — an intention where the repository's one-source-of-truth law wants
// an interpolation. A promise in a comment is how two numbers drift.
//
// IT IS 220,000 BECAUSE THAT IS WHAT REAL REPOSITORY WORK MEASURED, AND
// BECAUSE TWO INVARIANTS IN THIS PACKAGE BOUND IT FROM ABOVE. Both halves are
// measurements, and the next person to reach for this number needs the second
// half as much as the first.
//
// The measurement against real work: issue #898 — the frame law widened to see
// a package function taking the surface as a parameter — run headless on
// z-ai/glm-5.3 on 2026-09-11, run 04c2404b26072e41. Neither half of the
// original calibration above held. The median call carried 14.2k prompt tokens,
// not 11k, and the leaves that did the work ran 14 to 41 turns, not 8 to 16. At
// 150,000 every one of them was landed mid-edit:
//
// leaf turns spent grant then
// Issue 898 frame law fix 14 184,411 150,000
// finish-issue-898 41 190,524 164,462
// Answer remaining offenders 14 268,971 211,852
//
// (A grant above this constant is this constant plus the dependency term
// gatheringGrant adds — it is cmd/codeaf's, in subharness.go, which this package
// cannot link to; the overshoot past each is the landing reserve doing its job
// at landingTokenShare of the grant.) The run then re-planned around every
// landing — five rounds and seven nodes for one issue, 43 minutes, $2.11, and a
// delivery gate that refused at the end for want of time — while the WORK itself
// was correct and committed after the first two leaves. Every split is paid for
// twice: once in a fresh planning round, and once in the context the next leaf
// has to be told again. 220,000 covers the first two leaves outright with room
// over. The third, which had already been lifted to 211,852 by its dependency
// term and spent 268,971, is a job that wants splitting and is not an argument
// for a larger number.
//
// THE CEILING, AND WHY IT IS NOT 250,000. Two invariants bound this from above
// and neither may be moved to let a figure through. The band is 200,000 to about
// 241,000, measured by sweeping the constant and running them:
//
// - TestACacheDiscountedRunawayLandsOnItsMoney pins that a 98%-cached runaway
// runs out of money before the 1.4M raw tokens the audited melt-downs
// reached. At the discount the raw count is about 5.8× the grant, so 242,000
// is where it arrives and 250,000 pushed 1,442,112 — exactly what the grant
// forestalls. 220,000 reaches about 1.28M.
// - The same test closes by asserting that cost alone would have bought the
// leaf several times the work it did, and at 190,000 the simulated cost bound
// and the real run both land at 85 turns, so the comparison stops separating
// and the test says so. That is the floor, and it is why this is not 190,000
// even though the work only demanded that much.
//
// THE THIRD BOUND WAS NEVER REAL AND IS NOW CUT.
// TestObservationWindowIsSizedFromContextNotSpend appeared to pin the grant
// under 196,608, because it compared the context-derived window against
// DefaultLeafTokens/6. But the window has not read this constant since
// observationWindow became ctxbudget.ObservationBytes of the model's OWN
// context; the division was the test recomputing a historical figure — what the
// spend ceiling used to buy — from a number that had since moved. So it failed
// whenever the grant ROSE, which is backwards, since a larger grant does not
// shrink the memory a leaf is given. That constant is frozen at the 25,000 it
// actually was, and the assertion it supports is unweakened.
//
// AND THE ORIGINAL LESSON STILL STANDS, because three comments in this file
// rest on it. It was set at 400k once, which permitted around 37 turns, and
// every leaf ran to exactly that — the loop has no intrinsic reason to stop, so
// the binding limit is not a safety net, it is what makes the loop converge.
// Set it loosely and the model will spend all of it. maxTurnBackstop's own
// calibration and landingTokenShare's sizing both cite this paragraph; the
// figures they cite are the ORIGINAL calibration above, and the measurement in
// this comment is the reason both of them want re-reading when the grant moves.
//
// So this number is not a knob. It is load-bearing: it sits inside a band 41,000
// wide, with a measurement under it and two invariants over it, and moving it
// again means re-running the sweep in #920's replication and rewriting this
// comment, PERF.md and the manual page that quotes it. A lane that needs a
// bigger grant for one run passes `--token-budget` to `codeaf exec` or
// `codeaf run` rather than editing this.
const DefaultLeafTokens = 220_000
// rawTokenCeilingMultiple WAS the leaf's second bound and is now pressure on
// the wrap-up warning, for the reason set out at length against reuseCeiling in
// meter.go: Σ over turns of the prompt is turns × mean-context wearing a token
// name, and bounding it lands honest leaves early without reaching the runaway
// any sooner than the guard built to recognise one.
//
// The specific case below is answered instead by maxTurnBackstop and the
// no-progress guard. A 98%-cached loop that buys a million raw tokens with a
// 150k grant has, by construction, spent the grant — the provider charged for
// it, which is what the grant means — and what was actually wrong with the
// audited nodes was that they ran eighty turns without progressing, which is
// the thing noprogress.go measures and this never could.
//
// What follows is the reasoning it was introduced with, kept for the same
// reason meter.go keeps its own.
//
// Cost bounds spend; raw bounded convergence.
//
// spent() weights cache reads at cachedTokenWeightPercent, and that is the
// honest measure of what a leaf COSTS. It is not a measure of how far a leaf has
// got. The two came apart the moment the discount landed: at the 92-98% hit
// rates a warm run actually sees, a 150k discounted ceiling buys roughly a
// million raw prompt tokens, about eight times the allowance the number was
// calibrated for. The audited runs show exactly what a loop does with that —
// single nodes at 1.4M and 1.6M prompt tokens with no per-turn prompt above
// ~17k, i.e. nodes that melted by running very many cheap turns rather than by
// carrying a large context. The discount was right and the number beside it
// quietly stopped meaning anything, which is the failure DefaultLeafTokens'
// own comment predicts: set it loosely and the model will spend all of it.
//
// So the raw count comes back as a bound of its own, at three times the grant.
// That still hands a genuinely cache-friendly leaf the extra room the discount
// was meant to buy it, and it lands a 98%-cached runaway at ~450k raw tokens
// instead of ~1.2M. It is a multiple rather than an absolute because the grant
// is per-task, and because recalibrating DefaultLeafTokens downward instead
// would have to be redone per endpoint — the effective multiplier is the
// provider's hit rate, which is not ours to set.
const rawTokenCeilingMultiple = 3
// maxTurnBackstop is the ceiling on iterations. It is a ceiling rather than a
// default: whatever a caller asks for, this loop will not run more than this.
//
// It stood at 200, chosen when turns were understood to be the wrong meter and
// the token ceiling was believed to be the thing that bound. That reasoning held
// only while the token ceiling counted raw tokens; once cache reads were
// discounted, a warm runaway could afford far more turns than any real leaf
// needs, and 200 was the only thing left in its path — far too high to be in
// anyone's path. The measured distribution is the calibration: a well-sized leaf
// finishes in 8 to 16 turns (see DefaultLeafTokens, whose own measurement now
// puts a real repository leaf at 14 to 41 and is the reason this backstop wants
// re-reading when the grant moves), the profile's own spread
// reports identical briefs landing at 9, 16 and 25, and the longest honest leaf
// in the audit traces ran 25. Forty sits clear of every one of those.
//
// It stays a backstop. On a cache-discounted runaway the raw bound above is what
// fires first, and it fires into a landing rather than into a stop.
//
// Raised 40 → 400 (2026-08-12, product decision): forty was calibrated on the
// measured distribution of well-sized leaves, but it also stopped honest
// complex work — and stopping a paid-for leaf mid-thought is worse than
// letting the token budget, which is the meter that actually binds, do its
// job. Four hundred is not a target; it is the point past which a loop is
// running on rails no token bound can see, which is the only case a turn
// count was ever for.
const maxTurnBackstop = 400
// ModeFold is what Outcome.Mode says when the loop ran a task as a fold.
const ModeFold = "fold"
// FoldTurns is the whole run of a node whose material is already in its prompt.
//
// Two, and the second is a recovery rather than a continuation. A fold is a
// read-and-write over material it was handed, so one call is the honest shape of
// it: the measured alternative is a join that read three files it had already
// been given and spent thirteen turns and 181,354 tokens doing it, against about
// 7,700 for the single pass — 23×, and 160 of the 366 seconds of that job's
// critical path.
//
// One turn alone would be a trap, though, and the trap is not the model's fault.
// A first call can come back asking for a tool — to write the file it was told
// to produce, most often — and a first call can come back empty or truncated,
// which the loop already refuses to accept as a deliverable. Either is a turn
// spent without an answer, and a cap of one would settle the node on it. So the
// second call exists for exactly those two endings, and after it the loop stops:
// a fold still asking for tools on its third breath is not folding, and what
// happens to it is what happens to any leaf that reaches its cap, which the
// retry and judging paths above this loop already know how to handle.
const FoldTurns = 2
// NewLinear builds the loop. maxTokens is the limit that actually binds; maxTurns
// is the runaway backstop, clamped to maxTurnBackstop however high a caller asks.
//
// Counting turns was the wrong meter. Turns are not what a loop spends — one
// 25-turn leaf cost more than the other nine nodes of a run put together,
// because cost tracks accumulated context rather than iteration count. Bounding
// tokens lets a task take all the small steps it needs while still stopping one
// that is genuinely expensive. What the turn cap is for is the case tokens
// cannot see: a loop whose turns have become cheap enough that no token bound
// arrives in useful time.
func NewLinear(client Completer, workspace *Workspace, web *Web, maxTurns, maxTokens int, deadline time.Duration) *Linear {
if maxTurns <= 0 || maxTurns > maxTurnBackstop {
maxTurns = maxTurnBackstop
}
if maxTokens <= 0 {
maxTokens = DefaultLeafTokens
}
if deadline <= 0 {
// A caller that said nothing about time gets the generalist's own
// floor, asked for rather than written out again: this loop IS the
// generalist, so the shape registered under its name is the answer.
deadline = linearInfo.Deadline(0)
}
return &Linear{client: client, workspace: workspace, web: web,
maxTurns: maxTurns, maxTokens: maxTokens, deadline: deadline}
}
func (l *Linear) Subharness() string { return LinearSubharness }
// system is the leaf's standing contract: the harness's invariants, then the
// attribution law, then the narrowing for this assignment. It is
// what a specialised harness would have hand-written for this domain —
// generated instead, which is what keeps the loop generic.
//
// What it deliberately no longer carries is the planner's per-node contract.
//
// The system message is the first thing in every request, ahead of the tool
// block and the entire transcript, so it is the prefix every other leaf's
// prefix has to agree with to be served warm. Appending a contract written for
// one node made it the one part of the standing text that differed per node:
// four leaves of the same job, launched at once against the same model, shared
// nothing at all — each wrote the whole shared prefix cold and paid full price
// for the invariants all four of them were reading verbatim. The contract moved
// to the head of the brief, which is the first place two leaves were always
// going to diverge anyway, and the three inputs left here are the model, the
// model the attribution line names and whether this is a reflex — a handful of
// shapes across a whole run instead of one per node.
// The last block is assembled rather than written: each tool the leaf is
// actually holding contributes its own standing guidance, and a leaf holding
// two tools reads two lines. See Toolbox.Guidelines for why the guidance
// belongs to the tool. It goes LAST because it is the one part that varies with
// the toolbox, and everything ahead of it stays the byte-identical prefix four
// sibling leaves share.
func (l *Linear) system(task Task, guidelines []string) string {
system := systemPrompt + FillAttribution(attributionPrompt, l.assistedBy)
if task.Reflex {
system += reflexSystemPrompt
}
if len(guidelines) > 0 {
system += "\n\nYour tools, in the words of the tools themselves:\n\n" +
strings.Join(guidelines, "\n\n")
}
return system
}
// lengthStopBatchFailure is what every call in a length-stopped batch is
// answered with. It is stated as a fact plus the one move that fixes it: the
// arguments may be truncated, nothing ran, ask again — and, because the reply
// hit a ceiling, ask for less at a time. It goes back verbatim for every call in
// the batch rather than being tailored per tool, so a model reading four of them
// sees one thing that happened rather than four different problems.
//
// It is not routed through the observation memo — these bytes are identical
// across the batch by design, and collapsing them into "same as the result
// above" would turn the one sentence that has to be read into a pointer.
const lengthStopBatchFailure = "This tool call was NOT executed. The reply carrying it hit the output token " +
"limit, so its arguments may be truncated mid-write and running them could do something " +
"other than what you intended. Nothing was changed. Re-issue the call with complete " +
"arguments — and issue fewer or smaller calls this turn, since the last reply did not fit."
// Run executes one task.
func (l *Linear) Run(ctx context.Context, task Task) (returned *Outcome, runErr error) {
started := time.Now()
// One leaf is one routing lineage. The run key inherited from above already
// says "keep this run together"; this narrows it to "keep THIS leaf's turns
// together", which is the grain an automatic prefix cache can actually hit
// at — see provider.WithLeafCacheKey for what the narrowing gives up and why
// it is not close. It is applied before the deadline so that every call the
// leaf makes, including the tool-side model calls, rides the same key.
ctx = provider.WithLeafCacheKey(ctx, task.leafKey())
// The project's own account of whether it still works, read while the tree
// is still pristine. THIS BELT IS THE DEFAULT EVERY UNROUTED NODE GETS, and
// until it took a reading a whole graded run could finish with no
// verification event in its store at all — an absence that spells four
// different facts at once and diagnoses none of them (FAILSAFE.md, the sixth
// failure). The reading is an account of the run and never part of it: it
// cannot fail this leaf, and every way it can go wrong is written down
// rather than returned.
//
// It is taken BEFORE the tree is photographed on purpose. A test runner
// leaves its own droppings — a .pytest_cache, a target/, a coverage file —
// and a reading taken after WatchTree would file every one of them as
// something this leaf produced. Taken first, they belong to the world the
// leaf arrived in, which is what they are.
opening := PhotographBefore(ctx, l.workspace, l.history, l.deadline, task)
// The world's own account of what this leaf leaves behind starts here: the
// tree as it stands before a single turn has run. Everything the leaf writes
// with a shell command, a script or a build is invisible to the write tools
// and visible to this. See Workspace.WatchTree.
l.workspace.WatchTree(task.leafKey())
// WHICH NODE THESE CALLS BELONG TO, on the same context and for the same
// reason: every model call this leaf makes, turn after turn, is one node's
// work, and a log of a fanned-out run is unreadable without saying whose.
// The tag itself is derived from the routing class the scheduler already
// stamped (provider.WithCallTag), so "leaf" is spelled once, not twice.
//
// It is leafKey rather than NodeKey because NodeKey is empty on every
// headless plan leaf — that path carries its plan node's number in NodeID
// and sets no key at all — and naming the node from the field that is only
// sometimes filled left exactly those rows anonymous. leafKey is already
// the one place "who is this leaf, for naming purposes?" is answered, so
// the log now agrees with the artifact bucket and the flight recorder.
ctx = provider.WithCallTag(ctx, "leaf")
ctx = provider.WithCallNode(ctx, task.leafKey())
// Keep the context this leaf was HANDED before its own lease is put around
// it. An ordered landing must survive that lease expiring, while the caller's
// cancel and the errand's own wall must still reach it; deriving the landing
// from this context preserves exactly those two properties.
granted := ctx
ctx, cancel := context.WithTimeout(ctx, l.deadline)
defer cancel()
deadline, _ := ctx.Deadline()
wallStarted := time.Now()
wall := deadline.Sub(wallStarted)
landingReserve := deadlineLandingReserve(wall)
wallPaceAfter := time.Duration(float64(wall) * wallPaceAt)
turnCtx := ctx
var stopDeadlineLanding context.CancelFunc
defer func() {
if stopDeadlineLanding != nil {
stopDeadlineLanding()
}
}()
tools := newToolbox(l.workspace, task.leafKey(), l.web, l.history, l.media, l.contextTokens)
tools.share = task.Share
task.control.attach(tools)
defer func() {
if returned != nil && runErr == nil {
requests := tools.ServiceRequests(task.StoreNodeID)
if task.StoreNodeID == "" {
for index := range requests {
requests[index].Stop()
}
if len(requests) > 0 {
returned.Text = strings.TrimSpace(returned.Text) + "\n\nservice promotion is available only in resident chat; requested jobs stopped at leaf end"
}
} else {
returned.ServiceRequests = requests
}
} else {
tools.ForceClose()
}
terminated := tools.Close()
if returned != nil && terminated > 0 {
note := fmt.Sprintf("%d background jobs terminated at leaf end", terminated)
if strings.TrimSpace(returned.Text) == "" {
returned.Text = note
} else {
returned.Text = strings.TrimSpace(returned.Text) + "\n\n" + note
}
// A background job that was still running at landing may have
// written after the account was taken, so the tree is read once more
// before the list is rebuilt.
l.workspace.RecordChanges(task.leafKey())
returned.Artifacts = l.workspace.Artifacts(task.leafKey())
}
task.control.detach(tools, terminated)
}()
// What the assignment structurally already contains buys back its own
// schema before turn 1: a leaf handed an image must be able to look at it,
// and a leaf handed a PDF must be able to read it, without spending a turn
// asking. This is code judging structure — the presence of a file — and
// never code judging what the work is about.
if len(task.ImagePaths) > 0 {
tools.Arm("view_image")
}
if len(task.DocumentPaths) > 0 {
tools.Arm("read_document")
}
// Recomputed every turn rather than once, because a worker that asks for a
// capability has to be holding it on the turn after it asked. The cost is
// one slice build per turn against a model call.
definitions := func() []ai.ToolDefinition {
current := tools.Definitions()
if task.Reflex {
current = append(current, reflexPromotionDefinition())
}
// A reflex is one obvious micro-action and already has the verb for
// "this is bigger than it looked": promote. Offering it a second way to
// say so would be two answers to one question, and the first thing to
// go wrong with a second answer is that it disagrees.
if l.swarm && !task.Reflex {
current = append(current, requestSplitDefinition())
}
return current
}
// THE GENERALIST NOW LEAVES A RECORD. Every turn of this loop and every
// note the harness writes about itself already passes through this one
// object; wiring the store's transcript into it is what makes the default
// worker's work readable afterwards and resumable by whatever continues it.
// See tracer.sink.
trace := newRecordingTracer(ctx, l.workspace, task.leafKey())
defer trace.close()
system := l.system(task, tools.Guidelines())
if contract := strings.TrimSpace(task.Contract); contract != "" {
trace.note("contract:\n" + contract)
}
brief := l.brief(task)
if duration := wallDurationText(wall); duration != "" {
brief += "\n\nYou have " + duration + " of wall-clock time for this task."
}
userContent := text(brief)
workingModel := ""
if l.media != nil {
workingModel = l.media.WorkingModel
}
if len(task.ImagePaths) > 0 {
sees := l.media != nil && l.media.Catalog != nil &&
l.media.Catalog.Supports(workingModel, "input", "image")
if sees {
userContent = append(userContent, imageParts(task.ImagePaths)...)
} else if note := attachedImageNote(l.workspaceNames(task.ImagePaths), l.visionProxy()); note != "" {
// An attached image used to vanish here when the working model had
// no eyes: no content part, no fallback, and nobody told. The image
// is in the workspace now, so the leaf is told what it has and who
// can look — and when nothing can, that it must say so.
userContent = append(userContent, text(note)...)
}
}
messages := []ai.Message{
{Role: "system", Content: text(system)},
{Role: "user", Content: userContent},
}