Add Claude Ultracode with setting-change notices (#1625)

* Add Claude Ultracode mode notices

* Share provider notice definitions

* Test Claude Ultracode through public turn API
This commit is contained in:
Mohamed Boudra
2026-06-20 04:15:04 +00:00
committed by GitHub
parent 7af92120fe
commit 2b0740ff84
20 changed files with 310 additions and 53 deletions
@@ -27,6 +27,7 @@ import {
type AgentPermissionResponse,
type AgentPermissionResult,
type AgentPersistenceHandle,
type AgentProviderNotice,
type AgentPromptInput,
type AgentProvider,
type AgentRunOptions,
@@ -1276,9 +1277,9 @@ export class AgentManager {
});
}
async setAgentMode(agentId: string, modeId: string): Promise<void> {
async setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null> {
const agent = this.requireSessionAgent(agentId);
await agent.session.setMode(modeId);
const notice = (await agent.session.setMode(modeId)) ?? null;
const currentMode = (await agent.session.getCurrentMode()) ?? modeId;
agent.config.modeId = currentMode ?? undefined;
agent.currentModeId = currentMode;
@@ -1288,6 +1289,7 @@ export class AgentManager {
}
this.touchUpdatedAt(agent);
this.emitState(agent);
return notice;
}
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
@@ -1307,15 +1309,19 @@ export class AgentManager {
this.emitState(agent);
}
async setAgentThinkingOption(agentId: string, thinkingOptionId: string | null): Promise<void> {
async setAgentThinkingOption(
agentId: string,
thinkingOptionId: string | null,
): Promise<AgentProviderNotice | null> {
const agent = this.requireSessionAgent(agentId);
const normalizedThinkingOptionId =
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
? thinkingOptionId
: null;
let notice: AgentProviderNotice | null = null;
if (agent.session.setThinkingOption) {
await agent.session.setThinkingOption(normalizedThinkingOptionId);
notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null;
}
agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
@@ -1327,6 +1333,7 @@ export class AgentManager {
}
this.touchUpdatedAt(agent);
this.emitState(agent);
return notice;
}
async setAgentFeature(agentId: string, featureId: string, value: unknown): Promise<void> {
@@ -1,6 +1,9 @@
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
import type { AgentAttachment } from "@getpaseo/protocol/messages";
export type { AgentProviderNotice };
export type AgentProvider = string;
export interface AgentMetadata {
@@ -604,7 +607,7 @@ export interface AgentSession {
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
getAvailableModes(): Promise<AgentMode[]>;
getCurrentMode(): Promise<string | null>;
setMode(modeId: string): Promise<void>;
setMode(modeId: string): Promise<void | AgentProviderNotice>;
getPendingPermissions(): AgentPermissionRequest[];
respondToPermission(
requestId: string,
@@ -615,7 +618,7 @@ export interface AgentSession {
close(): Promise<void>;
listCommands?(): Promise<AgentSlashCommand[]>;
setModel?(modelId: string | null): Promise<void>;
setThinkingOption?(thinkingOptionId: string | null): Promise<void>;
setThinkingOption?(thinkingOptionId: string | null): Promise<void | AgentProviderNotice>;
setFeature?(featureId: string, value: unknown): Promise<void>;
revertConversation?(input: { messageId: string }): Promise<void>;
revertFiles?(input: { messageId: string }): Promise<void>;
@@ -134,8 +134,9 @@ class FakeLifecycleAgentManager implements LifecycleAgentManager {
this.notifiedAgentIds.push(agentId);
}
async setAgentMode(agentId: string, modeId: string): Promise<void> {
async setAgentMode(agentId: string, modeId: string) {
this.modeUpdates.push({ agentId, modeId });
return null;
}
async updateAgentMetadata(
@@ -286,7 +287,7 @@ describe("agent lifecycle commands", () => {
await expect(
setAgentModeCommand({ agentManager: manager }, { agentId: "agent-1", modeId: "plan" }),
).resolves.toEqual({ modeId: "plan" });
).resolves.toEqual({ modeId: "plan", notice: null });
expect(manager.modeUpdates).toEqual([{ agentId: "agent-1", modeId: "plan" }]);
});
@@ -2,6 +2,7 @@ import type { Logger } from "pino";
import type { ManagedAgent } from "./agent-manager.js";
import type { StoredAgentRecord } from "./agent-storage.js";
import type { AgentProviderNotice } from "./agent-sdk-types.js";
export type LifecycleAgentSnapshot = Pick<ManagedAgent, "id" | "cwd" | "lifecycle">;
@@ -20,7 +21,7 @@ export interface LifecycleAgentManager {
previousParentAgentId: string | null;
}>;
notifyAgentState(agentId: string): void;
setAgentMode(agentId: string, modeId: string): Promise<void>;
setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>;
updateAgentMetadata(
agentId: string,
updates: {
@@ -190,9 +191,9 @@ export async function setAgentModeCommand(
agentId: string;
modeId: string;
},
): Promise<{ modeId: string }> {
await dependencies.agentManager.setAgentMode(input.agentId, input.modeId);
return { modeId: input.modeId };
): Promise<{ modeId: string; notice: AgentProviderNotice | null }> {
const notice = await dependencies.agentManager.setAgentMode(input.agentId, input.modeId);
return { modeId: input.modeId, notice };
}
async function archiveStoredAgent(
@@ -0,0 +1,6 @@
import type { AgentProviderNotice } from "./agent-sdk-types.js";
export const SETTING_APPLIES_NEXT_TURN_NOTICE: AgentProviderNotice = {
type: "info",
message: "This change applies next turn.",
};
@@ -432,6 +432,29 @@ describe("ClaudeAgentClient.listModels", () => {
await fs.rm(emptyConfigDir, { recursive: true, force: true });
}
});
test("exposes Ultracode only on Claude models that support it", async () => {
const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-"));
try {
const client = new ClaudeAgentClient({
logger,
resolveBinary: async () => "/test/claude/bin",
configDir: emptyConfigDir,
});
const models = await client.listModels({ cwd: "/tmp/claude-models", force: false });
const getThinkingIds = (modelId: string) => {
return models.find((model) => model.id === modelId)?.thinkingOptions?.map(({ id }) => id);
};
expect(getThinkingIds("claude-fable-5")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-7")).not.toContain("ultracode");
expect(getThinkingIds("claude-sonnet-4-6")).not.toContain("ultracode");
} finally {
await fs.rm(emptyConfigDir, { recursive: true, force: true });
}
});
});
describe("ClaudeAgentClient binary resolution", () => {
@@ -526,12 +549,26 @@ describe("ClaudeAgentSession features", () => {
const logger = createTestLogger();
function createQueryMock() {
const queryReturn = vi.fn(async () => undefined);
let endQuery: (() => void) | null = null;
const queryEnded = new Promise<void>((resolve) => {
endQuery = resolve;
});
const queryReturn = vi.fn(async () => {
endQuery?.();
});
const queryMock = {
close: vi.fn(),
return: queryReturn,
applyFlagSettings: vi.fn(async () => undefined),
setModel: vi.fn(async () => undefined),
[Symbol.asyncIterator](): AsyncIterator<SDKMessage, void> {
return {
next: async () => {
await queryEnded;
return { value: undefined, done: true };
},
};
},
};
const queryFactory = vi.fn(() => queryMock);
return { queryFactory, queryMock };
@@ -585,6 +622,58 @@ describe("ClaudeAgentSession features", () => {
await session.close();
});
test("maps Ultracode to xhigh effort and Claude ultracode settings", async () => {
const { queryFactory } = createQueryMock();
const client = new ClaudeAgentClient({
logger,
queryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
model: "claude-opus-4-8",
thinkingOptionId: "ultracode",
});
await expect(session.startTurn("hello")).resolves.toEqual({
turnId: expect.stringMatching(/^foreground-turn-/),
});
expect(queryFactory.mock.calls[0]?.[0].options).toMatchObject({
effort: "xhigh",
thinking: { type: "adaptive" },
settings: { ultracode: true },
});
await session.close();
});
test("returns a next-turn notice when changing Claude thinking during an active turn", async () => {
const { queryFactory } = createQueryMock();
const client = new ClaudeAgentClient({
logger,
queryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
model: "claude-opus-4-8",
});
await expect(session.startTurn("hello")).resolves.toEqual({
turnId: expect.stringMatching(/^foreground-turn-/),
});
await expect(session.setThinkingOption?.("ultracode")).resolves.toEqual({
type: "info",
message: "This change applies next turn.",
});
await session.close();
});
test("toggles fast mode on the active query without restarting it", async () => {
const { queryFactory, queryMock } = createQueryMock();
const client = new ClaudeAgentClient({
@@ -47,6 +47,7 @@ import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./quer
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
import { claudeProjectDirSync } from "./project-dir.js";
import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js";
import {
getAgentStreamEventTurnId,
@@ -64,6 +65,7 @@ import {
type AgentPermissionResponse,
type AgentPermissionUpdate,
type AgentPersistenceHandle,
type AgentProviderNotice,
type AgentPromptInput,
type AgentRunOptions,
type AgentRunResult,
@@ -362,6 +364,7 @@ interface ClaudeAgentSessionOptions {
}
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
type ClaudeThinkingOption = ClaudeThinkingEffort | "ultracode";
function resolvePathEnvKey(): "Path" | "PATH" | null {
if (process.env["Path"] !== undefined) return "Path";
@@ -408,6 +411,10 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
);
}
function isClaudeThinkingOption(value: string | null | undefined): value is ClaudeThinkingOption {
return value === "ultracode" || isClaudeThinkingEffort(value);
}
interface ClaudeOptionsLogSummary {
cwd: string | null;
permissionMode: string | null;
@@ -1966,7 +1973,7 @@ class ClaudeAgentSession implements AgentSession {
this.persistence = null;
}
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
async setThinkingOption(thinkingOptionId: string | null): Promise<void | AgentProviderNotice> {
const normalizedThinkingOptionId =
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
? thinkingOptionId
@@ -1974,12 +1981,15 @@ class ClaudeAgentSession implements AgentSession {
if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
this.config.thinkingOptionId = undefined;
} else if (isClaudeThinkingEffort(normalizedThinkingOptionId)) {
} else if (isClaudeThinkingOption(normalizedThinkingOptionId)) {
this.config.thinkingOptionId = normalizedThinkingOptionId;
} else {
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
}
this.queryRestartNeeded = true;
if (this.activeForegroundTurnId || this.autonomousTurn) {
return SETTING_APPLIES_NEXT_TURN_NOTICE;
}
}
async setFeature(featureId: string, value: unknown): Promise<void> {
@@ -2607,15 +2617,19 @@ class ClaudeAgentSession implements AgentSession {
private resolveThinkingConfig(): {
thinking: ClaudeOptions["thinking"];
effort: ClaudeOptions["effort"];
ultracode: boolean;
} {
const thinkingOptionId =
this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
? this.config.thinkingOptionId
: undefined;
if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) {
return { thinking: { type: "adaptive" }, effort: thinkingOptionId };
if (thinkingOptionId === "ultracode") {
return { thinking: { type: "adaptive" }, effort: "xhigh", ultracode: true };
}
return { thinking: undefined, effort: undefined };
if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) {
return { thinking: { type: "adaptive" }, effort: thinkingOptionId, ultracode: false };
}
return { thinking: undefined, effort: undefined, ultracode: false };
}
private buildAppendedSystemPrompt(): string {
@@ -2641,10 +2655,10 @@ class ClaudeAgentSession implements AgentSession {
}
private async buildOptions(): Promise<ClaudeOptions> {
const { thinking, effort } = this.resolveThinkingConfig();
const { thinking, effort, ultracode } = this.resolveThinkingConfig();
const appendedSystemPrompt = this.buildAppendedSystemPrompt();
const extraClaudeOptions = this.config.extra?.claude;
const fastModeOptions = this.buildFastModeOptions(extraClaudeOptions);
const settingsOptions = this.buildSettingsOptions(extraClaudeOptions, { ultracode });
const sdkEnv = this.buildSdkEnv(extraClaudeOptions);
assertClaudeAutoModeEligible(this.currentMode, sdkEnv);
@@ -2697,7 +2711,7 @@ class ClaudeAgentSession implements AgentSession {
...(thinking ? { thinking } : {}),
...(effort ? { effort } : {}),
...extraClaudeOptions,
...fastModeOptions,
...settingsOptions,
...(this.persistSession === undefined ? {} : { persistSession: this.persistSession }),
env: sdkEnv,
};
@@ -2722,14 +2736,20 @@ class ClaudeAgentSession implements AgentSession {
return base;
}
private buildFastModeOptions(
private buildSettingsOptions(
extraClaudeOptions: Partial<ClaudeOptions> | undefined,
input: { ultracode: boolean },
): Pick<ClaudeOptions, "settings"> | Record<string, never> {
const fastMode = this.resolveFastModeSetting();
if (fastMode === null) {
if (fastMode === null && !input.ultracode) {
return {};
}
return { settings: mergeClaudeSettings(extraClaudeOptions?.settings, { fastMode }) };
return {
settings: mergeClaudeSettings(extraClaudeOptions?.settings, {
...(fastMode === null ? {} : { fastMode }),
...(input.ultracode ? { ultracode: true } : {}),
}),
};
}
private resolveFastModeSetting(): boolean | null {
@@ -20,20 +20,25 @@ const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [
{ id: "max", label: "Max" },
] as const;
const CLAUDE_ULTRACODE_THINKING_OPTIONS = [
...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS,
{ id: "ultracode", label: "Ultracode" },
] as const;
const CLAUDE_MODELS: AgentModelDefinition[] = [
{
provider: "claude",
id: "claude-fable-5",
label: "Fable 5",
description: "Fable 5 · Most powerful model",
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
},
{
provider: "claude",
id: "claude-opus-4-8[1m]",
label: "Opus 4.8 1M",
description: "Opus 4.8 with 1M context window",
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
},
{
provider: "claude",
@@ -41,7 +46,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [
label: "Opus 4.8",
description: "Opus 4.8 · Latest release",
isDefault: true,
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
},
{
provider: "claude",
@@ -338,6 +338,24 @@ describe("Codex app-server provider", () => {
});
});
test("setMode and setThinkingOption return a next-turn notice while a turn is active", async () => {
const session = createSession({ modeId: "auto", thinkingOptionId: "medium" });
await expect(session.setMode("full-access")).resolves.toEqual({
type: "info",
message: "This change applies next turn.",
});
await expect(session.setThinkingOption?.("high")).resolves.toEqual({
type: "info",
message: "This change applies next turn.",
});
session.activeForegroundTurnId = null;
await expect(session.setMode("auto")).resolves.toBeUndefined();
await expect(session.setThinkingOption?.("low")).resolves.toBeUndefined();
});
test.each(["auto_review", "guardian_subagent"])(
"parses %s thread/start response as auto-review mode",
async (approvalsReviewer) => {
@@ -13,6 +13,7 @@ import {
type AgentPermissionRequest,
type AgentPermissionResponse,
type AgentPermissionResult,
type AgentProviderNotice,
type AgentPromptContentBlock,
type AgentPromptInput,
type AgentRunOptions,
@@ -92,6 +93,7 @@ import {
toDiagnosticErrorMessage,
} from "./diagnostic-utils.js";
import { runProviderTurn } from "./provider-runner.js";
import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../provider-notices.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
function assertChildWithPipes(
@@ -3654,10 +3656,13 @@ export class CodexAppServerAgentSession implements AgentSession {
return this.currentMode ?? null;
}
async setMode(modeId: string): Promise<void> {
async setMode(modeId: string): Promise<void | AgentProviderNotice> {
validateCodexMode(modeId);
this.currentMode = modeId;
this.cachedRuntimeInfo = null;
if (this.activeForegroundTurnId) {
return SETTING_APPLIES_NEXT_TURN_NOTICE;
}
}
async setModel(modelId: string | null): Promise<void> {
@@ -3669,10 +3674,13 @@ export class CodexAppServerAgentSession implements AgentSession {
this.cachedRuntimeInfo = null;
}
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
async setThinkingOption(thinkingOptionId: string | null): Promise<void | AgentProviderNotice> {
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
this.refreshResolvedCollaborationMode();
this.cachedRuntimeInfo = null;
if (this.activeForegroundTurnId) {
return SETTING_APPLIES_NEXT_TURN_NOTICE;
}
}
async setFeature(featureId: string, value: unknown): Promise<void> {
+7 -4
View File
@@ -4672,14 +4672,17 @@ export class Session {
this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
try {
await setAgentModeCommand({ agentManager: this.agentManager }, { agentId, modeId });
const result = await setAgentModeCommand(
{ agentManager: this.agentManager },
{ agentId, modeId },
);
this.sessionLogger.info(
{ agentId, modeId, requestId },
"session: set_agent_mode_request success",
);
this.emit({
type: "set_agent_mode_response",
payload: { requestId, agentId, accepted: true, error: null },
payload: { requestId, agentId, accepted: true, error: null, notice: result.notice },
});
} catch (error) {
this.sessionLogger.error(
@@ -4808,14 +4811,14 @@ export class Session {
);
try {
await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
const notice = await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
this.sessionLogger.info(
{ agentId, thinkingOptionId, requestId },
"session: set_agent_thinking_request success",
);
this.emit({
type: "set_agent_thinking_response",
payload: { requestId, agentId, accepted: true, error: null },
payload: { requestId, agentId, accepted: true, error: null, notice },
});
} catch (error) {
this.sessionLogger.error(