feat(core,server,web,cli): mid-run steering messages and square-bracket markers (#63)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abd5b13d52
commit
7584171482
81 files changed
+3139
-930
No files matched your search
@@ -6,7 +6,7 @@
|
||||
* request emits token_usage, both mid-task and at the wrap-up round (reaching the threshold at
|
||||
* task end triggers compaction immediately, without waiting for the next task).
|
||||
* - summarize: appends a compaction prompt to the old LLM (merging in all of this round's tool
|
||||
* results first if mid-task); the summary is wrapped as a `<context_summary>` user text and fed
|
||||
* results first if mid-task); the summary is wrapped as a `[context_summary]` user text and fed
|
||||
* as the first input to the new LLM instance; on failure the original context is kept, never downgraded to discard.
|
||||
* - discard: deferred until task end if mid-task; sends no compaction request, just swaps in a new LLM instance directly.
|
||||
* - Process visibility: the compaction request's streamed output is never surfaced to the human,
|
||||
@@ -162,7 +162,7 @@ describe("context compaction", () => {
|
||||
{ messages: [assistantText("answer one"), usage(150, 150)] },
|
||||
// Compaction request: summary + usage (counted into the session cumulative total).
|
||||
{
|
||||
messages: [assistantText("<summary>the distilled summary</summary>"), usage(160, 310)],
|
||||
messages: [assistantText("[summary]the distilled summary[/summary]"), usage(160, 310)],
|
||||
},
|
||||
],
|
||||
"llm1",
|
||||
@@ -223,7 +223,7 @@ describe("context compaction", () => {
|
||||
expect(llm1.calls).toHaveLength(2);
|
||||
expect(llm2.calls).toHaveLength(1);
|
||||
const firstInput = llm2.calls[0]!.map(textOf);
|
||||
expect(firstInput[0]).toBe("<context_summary>\nthe distilled summary\n</context_summary>");
|
||||
expect(firstInput[0]).toBe("[context_summary]\nthe distilled summary\n[/context_summary]");
|
||||
expect(firstInput[1]).toBe("task two");
|
||||
|
||||
// Trace splits into files: the old file contains the compaction dialogue and paired events; the new file starts with session_meta.
|
||||
@@ -238,7 +238,7 @@ describe("context compaction", () => {
|
||||
expect(newTrace[0]!.type).toBe("session_meta");
|
||||
expect(
|
||||
newTrace.some((m) =>
|
||||
((m.payload as { text?: string }).text ?? "").startsWith("<context_summary>"),
|
||||
((m.payload as { text?: string }).text ?? "").startsWith("[context_summary]"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -251,7 +251,7 @@ describe("context compaction", () => {
|
||||
messages: [toolCall({ name: "t", arguments: "{}", toolCallId: "c1" }), usage(150, 150)],
|
||||
},
|
||||
// Compaction request (should include c1's tool_call_output plus the compaction prompt).
|
||||
{ messages: [assistantText("<summary>continue: finish step 2</summary>")] },
|
||||
{ messages: [assistantText("[summary]continue: finish step 2[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -279,7 +279,7 @@ describe("context compaction", () => {
|
||||
// The summary itself is the new instance's first input (no hardcoded continuation instruction appended); the task is finished by the new context.
|
||||
expect(llm2.calls).toHaveLength(1);
|
||||
expect(llm2.calls[0]!.map(textOf)).toEqual([
|
||||
"<context_summary>\ncontinue: finish step 2\n</context_summary>",
|
||||
"[context_summary]\ncontinue: finish step 2\n[/context_summary]",
|
||||
]);
|
||||
const finalTexts = out
|
||||
.filter((m) => (m.payload as { type?: string }).type === "text")
|
||||
@@ -302,7 +302,7 @@ describe("context compaction", () => {
|
||||
// Original context is kept: the task continues, tool outputs feed back into the old instance as usual (context usage keeps growing).
|
||||
{ messages: [assistantText("finished on old context"), usage(190, 340)] },
|
||||
// Second trigger (context still over the limit) -> retries compaction at the boundary, this time succeeding.
|
||||
{ messages: [assistantText("<summary>second try</summary>")] },
|
||||
{ messages: [assistantText("[summary]second try[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -350,7 +350,7 @@ describe("context compaction", () => {
|
||||
const llm1 = new ScriptedLLM(
|
||||
[
|
||||
{ messages: [assistantText("answer"), usage(150, 150)] },
|
||||
{ messages: [assistantText("<summary>s</summary>")] },
|
||||
{ messages: [assistantText("[summary]s[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -380,7 +380,7 @@ describe("context compaction", () => {
|
||||
const newTrace = await readTrace(trace.currentPath());
|
||||
expect(newTrace[0]!.type).toBe("session_meta");
|
||||
expect(
|
||||
((newTrace[1]!.payload as { text?: string }).text ?? "").startsWith("<context_summary>"),
|
||||
((newTrace[1]!.payload as { text?: string }).text ?? "").startsWith("[context_summary]"),
|
||||
).toBe(true);
|
||||
expect((newTrace[2]!.payload as { text?: string }).text).toBe("task 2");
|
||||
});
|
||||
@@ -421,7 +421,7 @@ describe("context compaction", () => {
|
||||
},
|
||||
{ messages: [assistantText("t1 done"), usage(10, 20)] },
|
||||
// Compaction request (sent out immediately when task 1 ends).
|
||||
{ messages: [assistantText("<summary>s</summary>")] },
|
||||
{ messages: [assistantText("[summary]s[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -445,7 +445,7 @@ describe("context compaction", () => {
|
||||
expect(compactionEvents(out2)).toHaveLength(0);
|
||||
expect(llm2.calls).toHaveLength(1);
|
||||
expect(llm2.calls[0]!.map(textOf)).toEqual([
|
||||
"<context_summary>\ns\n</context_summary>",
|
||||
"[context_summary]\ns\n[/context_summary]",
|
||||
"task 2",
|
||||
]);
|
||||
});
|
||||
@@ -455,7 +455,7 @@ describe("context compaction", () => {
|
||||
[
|
||||
// Wrap-up round context usage 100 == threshold 100 -> triggers.
|
||||
{ messages: [assistantText("answer"), usage(100, 100)] },
|
||||
{ messages: [assistantText("<summary>eq</summary>")] },
|
||||
{ messages: [assistantText("[summary]eq[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -511,7 +511,7 @@ describe("context compaction", () => {
|
||||
]);
|
||||
expect(llm1.calls).toHaveLength(2);
|
||||
|
||||
// The next round's input is used as-is as the new instance's first input (no <context_summary>).
|
||||
// The next round's input is used as-is as the new instance's first input (no [context_summary]).
|
||||
await collect(engine.run([userText("next task")], { approve: allowAll }));
|
||||
expect(llm2.calls[0]!.map(textOf)).toEqual(["next task"]);
|
||||
|
||||
@@ -521,7 +521,7 @@ describe("context compaction", () => {
|
||||
expect(newTrace[0]!.type).toBe("session_meta");
|
||||
});
|
||||
|
||||
it("lenient extraction: output without <summary> tags is used verbatim", async () => {
|
||||
it("lenient extraction: output without [summary] tags is used verbatim", async () => {
|
||||
const llm1 = new ScriptedLLM(
|
||||
[
|
||||
{ messages: [assistantText("answer"), usage(150, 150)] },
|
||||
@@ -540,7 +540,7 @@ describe("context compaction", () => {
|
||||
await collect(engine.run([userText("go")], { approve: allowAll }));
|
||||
await collect(engine.run([userText("next")], { approve: allowAll }));
|
||||
expect(textOf(llm2.calls[0]![0]!)).toBe(
|
||||
"<context_summary>\nplain summary text without tags\n</context_summary>",
|
||||
"[context_summary]\nplain summary text without tags\n[/context_summary]",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -550,7 +550,7 @@ describe("context compaction", () => {
|
||||
// One ordinary task (well under the limit).
|
||||
{ messages: [assistantText("small"), usage(10, 10)] },
|
||||
// Manual compaction request.
|
||||
{ messages: [assistantText("<summary>manual s</summary>")] },
|
||||
{ messages: [assistantText("[summary]manual s[/summary]")] },
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
@@ -570,7 +570,7 @@ describe("context compaction", () => {
|
||||
|
||||
await collect(engine.run([userText("next")], { approve: allowAll }));
|
||||
expect(llm2.calls[0]!.map(textOf)).toEqual([
|
||||
"<context_summary>\nmanual s\n</context_summary>",
|
||||
"[context_summary]\nmanual s\n[/context_summary]",
|
||||
"next",
|
||||
]);
|
||||
});
|
||||
@@ -607,7 +607,7 @@ describe("context compaction", () => {
|
||||
[
|
||||
// Usage is kept under maxContextLength (100 per settings()) so automatic compaction doesn't jump in first and reset sessionTurns.
|
||||
{ messages: [assistantText("hi"), usage(10, 10)] },
|
||||
{ messages: [assistantText("<summary>s</summary>")] }, // manual compaction request
|
||||
{ messages: [assistantText("[summary]s[/summary]")] }, // manual compaction request
|
||||
],
|
||||
"llm1",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user