fix(coding-agent): classify user-invoked /skill turns under auto thinking
A user-invoked /skill:<name> reaches the session as a user-attributed skill custom message (role custom, attribution user) whose expanded SKILL.md body is the task prompt. The auto-thinking gate in #promptWithMessage only accepted role === "user", so these turns skipped classifyDifficulty/applyAutoThinkingLevel and the effort stayed stuck on pending auto. Broaden the gate to also accept user-invoked skill prompts via the now-exported isUserInvokedSkillPrompt helper; agent-originated and autoload skill injections stay excluded. Fixes #8554
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `defaultThinkingLevel: auto` skipping classification for user-invoked `/skill:<name>` turns, which left the effort stuck on pending `auto`; user-attributed skill prompts now classify like any user turn while agent/autoload injections stay excluded ([#8554](https://github.com/can1357/oh-my-pi/issues/8554)).
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -297,6 +297,7 @@ import {
|
||||
type InterruptedThinkingDetails,
|
||||
isEmptyErrorTurn,
|
||||
isUserInterruptAbort,
|
||||
isUserInvokedSkillPrompt,
|
||||
logProviderTurnError,
|
||||
normalizeCustomMessagePayload,
|
||||
type PythonExecutionMessage,
|
||||
@@ -5568,10 +5569,14 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
// Auto thinking: classify this real user turn and set the effective level
|
||||
// before the model request. Synthetic/tool-continuation turns (developer/
|
||||
// custom roles) and non-auto sessions are skipped. Never blocks the turn —
|
||||
// failures fall back to a concrete level inside the helper.
|
||||
if (this.isAutoThinking && message.role === "user") {
|
||||
// before the model request. A user-invoked `/skill:<name>` arrives as a
|
||||
// user-attributed skill custom message whose expanded body is the task
|
||||
// prompt, so it counts as a user turn. Synthetic/tool-continuation turns
|
||||
// (developer roles), agent-originated or autoloaded skill injections, and
|
||||
// non-auto sessions are skipped. Never blocks the turn — failures fall
|
||||
// back to a concrete level inside the helper.
|
||||
const isUserTurn = message.role === "user" || (message.role === "custom" && isUserInvokedSkillPrompt(message));
|
||||
if (this.isAutoThinking && isUserTurn) {
|
||||
await this.#models.applyAutoThinkingLevel(expandedText, generation);
|
||||
if (this.#promptGeneration !== generation) {
|
||||
return;
|
||||
|
||||
@@ -1085,7 +1085,8 @@ function customMessageContentToLlmContent(content: CustomMessage["content"]): (T
|
||||
return typeof content === "string" ? [{ type: "text", text: content }] : content;
|
||||
}
|
||||
|
||||
function isUserInvokedSkillPrompt(message: CustomMessage): boolean {
|
||||
/** True for a `/skill:<name>` prompt the user invoked directly (attribution `user`), as opposed to an agent/autoload injection. */
|
||||
export function isUserInvokedSkillPrompt(message: CustomMessage): boolean {
|
||||
return message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user";
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { SKILL_PROMPT_MESSAGE_TYPE } from "@oh-my-pi/pi-coding-agent/session/messages";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import {
|
||||
AUTO_THINKING,
|
||||
@@ -374,6 +375,63 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
expect(session.agent.state.thinkingLevel).toBe(Effort.Medium);
|
||||
});
|
||||
|
||||
it("classifies a user-invoked /skill turn under auto (resolves concrete effort)", async () => {
|
||||
const model = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
||||
await createSession({
|
||||
initialModelId: model.id,
|
||||
initialThinkingLevel: Effort.High,
|
||||
modelRoles: { default: `${model.provider}/${model.id}` },
|
||||
});
|
||||
const promptSpy = vi.spyOn(session.agent, "prompt").mockResolvedValue(undefined);
|
||||
const classifierSpy = vi.spyOn(autoThinkingClassifier, "classifyDifficulty").mockResolvedValue(Effort.Medium);
|
||||
|
||||
session.setThinkingLevel(AUTO_THINKING);
|
||||
expect(session.autoResolvedThinkingLevel()).toBeUndefined();
|
||||
|
||||
// A /skill:<name> invocation reaches the session as a user-attributed
|
||||
// custom message, not a `user` role. It is still a real user turn.
|
||||
await session.promptCustomMessage({
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: "Expanded SKILL.md body: implement the focused parser fix",
|
||||
display: true,
|
||||
details: { name: "implement", path: "/skills/implement/SKILL.md", args: "the parser" },
|
||||
attribution: "user",
|
||||
});
|
||||
|
||||
expect(classifierSpy).toHaveBeenCalledTimes(1);
|
||||
expect(classifierSpy.mock.calls[0]?.[0]).toContain("implement the focused parser fix");
|
||||
expect(promptSpy).toHaveBeenCalledTimes(1);
|
||||
expect(session.configuredThinkingLevel()).toBe(AUTO_THINKING);
|
||||
expect(session.thinkingLevel).toBe(Effort.Medium);
|
||||
expect(session.autoResolvedThinkingLevel()).toBe(Effort.Medium);
|
||||
expect(session.agent.state.thinkingLevel).toBe(Effort.Medium);
|
||||
});
|
||||
|
||||
it("does not classify an agent-originated skill custom message under auto", async () => {
|
||||
const model = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
||||
await createSession({
|
||||
initialModelId: model.id,
|
||||
initialThinkingLevel: Effort.High,
|
||||
modelRoles: { default: `${model.provider}/${model.id}` },
|
||||
});
|
||||
vi.spyOn(session.agent, "prompt").mockResolvedValue(undefined);
|
||||
const classifierSpy = vi.spyOn(autoThinkingClassifier, "classifyDifficulty").mockResolvedValue(Effort.Medium);
|
||||
|
||||
session.setThinkingLevel(AUTO_THINKING);
|
||||
|
||||
// Autoloaded / agent-originated skill injections must stay excluded.
|
||||
await session.promptCustomMessage({
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: "Autoloaded skill body",
|
||||
display: false,
|
||||
details: { name: "autoload", path: "/skills/autoload/SKILL.md" },
|
||||
attribution: "agent",
|
||||
});
|
||||
|
||||
expect(classifierSpy).not.toHaveBeenCalled();
|
||||
expect(session.autoResolvedThinkingLevel()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps auto active on resume (pending until the next turn reclassifies)", async () => {
|
||||
const model = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
||||
const agent = new Agent({
|
||||
|
||||
Reference in New Issue
Block a user