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
+18 -4
View File
@@ -11,7 +11,7 @@ import path from "node:path";
import { Hono } from "hono";
import type { Context } from "hono";
import { imageUrlMessage, scratchpadDir, userText } from "@prismshadow/penguin-core";
import type { OmniMessage } from "@prismshadow/penguin-core";
import type { OmniMessage, ThinkingLevelName } from "@prismshadow/penguin-core";
import type {
ApprovalMode,
FilesStatResponse,
@@ -58,6 +58,9 @@ const APPROVAL_MODES: readonly ApprovalMode[] = [
"always-ask",
];
/** The five valid per-turn thinking level names (TaskCreateRequest.thinkingLevel). */
const THINKING_LEVELS: readonly ThinkingLevelName[] = ["none", "low", "medium", "high", "xhigh"];
/** Accepted `category` query values of the list endpoint (SessionCategory, spelled out for validation). */
const SESSION_CATEGORIES: readonly SessionCategory[] = [
"active",
@@ -201,8 +204,13 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
app.get("/:sessionId", async (c) => {
const row = resolveSession(c);
const hasTrace = await deps.sessionService.hasTrace(row);
const info = await deps.sessionService.toInfo(row, hasTrace);
// Single-session GET only: the latest Trace file's absolute path (a directory walk per
// call — too costly for list rows). The web's /model switch hands it to the new session's
// <model_switch_from> block so the model can read the source history itself.
const tracePath = hasTrace ? await deps.sessionService.latestTracePath(row) : undefined;
return c.json({
session: await deps.sessionService.toInfo(row, hasTrace),
session: { ...info, ...(tracePath !== undefined ? { tracePath } : {}) },
} satisfies SessionResponse);
});
@@ -357,9 +365,15 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
app.post("/:sessionId/tasks", async (c) => {
const row = resolveSession(c);
const input = parseTaskInput(await readJson(c));
const body = await readJson(c);
const input = parseTaskInput(body);
// Per-turn thinking level (optional): validated against the five names; omitted follows
// the session's default.
const thinkingLevel = optionalEnum(body, "thinkingLevel", THINKING_LEVELS);
// 202: the Task executes on the server, decoupled from the SSE connection; sessionId is the current actual id (the new id after self-heal).
const { sessionId } = await deps.manager.startTask(row.sessionId, input);
const { sessionId } = await deps.manager.startTask(row.sessionId, input, {
...(thinkingLevel !== undefined ? { thinkingLevel } : {}),
});
return c.json({ sessionId } satisfies TaskCreateResponse, 202);
});