refactor(core): ignore the recorded thinking level when resuming a session (#72)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
83e7b85eca
commit
274e19379f
5 files changed
+22
-45
No files matched your search
@@ -70,26 +70,6 @@ import type { ModelEntry } from "./state/index.js";
|
|||||||
*/
|
*/
|
||||||
const MAX_SUBAGENT_DEPTH = 1;
|
const MAX_SUBAGENT_DEPTH = 1;
|
||||||
|
|
||||||
/** The five valid thinking level names (legacy session_meta additionally recorded the literal "default" for "no level"). */
|
|
||||||
const THINKING_LEVEL_NAMES: readonly ThinkingLevelName[] = [
|
|
||||||
"none",
|
|
||||||
"low",
|
|
||||||
"medium",
|
|
||||||
"high",
|
|
||||||
"xhigh",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Narrows a legacy session_meta `thinking_level` back to a ThinkingLevelName; the literal
|
|
||||||
* "default" (no level recorded), a missing field (current Traces no longer record one), and
|
|
||||||
* anything unknown are not levels and yield undefined.
|
|
||||||
*/
|
|
||||||
function asThinkingLevelName(value: unknown): ThinkingLevelName | undefined {
|
|
||||||
return typeof value === "string" && (THINKING_LEVEL_NAMES as readonly string[]).includes(value)
|
|
||||||
? (value as ThinkingLevelName)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateAgentOptions {
|
export interface CreateAgentOptions {
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
@@ -394,15 +374,12 @@ export class Agent {
|
|||||||
// original history); the vault uses current values (it's injected into the
|
// original history); the vault uses current values (it's injected into the
|
||||||
// subprocess environment, not the history, so a resumed Session should get the
|
// subprocess environment, not the history, so a resumed Session should get the
|
||||||
// latest keys too).
|
// latest keys too).
|
||||||
// Back-compat: session_meta no longer records a thinking level (it became a per-turn
|
// The default thinking level comes from this Agent's current config only: session_meta
|
||||||
// run parameter), but OLD Traces still carry `thinking_level` in their meta JSON — when
|
// no longer records one (it became a per-turn run parameter), and a `thinking_level`
|
||||||
// present, keep honoring it as this Session's default level (a resumed legacy subagent
|
// still present in a legacy Trace's meta JSON is deliberately ignored — a resumed
|
||||||
// session keeps its inherited level instead of re-reading this Agent's config). The
|
// legacy subagent session falls back to this Agent's configured level instead of
|
||||||
// field is read loosely (it's gone from SessionMetaPayload); the legacy literal
|
// keeping the level it inherited at spawn time.
|
||||||
// "default" — and any current Trace without the field — falls back to the Agent config.
|
const thinkingLevel = this.state.systemConfig.model?.thinking_level;
|
||||||
const thinkingLevel =
|
|
||||||
asThinkingLevelName((meta as unknown as Record<string, unknown>).thinking_level) ??
|
|
||||||
this.state.systemConfig.model?.thinking_level;
|
|
||||||
|
|
||||||
const rt = await this.buildRuntime({
|
const rt = await this.buildRuntime({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
@@ -442,8 +419,7 @@ export class Agent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return new Session({
|
return new Session({
|
||||||
// Invariants only (the legacy thinking_level, when honored above, feeds the LLM default
|
// Invariants only — meta writes never contain a thinking level.
|
||||||
// but is not re-recorded — new meta writes never contain it).
|
|
||||||
meta: {
|
meta: {
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
provider: modelEntry.provider,
|
provider: modelEntry.provider,
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export interface ToolDefinition {
|
|||||||
/**
|
/**
|
||||||
* Session metadata. Holds **per-session invariants only** — values fixed for the Session's
|
* Session metadata. Holds **per-session invariants only** — values fixed for the Session's
|
||||||
* lifetime (model reference, assembled system prompt, tool schemas, paths, origin). Per-turn
|
* lifetime (model reference, assembled system prompt, tool schemas, paths, origin). Per-turn
|
||||||
* parameters (e.g. the thinking level, passed with each run) never belong here; legacy Traces
|
* parameters (e.g. the thinking level, passed with each run) never belong here; a
|
||||||
* may still carry a `thinking_level` field, which resume reads loosely for back-compat.
|
* `thinking_level` field still present in a legacy Trace's meta is ignored on resume.
|
||||||
*/
|
*/
|
||||||
export interface SessionMetaPayload {
|
export interface SessionMetaPayload {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
|
|||||||
@@ -175,11 +175,12 @@ describe("agent.resumeSession", () => {
|
|||||||
).toEqual(["text", "text", "abort"]);
|
).toEqual(["text", "text", "abort"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors a legacy trace's recorded thinking_level; new meta never re-records it", async () => {
|
it("ignores a legacy trace's recorded thinking_level; the Agent config always wins", async () => {
|
||||||
// session_meta no longer carries a thinking level (it became a per-turn run parameter),
|
// session_meta no longer carries a thinking level (it became a per-turn run parameter);
|
||||||
// but OLD traces still have it in their meta JSON: resume must keep honoring it as the
|
// a `thinking_level` still present in an OLD trace's meta JSON is deliberately ignored —
|
||||||
// session's default level (a legacy subagent session keeps its inherited level instead of
|
// resume always reads this Agent's current config, so a resumed legacy subagent session
|
||||||
// re-reading this Agent's config). The seeded Agent config here pins "medium".
|
// falls back to the config level instead of keeping the level it inherited at spawn
|
||||||
|
// time. The seeded Agent config here pins "medium".
|
||||||
const agent = await createAgent({});
|
const agent = await createAgent({});
|
||||||
expect(agent.state.systemConfig.model?.thinking_level).toBe("medium");
|
expect(agent.state.systemConfig.model?.thinking_level).toBe("medium");
|
||||||
const levelOf = (session: unknown): unknown =>
|
const levelOf = (session: unknown): unknown =>
|
||||||
@@ -190,14 +191,14 @@ describe("agent.resumeSession", () => {
|
|||||||
// A legacy trace: inject the retired field loosely into the on-disk meta JSON.
|
// A legacy trace: inject the retired field loosely into the on-disk meta JSON.
|
||||||
(recorded.payload as unknown as Record<string, unknown>).thinking_level = "xhigh";
|
(recorded.payload as unknown as Record<string, unknown>).thinking_level = "xhigh";
|
||||||
await writeTraceFile(tmpRoot, SID, [recorded, userText("hello")]);
|
await writeTraceFile(tmpRoot, SID, [recorded, userText("hello")]);
|
||||||
const inherited = await agent.resumeSession({ sessionId: SID });
|
const ignored = await agent.resumeSession({ sessionId: SID });
|
||||||
expect(levelOf(inherited)).toBe("xhigh");
|
expect(levelOf(ignored)).toBe("medium");
|
||||||
// The rebuilt meta holds invariants only: the legacy field is honored but never re-recorded.
|
// The rebuilt meta holds invariants only: the legacy field is never re-recorded either.
|
||||||
expect(
|
expect(
|
||||||
"thinking_level" in (inherited.metaMessage.payload as unknown as Record<string, unknown>),
|
"thinking_level" in (ignored.metaMessage.payload as unknown as Record<string, unknown>),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
// A current trace (no field) — and the legacy literal "default" — fall back to the Agent config.
|
// A current trace (no field) — and the legacy literal "default" — read the Agent config too.
|
||||||
const SID2 = "session-2026-07-06-11-00-00-abcdef02";
|
const SID2 = "session-2026-07-06-11-00-00-abcdef02";
|
||||||
await writeTraceFile(tmpRoot, SID2, [metaFor(SID2, workspace), userText("hi")]);
|
await writeTraceFile(tmpRoot, SID2, [metaFor(SID2, workspace), userText("hi")]);
|
||||||
const fallback = await agent.resumeSession({ sessionId: SID2 });
|
const fallback = await agent.resumeSession({ sessionId: SID2 });
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ interface ToolDefinition {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
session_meta holds **per-session invariants only** — the model, system prompt and Workspace are immutable for the Session's lifetime; on resume, the engine takes this Trace line as the runtime config. See [Sessions & Traces](/sessions-and-traces). The thinking level is a per-turn parameter (sent with each Task) and is not recorded here; legacy Traces may still carry a `thinking_level` field in their meta, which resume keeps honoring for back-compat.
|
session_meta holds **per-session invariants only** — the model, system prompt and Workspace are immutable for the Session's lifetime; on resume, the engine takes this Trace line as the runtime config. See [Sessions & Traces](/sessions-and-traces). The thinking level is a per-turn parameter (sent with each Task) and is not recorded here; a `thinking_level` field still present in a legacy Trace's meta is ignored on resume — the resumed Session reads the Agent's current config instead.
|
||||||
|
|
||||||
## model_msg: complete payloads
|
## model_msg: complete payloads
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ interface ToolDefinition {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
session_meta 只承载**会话级不变量**——模型、系统提示词、Workspace 在 Session 生命周期内不可变;恢复 Session 时引擎直接以 Trace 中的这条消息为运行时配置,见 [Session 与 Trace](/sessions-and-traces)。思考等级是逐轮参数(随每次 Task 下发),不记录在此;旧版 Trace 的 meta 里可能仍带 `thinking_level` 字段,恢复时按兼容逻辑继续生效。
|
session_meta 只承载**会话级不变量**——模型、系统提示词、Workspace 在 Session 生命周期内不可变;恢复 Session 时引擎直接以 Trace 中的这条消息为运行时配置,见 [Session 与 Trace](/sessions-and-traces)。思考等级是逐轮参数(随每次 Task 下发),不记录在此;旧版 Trace 的 meta 里可能仍带 `thinking_level` 字段,恢复时会被忽略——恢复后的 Session 直接读取 Agent 当前配置。
|
||||||
|
|
||||||
## model_msg:完整消息
|
## model_msg:完整消息
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user