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:
+4
-2
@@ -343,7 +343,7 @@ interface AgentSession {
|
|||||||
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
||||||
getAvailableModes(): Promise<AgentMode[]>;
|
getAvailableModes(): Promise<AgentMode[]>;
|
||||||
getCurrentMode(): Promise<string | null>;
|
getCurrentMode(): Promise<string | null>;
|
||||||
setMode(modeId: string): Promise<void>;
|
setMode(modeId: string): Promise<void | AgentProviderNotice>;
|
||||||
getPendingPermissions(): AgentPermissionRequest[];
|
getPendingPermissions(): AgentPermissionRequest[];
|
||||||
respondToPermission(
|
respondToPermission(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
@@ -355,7 +355,7 @@ interface AgentSession {
|
|||||||
// Optional:
|
// Optional:
|
||||||
listCommands?(): Promise<AgentSlashCommand[]>;
|
listCommands?(): Promise<AgentSlashCommand[]>;
|
||||||
setModel?(modelId: string | null): Promise<void>;
|
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>;
|
setFeature?(featureId: string, value: unknown): Promise<void>;
|
||||||
tryHandleOutOfBand?(prompt: AgentPromptInput): {
|
tryHandleOutOfBand?(prompt: AgentPromptInput): {
|
||||||
run(ctx: { emit: (event: AgentStreamEvent) => void }): Promise<void>;
|
run(ctx: { emit: (event: AgentStreamEvent) => void }): Promise<void>;
|
||||||
@@ -363,6 +363,8 @@ interface AgentSession {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`setMode` and `setThinkingOption` may return an `AgentProviderNotice` when the provider knows the change needs user-facing context. For example, providers that stage changes until the next turn should return an `info` notice while a turn is already running. The app renders the notice generically as a toast; provider-specific lifecycle behavior stays in the provider implementation.
|
||||||
|
|
||||||
### Steps
|
### Steps
|
||||||
|
|
||||||
1. Create `packages/server/src/server/agent/providers/{name}-agent.ts` implementing both interfaces
|
1. Create `packages/server/src/server/agent/providers/{name}-agent.ts` implementing both interfaces
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
|
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
||||||
|
|
||||||
interface AgentControlOption {
|
interface AgentControlOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1486,10 +1487,13 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
console.warn("[AgentControls] persist thinking preference failed", error);
|
console.warn("[AgentControls] persist thinking preference failed", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => {
|
void client
|
||||||
console.warn("[AgentControls] setAgentThinkingOption failed", error);
|
.setAgentThinkingOption(agentId, thinkingOptionId)
|
||||||
toast.error(toErrorMessage(error));
|
.then((notice) => showProviderNoticeToast(toast, notice))
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.warn("[AgentControls] setAgentThinkingOption failed", error);
|
||||||
|
toast.error(toErrorMessage(error));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[activeModelId, agentId, agentProvider, client, toast, updatePreferences],
|
[activeModelId, agentId, agentProvider, client, toast, updatePreferences],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { resolveProviderDefinition } from "@/utils/provider-definitions";
|
|||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
|
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
||||||
import { formatAgentModeLabel } from "@/composer/agent-controls/utils";
|
import { formatAgentModeLabel } from "@/composer/agent-controls/utils";
|
||||||
import type { AgentMode, AgentProvider } from "@getpaseo/protocol/agent-types";
|
import type { AgentMode, AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||||
import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
|
import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
|
||||||
@@ -272,10 +273,13 @@ export const AgentModeControl = memo(function AgentModeControl({
|
|||||||
const handleSelectMode = useCallback(
|
const handleSelectMode = useCallback(
|
||||||
(modeId: string) => {
|
(modeId: string) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
void client.setAgentMode(agentId, modeId).catch((error) => {
|
void client
|
||||||
console.warn("[AgentModeControl] setAgentMode failed", error);
|
.setAgentMode(agentId, modeId)
|
||||||
toast.error(toErrorMessage(error));
|
.then((notice) => showProviderNoticeToast(toast, notice))
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.warn("[AgentModeControl] setAgentMode failed", error);
|
||||||
|
toast.error(toErrorMessage(error));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[agentId, client, toast],
|
[agentId, client, toast],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
|
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
||||||
import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache";
|
import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache";
|
||||||
import {
|
import {
|
||||||
applyLegacyDaemonWorkspaceOwnership,
|
applyLegacyDaemonWorkspaceOwnership,
|
||||||
@@ -1951,10 +1952,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
console.warn("[Session] setAgentMode skipped: daemon unavailable");
|
console.warn("[Session] setAgentMode skipped: daemon unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void client.setAgentMode(agentId, modeId).catch((error) => {
|
void client
|
||||||
console.error("[Session] Failed to set agent mode:", error);
|
.setAgentMode(agentId, modeId)
|
||||||
toast.error(toErrorMessage(error));
|
.then((notice) => showProviderNoticeToast(toast, notice))
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.error("[Session] Failed to set agent mode:", error);
|
||||||
|
toast.error(toErrorMessage(error));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[client, toast],
|
[client, toast],
|
||||||
);
|
);
|
||||||
@@ -1979,10 +1983,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
console.warn("[Session] setAgentThinkingOption skipped: daemon unavailable");
|
console.warn("[Session] setAgentThinkingOption skipped: daemon unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => {
|
void client
|
||||||
console.error("[Session] Failed to set agent thinking option:", error);
|
.setAgentThinkingOption(agentId, thinkingOptionId)
|
||||||
toast.error(toErrorMessage(error));
|
.then((notice) => showProviderNoticeToast(toast, notice))
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.error("[Session] Failed to set agent thinking option:", error);
|
||||||
|
toast.error(toErrorMessage(error));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[client, toast],
|
[client, toast],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
|
||||||
|
import type { ToastApi } from "@/components/toast-host";
|
||||||
|
|
||||||
|
export function showProviderNoticeToast(
|
||||||
|
toast: ToastApi,
|
||||||
|
notice: AgentProviderNotice | null | undefined,
|
||||||
|
): void {
|
||||||
|
if (!notice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (notice.type === "error") {
|
||||||
|
toast.error(notice.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.show(notice.message, {
|
||||||
|
variant: "default",
|
||||||
|
durationMs: notice.type === "warning" ? 3200 : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -85,6 +85,7 @@ import type {
|
|||||||
AgentPermissionRequest,
|
AgentPermissionRequest,
|
||||||
AgentPermissionResponse,
|
AgentPermissionResponse,
|
||||||
AgentPersistenceHandle,
|
AgentPersistenceHandle,
|
||||||
|
AgentProviderNotice,
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
} from "@getpaseo/protocol/agent-types";
|
} from "@getpaseo/protocol/agent-types";
|
||||||
@@ -2444,7 +2445,7 @@ export class DaemonClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
async setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null> {
|
||||||
const requestId = this.createRequestId();
|
const requestId = this.createRequestId();
|
||||||
const message = SessionInboundMessageSchema.parse({
|
const message = SessionInboundMessageSchema.parse({
|
||||||
type: "set_agent_mode_request",
|
type: "set_agent_mode_request",
|
||||||
@@ -2470,6 +2471,7 @@ export class DaemonClient {
|
|||||||
if (!payload.accepted) {
|
if (!payload.accepted) {
|
||||||
throw new Error(payload.error ?? "setAgentMode rejected");
|
throw new Error(payload.error ?? "setAgentMode rejected");
|
||||||
}
|
}
|
||||||
|
return payload.notice ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
|
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
|
||||||
@@ -2529,7 +2531,10 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentThinkingOption(agentId: string, thinkingOptionId: string | null): Promise<void> {
|
async setAgentThinkingOption(
|
||||||
|
agentId: string,
|
||||||
|
thinkingOptionId: string | null,
|
||||||
|
): Promise<AgentProviderNotice | null> {
|
||||||
const requestId = this.createRequestId();
|
const requestId = this.createRequestId();
|
||||||
const message = SessionInboundMessageSchema.parse({
|
const message = SessionInboundMessageSchema.parse({
|
||||||
type: "set_agent_thinking_request",
|
type: "set_agent_thinking_request",
|
||||||
@@ -2555,6 +2560,7 @@ export class DaemonClient {
|
|||||||
if (!payload.accepted) {
|
if (!payload.accepted) {
|
||||||
throw new Error(payload.error ?? "setAgentThinkingOption rejected");
|
throw new Error(payload.error ?? "setAgentThinkingOption rejected");
|
||||||
}
|
}
|
||||||
|
return payload.notice ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartServer(reason?: string, requestId?: string): Promise<RestartRequestedStatusPayload> {
|
async restartServer(reason?: string, requestId?: string): Promise<RestartRequestedStatusPayload> {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export interface AgentMetadata {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AgentProviderNotice =
|
||||||
|
| { type: "info"; message: string }
|
||||||
|
| { type: "warning"; message: string }
|
||||||
|
| { type: "error"; message: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stdio-based MCP server (spawns a subprocess).
|
* Stdio-based MCP server (spawns a subprocess).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -178,6 +178,47 @@ describe("agent detach RPC", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("agent setting action responses", () => {
|
||||||
|
test("parses optional provider notices on mode and thinking responses", () => {
|
||||||
|
const mode = SessionOutboundMessageSchema.parse({
|
||||||
|
type: "set_agent_mode_response",
|
||||||
|
payload: {
|
||||||
|
requestId: "req-mode",
|
||||||
|
agentId: "agent-1",
|
||||||
|
accepted: true,
|
||||||
|
error: null,
|
||||||
|
notice: {
|
||||||
|
type: "info",
|
||||||
|
message: "This change applies next turn.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const thinking = SessionOutboundMessageSchema.parse({
|
||||||
|
type: "set_agent_thinking_response",
|
||||||
|
payload: {
|
||||||
|
requestId: "req-thinking",
|
||||||
|
agentId: "agent-1",
|
||||||
|
accepted: true,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mode.type).toBe("set_agent_mode_response");
|
||||||
|
if (mode.type !== "set_agent_mode_response") {
|
||||||
|
throw new Error("Expected set_agent_mode_response");
|
||||||
|
}
|
||||||
|
expect(mode.payload.notice).toEqual({
|
||||||
|
type: "info",
|
||||||
|
message: "This change applies next turn.",
|
||||||
|
});
|
||||||
|
expect(thinking.type).toBe("set_agent_thinking_response");
|
||||||
|
if (thinking.type !== "set_agent_thinking_response") {
|
||||||
|
throw new Error("Expected set_agent_thinking_response");
|
||||||
|
}
|
||||||
|
expect(thinking.payload.notice).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("file explorer request compatibility", () => {
|
describe("file explorer request compatibility", () => {
|
||||||
test("acceptBinary is optional for old clients and accepted for new clients", () => {
|
test("acceptBinary is optional for old clients and accepted for new clients", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ import type {
|
|||||||
ProviderStatus,
|
ProviderStatus,
|
||||||
AgentRuntimeInfo,
|
AgentRuntimeInfo,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
|
AgentProviderNotice,
|
||||||
ToolCallDetail,
|
ToolCallDetail,
|
||||||
ToolCallTimelineItem,
|
ToolCallTimelineItem,
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
@@ -202,6 +203,12 @@ const AgentSelectOptionSchema = z.object({
|
|||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const AgentProviderNoticeSchema: z.ZodType<AgentProviderNotice> = z.discriminatedUnion("type", [
|
||||||
|
z.object({ type: z.literal("info"), message: z.string() }),
|
||||||
|
z.object({ type: z.literal("warning"), message: z.string() }),
|
||||||
|
z.object({ type: z.literal("error"), message: z.string() }),
|
||||||
|
]);
|
||||||
|
|
||||||
export const AgentFeatureToggleSchema = z.object({
|
export const AgentFeatureToggleSchema = z.object({
|
||||||
type: z.literal("toggle"),
|
type: z.literal("toggle"),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -1264,6 +1271,7 @@ const AgentActionResponsePayloadSchema = z.object({
|
|||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
accepted: z.boolean(),
|
accepted: z.boolean(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
|
notice: AgentProviderNoticeSchema.nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SetAgentModeResponseMessageSchema = z.object({
|
export const SetAgentModeResponseMessageSchema = z.object({
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
type AgentPermissionResponse,
|
type AgentPermissionResponse,
|
||||||
type AgentPermissionResult,
|
type AgentPermissionResult,
|
||||||
type AgentPersistenceHandle,
|
type AgentPersistenceHandle,
|
||||||
|
type AgentProviderNotice,
|
||||||
type AgentPromptInput,
|
type AgentPromptInput,
|
||||||
type AgentProvider,
|
type AgentProvider,
|
||||||
type AgentRunOptions,
|
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);
|
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;
|
const currentMode = (await agent.session.getCurrentMode()) ?? modeId;
|
||||||
agent.config.modeId = currentMode ?? undefined;
|
agent.config.modeId = currentMode ?? undefined;
|
||||||
agent.currentModeId = currentMode;
|
agent.currentModeId = currentMode;
|
||||||
@@ -1288,6 +1289,7 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
this.touchUpdatedAt(agent);
|
this.touchUpdatedAt(agent);
|
||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
|
return notice;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
|
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
|
||||||
@@ -1307,15 +1309,19 @@ export class AgentManager {
|
|||||||
this.emitState(agent);
|
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 agent = this.requireSessionAgent(agentId);
|
||||||
const normalizedThinkingOptionId =
|
const normalizedThinkingOptionId =
|
||||||
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
|
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
|
||||||
? thinkingOptionId
|
? thinkingOptionId
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
let notice: AgentProviderNotice | null = null;
|
||||||
if (agent.session.setThinkingOption) {
|
if (agent.session.setThinkingOption) {
|
||||||
await agent.session.setThinkingOption(normalizedThinkingOptionId);
|
notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
|
agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
|
||||||
@@ -1327,6 +1333,7 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
this.touchUpdatedAt(agent);
|
this.touchUpdatedAt(agent);
|
||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
|
return notice;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentFeature(agentId: string, featureId: string, value: unknown): Promise<void> {
|
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 { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
|
||||||
import type { AgentAttachment } from "@getpaseo/protocol/messages";
|
import type { AgentAttachment } from "@getpaseo/protocol/messages";
|
||||||
|
|
||||||
|
export type { AgentProviderNotice };
|
||||||
|
|
||||||
export type AgentProvider = string;
|
export type AgentProvider = string;
|
||||||
|
|
||||||
export interface AgentMetadata {
|
export interface AgentMetadata {
|
||||||
@@ -604,7 +607,7 @@ export interface AgentSession {
|
|||||||
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
||||||
getAvailableModes(): Promise<AgentMode[]>;
|
getAvailableModes(): Promise<AgentMode[]>;
|
||||||
getCurrentMode(): Promise<string | null>;
|
getCurrentMode(): Promise<string | null>;
|
||||||
setMode(modeId: string): Promise<void>;
|
setMode(modeId: string): Promise<void | AgentProviderNotice>;
|
||||||
getPendingPermissions(): AgentPermissionRequest[];
|
getPendingPermissions(): AgentPermissionRequest[];
|
||||||
respondToPermission(
|
respondToPermission(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
@@ -615,7 +618,7 @@ export interface AgentSession {
|
|||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
listCommands?(): Promise<AgentSlashCommand[]>;
|
listCommands?(): Promise<AgentSlashCommand[]>;
|
||||||
setModel?(modelId: string | null): Promise<void>;
|
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>;
|
setFeature?(featureId: string, value: unknown): Promise<void>;
|
||||||
revertConversation?(input: { messageId: string }): Promise<void>;
|
revertConversation?(input: { messageId: string }): Promise<void>;
|
||||||
revertFiles?(input: { messageId: string }): Promise<void>;
|
revertFiles?(input: { messageId: string }): Promise<void>;
|
||||||
|
|||||||
@@ -134,8 +134,9 @@ class FakeLifecycleAgentManager implements LifecycleAgentManager {
|
|||||||
this.notifiedAgentIds.push(agentId);
|
this.notifiedAgentIds.push(agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
async setAgentMode(agentId: string, modeId: string) {
|
||||||
this.modeUpdates.push({ agentId, modeId });
|
this.modeUpdates.push({ agentId, modeId });
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAgentMetadata(
|
async updateAgentMetadata(
|
||||||
@@ -286,7 +287,7 @@ describe("agent lifecycle commands", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
setAgentModeCommand({ agentManager: manager }, { agentId: "agent-1", modeId: "plan" }),
|
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" }]);
|
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 { ManagedAgent } from "./agent-manager.js";
|
||||||
import type { StoredAgentRecord } from "./agent-storage.js";
|
import type { StoredAgentRecord } from "./agent-storage.js";
|
||||||
|
import type { AgentProviderNotice } from "./agent-sdk-types.js";
|
||||||
|
|
||||||
export type LifecycleAgentSnapshot = Pick<ManagedAgent, "id" | "cwd" | "lifecycle">;
|
export type LifecycleAgentSnapshot = Pick<ManagedAgent, "id" | "cwd" | "lifecycle">;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ export interface LifecycleAgentManager {
|
|||||||
previousParentAgentId: string | null;
|
previousParentAgentId: string | null;
|
||||||
}>;
|
}>;
|
||||||
notifyAgentState(agentId: string): void;
|
notifyAgentState(agentId: string): void;
|
||||||
setAgentMode(agentId: string, modeId: string): Promise<void>;
|
setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>;
|
||||||
updateAgentMetadata(
|
updateAgentMetadata(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
updates: {
|
updates: {
|
||||||
@@ -190,9 +191,9 @@ export async function setAgentModeCommand(
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
modeId: string;
|
modeId: string;
|
||||||
},
|
},
|
||||||
): Promise<{ modeId: string }> {
|
): Promise<{ modeId: string; notice: AgentProviderNotice | null }> {
|
||||||
await dependencies.agentManager.setAgentMode(input.agentId, input.modeId);
|
const notice = await dependencies.agentManager.setAgentMode(input.agentId, input.modeId);
|
||||||
return { modeId: input.modeId };
|
return { modeId: input.modeId, notice };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function archiveStoredAgent(
|
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 });
|
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", () => {
|
describe("ClaudeAgentClient binary resolution", () => {
|
||||||
@@ -526,12 +549,26 @@ describe("ClaudeAgentSession features", () => {
|
|||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
function createQueryMock() {
|
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 = {
|
const queryMock = {
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
return: queryReturn,
|
return: queryReturn,
|
||||||
applyFlagSettings: vi.fn(async () => undefined),
|
applyFlagSettings: vi.fn(async () => undefined),
|
||||||
setModel: 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);
|
const queryFactory = vi.fn(() => queryMock);
|
||||||
return { queryFactory, queryMock };
|
return { queryFactory, queryMock };
|
||||||
@@ -585,6 +622,58 @@ describe("ClaudeAgentSession features", () => {
|
|||||||
await session.close();
|
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 () => {
|
test("toggles fast mode on the active query without restarting it", async () => {
|
||||||
const { queryFactory, queryMock } = createQueryMock();
|
const { queryFactory, queryMock } = createQueryMock();
|
||||||
const client = new ClaudeAgentClient({
|
const client = new ClaudeAgentClient({
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./quer
|
|||||||
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
|
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
|
||||||
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
||||||
import { claudeProjectDirSync } from "./project-dir.js";
|
import { claudeProjectDirSync } from "./project-dir.js";
|
||||||
|
import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getAgentStreamEventTurnId,
|
getAgentStreamEventTurnId,
|
||||||
@@ -64,6 +65,7 @@ import {
|
|||||||
type AgentPermissionResponse,
|
type AgentPermissionResponse,
|
||||||
type AgentPermissionUpdate,
|
type AgentPermissionUpdate,
|
||||||
type AgentPersistenceHandle,
|
type AgentPersistenceHandle,
|
||||||
|
type AgentProviderNotice,
|
||||||
type AgentPromptInput,
|
type AgentPromptInput,
|
||||||
type AgentRunOptions,
|
type AgentRunOptions,
|
||||||
type AgentRunResult,
|
type AgentRunResult,
|
||||||
@@ -362,6 +364,7 @@ interface ClaudeAgentSessionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||||
|
type ClaudeThinkingOption = ClaudeThinkingEffort | "ultracode";
|
||||||
|
|
||||||
function resolvePathEnvKey(): "Path" | "PATH" | null {
|
function resolvePathEnvKey(): "Path" | "PATH" | null {
|
||||||
if (process.env["Path"] !== undefined) return "Path";
|
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 {
|
interface ClaudeOptionsLogSummary {
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
permissionMode: string | null;
|
permissionMode: string | null;
|
||||||
@@ -1966,7 +1973,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.persistence = null;
|
this.persistence = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
|
async setThinkingOption(thinkingOptionId: string | null): Promise<void | AgentProviderNotice> {
|
||||||
const normalizedThinkingOptionId =
|
const normalizedThinkingOptionId =
|
||||||
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
|
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
|
||||||
? thinkingOptionId
|
? thinkingOptionId
|
||||||
@@ -1974,12 +1981,15 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
|
if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
|
||||||
this.config.thinkingOptionId = undefined;
|
this.config.thinkingOptionId = undefined;
|
||||||
} else if (isClaudeThinkingEffort(normalizedThinkingOptionId)) {
|
} else if (isClaudeThinkingOption(normalizedThinkingOptionId)) {
|
||||||
this.config.thinkingOptionId = normalizedThinkingOptionId;
|
this.config.thinkingOptionId = normalizedThinkingOptionId;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
|
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
|
||||||
}
|
}
|
||||||
this.queryRestartNeeded = true;
|
this.queryRestartNeeded = true;
|
||||||
|
if (this.activeForegroundTurnId || this.autonomousTurn) {
|
||||||
|
return SETTING_APPLIES_NEXT_TURN_NOTICE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFeature(featureId: string, value: unknown): Promise<void> {
|
async setFeature(featureId: string, value: unknown): Promise<void> {
|
||||||
@@ -2607,15 +2617,19 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private resolveThinkingConfig(): {
|
private resolveThinkingConfig(): {
|
||||||
thinking: ClaudeOptions["thinking"];
|
thinking: ClaudeOptions["thinking"];
|
||||||
effort: ClaudeOptions["effort"];
|
effort: ClaudeOptions["effort"];
|
||||||
|
ultracode: boolean;
|
||||||
} {
|
} {
|
||||||
const thinkingOptionId =
|
const thinkingOptionId =
|
||||||
this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
|
this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
|
||||||
? this.config.thinkingOptionId
|
? this.config.thinkingOptionId
|
||||||
: undefined;
|
: undefined;
|
||||||
if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) {
|
if (thinkingOptionId === "ultracode") {
|
||||||
return { thinking: { type: "adaptive" }, effort: thinkingOptionId };
|
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 {
|
private buildAppendedSystemPrompt(): string {
|
||||||
@@ -2641,10 +2655,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildOptions(): Promise<ClaudeOptions> {
|
private async buildOptions(): Promise<ClaudeOptions> {
|
||||||
const { thinking, effort } = this.resolveThinkingConfig();
|
const { thinking, effort, ultracode } = this.resolveThinkingConfig();
|
||||||
const appendedSystemPrompt = this.buildAppendedSystemPrompt();
|
const appendedSystemPrompt = this.buildAppendedSystemPrompt();
|
||||||
const extraClaudeOptions = this.config.extra?.claude;
|
const extraClaudeOptions = this.config.extra?.claude;
|
||||||
const fastModeOptions = this.buildFastModeOptions(extraClaudeOptions);
|
const settingsOptions = this.buildSettingsOptions(extraClaudeOptions, { ultracode });
|
||||||
const sdkEnv = this.buildSdkEnv(extraClaudeOptions);
|
const sdkEnv = this.buildSdkEnv(extraClaudeOptions);
|
||||||
assertClaudeAutoModeEligible(this.currentMode, sdkEnv);
|
assertClaudeAutoModeEligible(this.currentMode, sdkEnv);
|
||||||
|
|
||||||
@@ -2697,7 +2711,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
...(thinking ? { thinking } : {}),
|
...(thinking ? { thinking } : {}),
|
||||||
...(effort ? { effort } : {}),
|
...(effort ? { effort } : {}),
|
||||||
...extraClaudeOptions,
|
...extraClaudeOptions,
|
||||||
...fastModeOptions,
|
...settingsOptions,
|
||||||
...(this.persistSession === undefined ? {} : { persistSession: this.persistSession }),
|
...(this.persistSession === undefined ? {} : { persistSession: this.persistSession }),
|
||||||
env: sdkEnv,
|
env: sdkEnv,
|
||||||
};
|
};
|
||||||
@@ -2722,14 +2736,20 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildFastModeOptions(
|
private buildSettingsOptions(
|
||||||
extraClaudeOptions: Partial<ClaudeOptions> | undefined,
|
extraClaudeOptions: Partial<ClaudeOptions> | undefined,
|
||||||
|
input: { ultracode: boolean },
|
||||||
): Pick<ClaudeOptions, "settings"> | Record<string, never> {
|
): Pick<ClaudeOptions, "settings"> | Record<string, never> {
|
||||||
const fastMode = this.resolveFastModeSetting();
|
const fastMode = this.resolveFastModeSetting();
|
||||||
if (fastMode === null) {
|
if (fastMode === null && !input.ultracode) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return { settings: mergeClaudeSettings(extraClaudeOptions?.settings, { fastMode }) };
|
return {
|
||||||
|
settings: mergeClaudeSettings(extraClaudeOptions?.settings, {
|
||||||
|
...(fastMode === null ? {} : { fastMode }),
|
||||||
|
...(input.ultracode ? { ultracode: true } : {}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveFastModeSetting(): boolean | null {
|
private resolveFastModeSetting(): boolean | null {
|
||||||
|
|||||||
@@ -20,20 +20,25 @@ const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [
|
|||||||
{ id: "max", label: "Max" },
|
{ id: "max", label: "Max" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const CLAUDE_ULTRACODE_THINKING_OPTIONS = [
|
||||||
|
...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS,
|
||||||
|
{ id: "ultracode", label: "Ultracode" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
const CLAUDE_MODELS: AgentModelDefinition[] = [
|
const CLAUDE_MODELS: AgentModelDefinition[] = [
|
||||||
{
|
{
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
id: "claude-fable-5",
|
id: "claude-fable-5",
|
||||||
label: "Fable 5",
|
label: "Fable 5",
|
||||||
description: "Fable 5 · Most powerful model",
|
description: "Fable 5 · Most powerful model",
|
||||||
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
|
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
id: "claude-opus-4-8[1m]",
|
id: "claude-opus-4-8[1m]",
|
||||||
label: "Opus 4.8 1M",
|
label: "Opus 4.8 1M",
|
||||||
description: "Opus 4.8 with 1M context window",
|
description: "Opus 4.8 with 1M context window",
|
||||||
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
|
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
@@ -41,7 +46,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [
|
|||||||
label: "Opus 4.8",
|
label: "Opus 4.8",
|
||||||
description: "Opus 4.8 · Latest release",
|
description: "Opus 4.8 · Latest release",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
|
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: "claude",
|
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"])(
|
test.each(["auto_review", "guardian_subagent"])(
|
||||||
"parses %s thread/start response as auto-review mode",
|
"parses %s thread/start response as auto-review mode",
|
||||||
async (approvalsReviewer) => {
|
async (approvalsReviewer) => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type AgentPermissionRequest,
|
type AgentPermissionRequest,
|
||||||
type AgentPermissionResponse,
|
type AgentPermissionResponse,
|
||||||
type AgentPermissionResult,
|
type AgentPermissionResult,
|
||||||
|
type AgentProviderNotice,
|
||||||
type AgentPromptContentBlock,
|
type AgentPromptContentBlock,
|
||||||
type AgentPromptInput,
|
type AgentPromptInput,
|
||||||
type AgentRunOptions,
|
type AgentRunOptions,
|
||||||
@@ -92,6 +93,7 @@ import {
|
|||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
import { runProviderTurn } from "./provider-runner.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";
|
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
||||||
|
|
||||||
function assertChildWithPipes(
|
function assertChildWithPipes(
|
||||||
@@ -3654,10 +3656,13 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
return this.currentMode ?? null;
|
return this.currentMode ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMode(modeId: string): Promise<void> {
|
async setMode(modeId: string): Promise<void | AgentProviderNotice> {
|
||||||
validateCodexMode(modeId);
|
validateCodexMode(modeId);
|
||||||
this.currentMode = modeId;
|
this.currentMode = modeId;
|
||||||
this.cachedRuntimeInfo = null;
|
this.cachedRuntimeInfo = null;
|
||||||
|
if (this.activeForegroundTurnId) {
|
||||||
|
return SETTING_APPLIES_NEXT_TURN_NOTICE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setModel(modelId: string | null): Promise<void> {
|
async setModel(modelId: string | null): Promise<void> {
|
||||||
@@ -3669,10 +3674,13 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
this.cachedRuntimeInfo = null;
|
this.cachedRuntimeInfo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
|
async setThinkingOption(thinkingOptionId: string | null): Promise<void | AgentProviderNotice> {
|
||||||
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
|
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
|
||||||
this.refreshResolvedCollaborationMode();
|
this.refreshResolvedCollaborationMode();
|
||||||
this.cachedRuntimeInfo = null;
|
this.cachedRuntimeInfo = null;
|
||||||
|
if (this.activeForegroundTurnId) {
|
||||||
|
return SETTING_APPLIES_NEXT_TURN_NOTICE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFeature(featureId: string, value: unknown): Promise<void> {
|
async setFeature(featureId: string, value: unknown): Promise<void> {
|
||||||
|
|||||||
@@ -4672,14 +4672,17 @@ export class Session {
|
|||||||
this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
|
this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setAgentModeCommand({ agentManager: this.agentManager }, { agentId, modeId });
|
const result = await setAgentModeCommand(
|
||||||
|
{ agentManager: this.agentManager },
|
||||||
|
{ agentId, modeId },
|
||||||
|
);
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ agentId, modeId, requestId },
|
{ agentId, modeId, requestId },
|
||||||
"session: set_agent_mode_request success",
|
"session: set_agent_mode_request success",
|
||||||
);
|
);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "set_agent_mode_response",
|
type: "set_agent_mode_response",
|
||||||
payload: { requestId, agentId, accepted: true, error: null },
|
payload: { requestId, agentId, accepted: true, error: null, notice: result.notice },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.sessionLogger.error(
|
this.sessionLogger.error(
|
||||||
@@ -4808,14 +4811,14 @@ export class Session {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
|
const notice = await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ agentId, thinkingOptionId, requestId },
|
{ agentId, thinkingOptionId, requestId },
|
||||||
"session: set_agent_thinking_request success",
|
"session: set_agent_thinking_request success",
|
||||||
);
|
);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "set_agent_thinking_response",
|
type: "set_agent_thinking_response",
|
||||||
payload: { requestId, agentId, accepted: true, error: null },
|
payload: { requestId, agentId, accepted: true, error: null, notice },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.sessionLogger.error(
|
this.sessionLogger.error(
|
||||||
|
|||||||
Reference in New Issue
Block a user