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:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-26 23:01:18 +08:00
1 parent abd5b13d52
commit 7584171482
81 files changed
+3139 -930

No files matched your search

+33 -2
View File
@@ -340,7 +340,7 @@ describe("trace-service", () => {
// (the frontend uses this to list it on this turn's card), but the duration
// only looks at the LLM request — the start point is the first request_begin,
// and the user text's timestamp doesn't participate (the compaction summary
// `<context_summary>` is created during compaction but only persisted on the
// `[context_summary]` is created during compaction but only persisted on the
// next run; using it as the start point would stretch the first turn out for
// no reason). Also, if the turn list were built only from turns with "a model
// segment or a tool span", a turn that fails outright with no output at all
@@ -584,6 +584,37 @@ describe("trace-service", () => {
expect(a.requests.map((r) => r.taskIndex)).toEqual([0, 0, 1]);
});
it("Task grouping: [user_steering] user texts never start a new Task (steering continuation stays in the same Task)", async () => {
const T = (sec: string) => `2026-07-05T10:03:${sec}Z`;
await writeTraceFile(root, P, A, "2026-07-05", S, 12, [
sessionMeta(metaPayload()),
// Task 0, round 1: calls a tool; a steering message rides alongside the tool output.
at(T("00.000"), userText("q1")),
at(T("01.000"), requestBegin()),
at(T("02.000"), toolCall({ name: "read_file", arguments: "{}", toolCallId: "t1" })),
at(T("02.500"), requestEnd("completed")),
at(T("03.000"), toolCallOutput({ output: "o", toolCallId: "t1" })),
at(T("03.200"), userText("[user_steering]\nalso check the tests\n[/user_steering]")),
at(T("03.500"), requestBegin()),
at(T("04.000"), assistantText("answer 1")),
at(T("04.500"), requestEnd("completed")),
// Loop-end steering: the answer round produced no tool call — a plain user text here
// would start a new Task, but the steering continuation stays in Task 0.
at(T("05.000"), userText("[user_steering]\none more thing\n[/user_steering]")),
at(T("05.500"), requestBegin()),
at(T("06.000"), assistantText("answer 2")),
at(T("06.500"), requestEnd("completed")),
// A real new user turn afterwards starts Task 1 as usual.
at(T("20.000"), userText("q2")),
at(T("21.000"), requestBegin()),
at(T("22.000"), assistantText("answer 3")),
at(T("22.500"), requestEnd("completed")),
]);
const a = await service.analyze(P, A, S, 12);
expect(a.requests.map((r) => r.taskIndex)).toEqual([0, 0, 0, 1]);
expect(a.modelSegments.map((s) => s.taskIndex)).toEqual([0, 0, 0, 1]);
});
// Compaction is its own turn: the previous turn called a tool and would
// otherwise "continue", but compaction_begin breaks that continuation, so the
// compaction request lands on a new taskIndex. A successful compaction splits
@@ -604,7 +635,7 @@ describe("trace-service", () => {
compactionBegin({ reason: "context", mode: "summarize", context: 1, turns: 1 }),
),
at(T("04.500"), requestBegin()),
at(T("05.000"), assistantText("<summary>…</summary>")),
at(T("05.000"), assistantText("[summary]…[/summary]")),
at(T("05.500"), requestEnd("completed")),
at(T("06.000"), compactionEnd({ reason: "context", mode: "summarize", status: "completed" })),
]);