Every package-level name that only the compactor uses starts with
compactor: its limit of tries, the ruler, the task text, the note on a
long line, and the helpers that cut a line and pick the shortest one.
Names such as ruler and shortest read as helpers of the whole program,
but only the compactor uses them.
The declarations of the compactor files follow the order of the go-code
skill. The behavior does not change.
Issue:
947bddb
-// compactionTries is the most calls for one node.
-const compactionTries = 5
+// compactorTries is the most calls for one node.
+const compactorTries = 5
-// ruler shows the model the size limit of a line. Models cannot count bytes,
-// and a sample line as the ruler gets its content copied.
-var ruler = strings.Repeat("-", nodeBytes)
+// compactorRuler shows the model the size limit of a line. Models cannot count
+// bytes, and a sample line as the ruler gets its content copied.
+var compactorRuler = strings.Repeat("-", nodeBytes)
+
+// newCompactor returns the compactor of an agent, which sends its calls through
+// client with model. instructions gives the system prompt of the agent at each
+// build, so a build sends the same prompt as the turns of that time.
+func newCompactor(client *ModelClient, instructions func() (string, error), model ModelConfig) *Compactor {
+ return &Compactor{
+ client: client,
+ model: model,
+ instructions: instructions,
+ tools: promptTools(),
+ }
+}
// Compactor builds the nodes of one agent with a model. A compaction is a call
// like a turn, with the same system prompt and tools, so it reads them from the
// Compactor builds the nodes of one agent with a model. A compaction is a call
// like a turn, with the same system prompt and tools, so it reads them from the
// When the line is over nodeBytes, the next try sends the same view and task,
// plus a note with the size of the newest line and its cut at the limit. A try
// never sends the model's earlier reply or reasoning back, so every try has the
// When the line is over nodeBytes, the next try sends the same view and task,
// plus a note with the size of the newest line and its cut at the limit. A try
// never sends the model's earlier reply or reasoning back, so every try has the
-// same cached prefix and about the same size. After at most compactionTries
+// same cached prefix and about the same size. After at most compactorTries
// tries, the shortest line is the node. A few bytes over the limit is fine,
// because the view measures the real sizes. A call that fails before any line
// fails the build, and the core tries it again at the next message.
// tries, the shortest line is the node. A few bytes over the limit is fine,
// because the view measures the real sizes. A call that fails before any line
// fails the build, and the core tries it again at the next message.
-// newCompactor returns the compactor of an agent, which sends its calls through
-// client with model. instructions gives the system prompt of the agent at each
-// build, so a build sends the same prompt as the turns of that time.
-func newCompactor(client *ModelClient, instructions func() (string, error), model ModelConfig) *Compactor {
- return &Compactor{
- client: client,
- model: model,
- instructions: instructions,
- tools: promptTools(),
- }
-}
-
// Summarize returns the line of the node of task.
func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) {
view := task.Context + "\n"
// Summarize returns the line of the node of task.
func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) {
view := task.Context + "\n"
- text, err := compactionTask(task)
+ text, err := compactorTask(task)
if err != nil {
return "", err
}
if err != nil {
return "", err
}
var lines []string
note := ""
var lines []string
note := ""
- for len(lines) < compactionTries {
+ for len(lines) < compactorTries {
parts := []string{view, text}
if note != "" {
parts = append(parts, note)
parts := []string{view, text}
if note != "" {
parts = append(parts, note)
+ note = compactorTooLong(line)
}
if len(lines) == 0 {
return "", fmt.Errorf("compact node (%d, %d): %w", task.Level, task.Index, err)
}
}
if len(lines) == 0 {
return "", fmt.Errorf("compact node (%d, %d): %w", task.Level, task.Index, err)
}
- return shortest(lines), nil
+ return compactorShortest(lines), nil
-// compactionTask returns the task of a build, with its input.
-func compactionTask(task Task) (string, error) {
+// compactorTask returns the task of a build, with its input.
+func compactorTask(task Task) (string, error) {
if task.Level == 0 && len(task.Input) == 1 {
return fmt.Sprintf(`Compaction: compress message %d into one line of at most %d bytes
(about 70 words), the length of this ruler:
%s
<input>
%s
if task.Level == 0 && len(task.Input) == 1 {
return fmt.Sprintf(`Compaction: compress message %d into one line of at most %d bytes
(about 70 words), the length of this ruler:
%s
<input>
%s
-</input>`, task.Index, nodeBytes, ruler, task.Input[0]), nil
+</input>`, task.Index, nodeBytes, compactorRuler, task.Input[0]), nil
}
if task.Level > 0 && len(task.Input) == 2 {
}
if task.Level > 0 && len(task.Input) == 2 {
-</input>`, first, half, first+half, half, nodeBytes, ruler, first, nodeEnd(task.Level, task.Index)-1, task.Input[0], task.Input[1]), nil
+</input>`, first, half, first+half, half, nodeBytes, compactorRuler, first, nodeEnd(task.Level, task.Index)-1, task.Input[0], task.Input[1]), nil
}
return "", fmt.Errorf("node (%d, %d) has %d inputs", task.Level, task.Index, len(task.Input))
}
}
return "", fmt.Errorf("node (%d, %d) has %d inputs", task.Level, task.Index, len(task.Input))
}
-// tooLong returns the note for a line over nodeBytes: its size and its first
-// nodeBytes bytes, which show the model where the limit cuts it.
-func tooLong(line string) string {
+// compactorTooLong returns the note for a line over nodeBytes: its size and its
+// first nodeBytes bytes, which show the model where the limit cuts it.
+func compactorTooLong(line string) string {
return fmt.Sprintf(`Too long: your last line for this <input> was %d bytes,
over the %d-byte limit. Write the whole line again, cutting just
enough of the least valuable items to fit before this cut:
return fmt.Sprintf(`Too long: your last line for this <input> was %d bytes,
over the %d-byte limit. Write the whole line again, cutting just
enough of the least valuable items to fit before this cut:
-%s| <- LIMIT`, len(line), nodeBytes, cutLine(line))
+%s| <- LIMIT`, len(line), nodeBytes, compactorCutLine(line))
+}
+
+// compactorShortest returns the first of the compactorShortest lines.
+func compactorShortest(lines []string) string {
+ best := lines[0]
+ for _, line := range lines[1:] {
+ if len(line) < len(best) {
+ best = line
+ }
+ }
+
+ return best
-// cutLine returns the first nodeBytes bytes of line, without a part of a
-// character at the end.
-func cutLine(line string) string {
+// compactorCutLine returns the first nodeBytes bytes of line, without a part of
+// a character at the end.
+func compactorCutLine(line string) string {
if len(line) <= nodeBytes {
return line
}
if len(line) <= nodeBytes {
return line
}
-
-// shortest returns the first of the shortest lines.
-func shortest(lines []string) string {
- best := lines[0]
- for _, line := range lines[1:] {
- if len(line) < len(best) {
- best = line
- }
- }
-
- return best
-}
require.NoError(t, err)
require.JSONEq(t, string(tools), string(sent))
require.NoError(t, err)
require.JSONEq(t, string(tools), string(sent))
- parts := inputParts(t, server.Call(0))
+ parts := compactorInputParts(t, server.Call(0))
require.Equal(t, []string{
"<chat>\n0+4|user: earlier\n</chat>\n",
"Compaction: compress message 7 into one line of at most 512 bytes\n" +
require.Equal(t, []string{
"<chat>\n0+4|user: earlier\n</chat>\n",
"Compaction: compress message 7 into one line of at most 512 bytes\n" +
}
func TestCompactorWritesTheMergeTask(t *testing.T) {
}
func TestCompactorWritesTheMergeTask(t *testing.T) {
- text, err := compactionTask(Task{
+ text, err := compactorTask(Task{
Level: 2,
Index: 3,
Input: []string{"left line", "right line"},
Level: 2,
Index: 3,
Input: []string{"left line", "right line"},
}
compactor := newTestCompactor(t, server)
}
compactor := newTestCompactor(t, server)
- line, err := compactor.Summarize(t.Context(), leafTask())
+ line, err := compactor.Summarize(t.Context(), compactorLeafTask())
require.NoError(t, err)
require.Equal(t, long[2], line)
require.Equal(t, 3, server.Calls())
require.NoError(t, err)
require.Equal(t, long[2], line)
require.Equal(t, 3, server.Calls())
require.Equal(t, len(long[n]), record.TextBytes)
}
require.Equal(t, len(long[n]), record.TextBytes)
}
- first := inputParts(t, server.Call(0))
+ first := compactorInputParts(t, server.Call(0))
require.Len(t, first, 2)
for n, want := range []string{long[0], long[1]} {
call := server.Call(n + 1)
require.Len(t, first, 2)
for n, want := range []string{long[0], long[1]} {
call := server.Call(n + 1)
- parts := inputParts(t, call)
+ parts := compactorInputParts(t, call)
require.Equal(t, first, parts[:2])
require.Equal(t, first, parts[:2])
- require.Equal(t, []string{tooLong(want)}, parts[2:])
+ require.Equal(t, []string{compactorTooLong(want)}, parts[2:])
require.Contains(t, parts[2], "was "+strconv.Itoa(len(want))+" bytes,")
require.Contains(t, parts[2], "\n"+want[:512]+"| <- LIMIT")
require.Contains(t, parts[2], "was "+strconv.Itoa(len(want))+" bytes,")
require.Contains(t, parts[2], "\n"+want[:512]+"| <- LIMIT")
modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(strings.Repeat("z", sizes[call.number]))}, 100, 0, 5))
}
modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(strings.Repeat("z", sizes[call.number]))}, 100, 0, 5))
}
- line, err := newTestCompactor(t, server).Summarize(t.Context(), leafTask())
+ line, err := newTestCompactor(t, server).Summarize(t.Context(), compactorLeafTask())
- require.Equal(t, compactionTries, server.Calls())
+ require.Equal(t, compactorTries, server.Calls())
require.Len(t, line, 520)
}
require.Len(t, line, 520)
}
server := newModelFake(t)
server.reply = tc.reply
server := newModelFake(t)
server.reply = tc.reply
- _, err := newTestCompactor(t, server).Summarize(t.Context(), leafTask())
+ _, err := newTestCompactor(t, server).Summarize(t.Context(), compactorLeafTask())
require.ErrorContains(t, err, tc.want)
require.Equal(t, 1, server.Calls())
})
require.ErrorContains(t, err, tc.want)
require.Equal(t, 1, server.Calls())
})
func TestCutLineKeepsWholeCharacters(t *testing.T) {
line := strings.Repeat("a", 511) + "é" + "tail"
func TestCutLineKeepsWholeCharacters(t *testing.T) {
line := strings.Repeat("a", 511) + "é" + "tail"
+ cut := compactorCutLine(line)
require.Equal(t, strings.Repeat("a", 511), cut)
require.True(t, utf8.ValidString(cut))
require.Equal(t, strings.Repeat("a", 511), cut)
require.True(t, utf8.ValidString(cut))
- require.Equal(t, "short", cutLine("short"))
+ require.Equal(t, "short", compactorCutLine("short"))
}
func TestCompactorBuildsNodesInTheCore(t *testing.T) {
}
func TestCompactorBuildsNodesInTheCore(t *testing.T) {
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
- return newCompactor(client, adaPrompt, ModelConfig{
+ return newCompactor(client, compactorAdaPrompt, ModelConfig{
Model: "gpt-test",
Effort: "high",
})
}
Model: "gpt-test",
Effort: "high",
})
}
-// adaPrompt returns the prompt of the agent Ada, without instructions.
-func adaPrompt() (string, error) {
- return prompt("Ada", ""), nil
-}
-
-func leafTask() Task {
- return Task{
- Level: 0,
- Index: 0,
- Context: "<chat>\n</chat>",
- Input: []string{"user: " + strings.Repeat("y", 600)},
- }
-}
-
-// inputParts returns the texts of the one user message of call.
-func inputParts(t *testing.T, call ModelFakeCall) []string {
+// compactorInputParts returns the texts of the one user message of call.
+func compactorInputParts(t *testing.T, call ModelFakeCall) []string {
t.Helper()
var body struct {
t.Helper()
var body struct {
+
+func compactorLeafTask() Task {
+ return Task{
+ Level: 0,
+ Index: 0,
+ Context: "<chat>\n</chat>",
+ Input: []string{"user: " + strings.Repeat("y", 600)},
+ }
+}
+
+// compactorAdaPrompt returns the prompt of the agent Ada, without instructions.
+func compactorAdaPrompt() (string, error) {
+ return prompt("Ada", ""), nil
+}