Handoff-style /model switch command and per-turn thinking level (#64)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-26 16:31:51 +08:00
committed by GitHub
parent 816a823b75
commit 1e23452f48
58 changed files with 1064 additions and 336 deletions
+16 -11
View File
@@ -70,7 +70,7 @@ import type { ModelEntry } from "./state/index.js";
*/
const MAX_SUBAGENT_DEPTH = 1;
/** The five valid thinking level names (session_meta additionally records the literal "default" for "no level"). */
/** 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",
@@ -80,9 +80,9 @@ const THINKING_LEVEL_NAMES: readonly ThinkingLevelName[] = [
];
/**
* Narrows a recorded session_meta `thinking_level` back to a ThinkingLevelName; the literal
* "default" (no level recorded), a missing field (legacy Trace), and anything unknown are
* not levels and yield undefined.
* 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)
@@ -291,6 +291,8 @@ export class Agent {
});
return new Session({
// session_meta holds per-session invariants only: the thinking level is a per-turn
// run parameter (RunOptions.thinkingLevel) and is deliberately not recorded here.
meta: {
session_id: sessionId,
provider: modelEntry.provider,
@@ -298,7 +300,6 @@ export class Agent {
model_context_window: modelEntry.context_window ?? "unknown",
system_prompt: systemPrompt,
tools: rt.tools,
thinking_level: thinkingLevel ?? "default",
agent_state: this.state.stateDir,
workspace: workspaceDir,
...(opts.source !== undefined ? { source: opts.source } : {}),
@@ -393,12 +394,15 @@ export class Agent {
// original history); the vault uses current values (it's injected into the
// subprocess environment, not the history, so a resumed Session should get the
// latest keys too).
// The recorded thinking level is restored with the rest of session_meta (like model,
// Workspace, and system prompt): a resumed subagent session keeps its inherited level
// instead of re-reading this Agent's config. The literal "default" (no level recorded)
// — and a legacy Trace without the field — falls back to the Agent's current config.
// Back-compat: session_meta no longer records a thinking level (it became a per-turn
// run parameter), but OLD Traces still carry `thinking_level` in their meta JSON — when
// present, keep honoring it as this Session's default level (a resumed legacy subagent
// session keeps its inherited level instead of re-reading this Agent's config). The
// field is read loosely (it's gone from SessionMetaPayload); the legacy literal
// "default" — and any current Trace without the field — falls back to the Agent config.
const thinkingLevel =
asThinkingLevelName(meta.thinking_level) ?? this.state.systemConfig.model?.thinking_level;
asThinkingLevelName((meta as unknown as Record<string, unknown>).thinking_level) ??
this.state.systemConfig.model?.thinking_level;
const rt = await this.buildRuntime({
workspaceDir,
@@ -438,6 +442,8 @@ export class Agent {
});
return new Session({
// Invariants only (the legacy thinking_level, when honored above, feeds the LLM default
// but is not re-recorded — new meta writes never contain it).
meta: {
session_id: sessionId,
provider: modelEntry.provider,
@@ -445,7 +451,6 @@ export class Agent {
model_context_window: modelEntry.context_window ?? "unknown",
system_prompt: meta.system_prompt,
tools: rt.tools,
thinking_level: thinkingLevel ?? "default",
agent_state: this.state.stateDir,
workspace: workspaceDir,
// The origin carries over from the original session_meta (a resumed scheduled/subagent