From 6fb933f99b3a48604a655c287f9544b5ae93a300 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 12:02:27 +0900 Subject: [PATCH 1/2] feat(plugin): add session agent resolver for subagent_type lookup --- src/plugin/session-agent-resolver.test.ts | 96 +++++++++++++++++++++++ src/plugin/session-agent-resolver.ts | 36 +++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/plugin/session-agent-resolver.test.ts create mode 100644 src/plugin/session-agent-resolver.ts diff --git a/src/plugin/session-agent-resolver.test.ts b/src/plugin/session-agent-resolver.test.ts new file mode 100644 index 00000000..a84e51a5 --- /dev/null +++ b/src/plugin/session-agent-resolver.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, test } from "bun:test" +import { resolveSessionAgent } from "./session-agent-resolver" + +describe("resolveSessionAgent", () => { + test("returns agent from first message with agent field", async () => { + //#given + const client = { + session: { + messages: async () => ({ + data: [ + { info: { role: "user" } }, + { info: { role: "assistant", agent: "explore" } }, + { info: { role: "assistant", agent: "oracle" } }, + ], + }), + }, + } + + //#when + const agent = await resolveSessionAgent(client, "ses_test") + + //#then + expect(agent).toBe("explore") + }) + + test("skips messages without agent field", async () => { + //#given + const client = { + session: { + messages: async () => ({ + data: [ + { info: { role: "user" } }, + { info: { role: "system" } }, + { info: { role: "assistant", agent: "plan" } }, + ], + }), + }, + } + + //#when + const agent = await resolveSessionAgent(client, "ses_test") + + //#then + expect(agent).toBe("plan") + }) + + test("returns undefined when no messages have agent", async () => { + //#given + const client = { + session: { + messages: async () => ({ + data: [ + { info: { role: "user" } }, + { info: { role: "assistant" } }, + ], + }), + }, + } + + //#when + const agent = await resolveSessionAgent(client, "ses_test") + + //#then + expect(agent).toBeUndefined() + }) + + test("returns undefined when session has no messages", async () => { + //#given + const client = { + session: { + messages: async () => ({ data: [] }), + }, + } + + //#when + const agent = await resolveSessionAgent(client, "ses_test") + + //#then + expect(agent).toBeUndefined() + }) + + test("returns undefined when API call fails", async () => { + //#given + const client = { + session: { + messages: async () => { throw new Error("API error") }, + }, + } + + //#when + const agent = await resolveSessionAgent(client, "ses_test") + + //#then + expect(agent).toBeUndefined() + }) +}) diff --git a/src/plugin/session-agent-resolver.ts b/src/plugin/session-agent-resolver.ts new file mode 100644 index 00000000..8d9837ff --- /dev/null +++ b/src/plugin/session-agent-resolver.ts @@ -0,0 +1,36 @@ +import { log } from "../shared" + +interface SessionMessage { + info?: { + agent?: string + role?: string + } +} + +type SessionClient = { + session: { + messages: (opts: { path: { id: string } }) => Promise<{ data?: SessionMessage[] }> + } +} + +export async function resolveSessionAgent( + client: SessionClient, + sessionId: string, +): Promise { + try { + const messagesResp = await client.session.messages({ path: { id: sessionId } }) + const messages = (messagesResp.data ?? []) as SessionMessage[] + + for (const msg of messages) { + if (msg.info?.agent) { + return msg.info.agent + } + } + } catch (error) { + log("[session-agent-resolver] Failed to resolve agent from session", { + sessionId, + error: String(error), + }) + } + return undefined +} From 1a5c9f228d5d8efedadd978a613cc7b1f36ee701 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 12:02:40 +0900 Subject: [PATCH 2/2] fix(tool-execute-before): resolve subagent_type for TUI display OpenCode TUI reads input.subagent_type to display task type. When subagent_type was missing (e.g., category-only or session continuation), TUI showed 'Unknown Task'. Fix: - category provided: always set subagent_type to 'sisyphus-junior' (previously only when subagent_type was absent) - session_id continuation: resolve agent from session's first message - fallback to 'continue' if session has no agent info --- src/plugin/tool-execute-before.test.ts | 138 +++++++++++++++++++++++++ src/plugin/tool-execute-before.ts | 8 +- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/plugin/tool-execute-before.test.ts diff --git a/src/plugin/tool-execute-before.test.ts b/src/plugin/tool-execute-before.test.ts new file mode 100644 index 00000000..1b5b8078 --- /dev/null +++ b/src/plugin/tool-execute-before.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, test } from "bun:test" +import { createToolExecuteBeforeHandler } from "./tool-execute-before" +import type { CreatedHooks } from "../create-hooks" + +describe("createToolExecuteBeforeHandler", () => { + describe("task tool subagent_type normalization", () => { + const emptyHooks = {} as CreatedHooks + + function createCtxWithSessionMessages(messages: Array<{ info?: { agent?: string; role?: string } }> = []) { + return { + client: { + session: { + messages: async () => ({ data: messages }), + }, + }, + } as unknown as Parameters[0]["ctx"] + } + + test("sets subagent_type to sisyphus-junior when category is provided without subagent_type", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { category: "quick", description: "Test" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("sisyphus-junior") + }) + + test("preserves existing subagent_type when explicitly provided", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { subagent_type: "plan", description: "Plan test" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("plan") + }) + + test("sets subagent_type to sisyphus-junior when category provided with different subagent_type", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { category: "quick", subagent_type: "oracle", description: "Test" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("sisyphus-junior") + }) + + test("resolves subagent_type from session first message when session_id provided without subagent_type", async () => { + //#given + const ctx = createCtxWithSessionMessages([ + { info: { role: "user" } }, + { info: { role: "assistant", agent: "explore" } }, + { info: { role: "assistant", agent: "oracle" } }, + ]) + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { session_id: "ses_abc123", description: "Continue task", prompt: "fix it" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("explore") + }) + + test("falls back to 'continue' when session has no agent info", async () => { + //#given + const ctx = createCtxWithSessionMessages([ + { info: { role: "user" } }, + { info: { role: "assistant" } }, + ]) + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { session_id: "ses_abc123", description: "Continue task", prompt: "fix it" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("continue") + }) + + test("preserves subagent_type when session_id is provided with explicit subagent_type", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { session_id: "ses_abc123", subagent_type: "explore", description: "Continue explore" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("explore") + }) + + test("does not modify args for non-task tools", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "bash", sessionID: "ses_123", callID: "call_1" } + const output = { args: { command: "ls" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBeUndefined() + }) + + test("does not set subagent_type when neither category nor session_id is provided and subagent_type is present", async () => { + //#given + const ctx = createCtxWithSessionMessages() + const handler = createToolExecuteBeforeHandler({ ctx, hooks: emptyHooks }) + const input = { tool: "task", sessionID: "ses_123", callID: "call_1" } + const output = { args: { subagent_type: "oracle", description: "Oracle task" } as Record } + + //#when + await handler(input, output) + + //#then + expect(output.args.subagent_type).toBe("oracle") + }) + }) +}) diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index c7fefb0a..9d07e5ef 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -3,6 +3,7 @@ import type { PluginContext } from "./types" import { getMainSessionID } from "../features/claude-code-session-state" import { clearBoulderState } from "../features/boulder-state" import { log } from "../shared" +import { resolveSessionAgent } from "./session-agent-resolver" import type { CreatedHooks } from "../create-hooks" @@ -34,8 +35,13 @@ export function createToolExecuteBeforeHandler(args: { const argsObject = output.args const category = typeof argsObject.category === "string" ? argsObject.category : undefined const subagentType = typeof argsObject.subagent_type === "string" ? argsObject.subagent_type : undefined - if (category && !subagentType) { + const sessionId = typeof argsObject.session_id === "string" ? argsObject.session_id : undefined + + if (category) { argsObject.subagent_type = "sisyphus-junior" + } else if (!subagentType && sessionId) { + const resolvedAgent = await resolveSessionAgent(ctx.client, sessionId) + argsObject.subagent_type = resolvedAgent ?? "continue" } }