Configure agent thinking from the CLI (#2533)
* feat(cli): update agent thinking from the CLI * fix(cli): harden agent thinking updates * feat(cli): configure thinking for schedules * fix(cli): report applied thinking updates * fix(cli): report current agent thinking state
This commit is contained in:
@@ -35,6 +35,19 @@ describe("canonical CLI surface", () => {
|
|||||||
expect(run?.helpInformation()).not.toContain("--detach");
|
expect(run?.helpInformation()).not.toContain("--detach");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("offers thinking configuration when running, updating, and scheduling agents", () => {
|
||||||
|
const cli = createCli();
|
||||||
|
const run = cli.commands.find((command) => command.name() === "run");
|
||||||
|
const agent = cli.commands.find((command) => command.name() === "agent");
|
||||||
|
const update = agent?.commands.find((command) => command.name() === "update");
|
||||||
|
const schedule = cli.commands.find((command) => command.name() === "schedule");
|
||||||
|
const scheduleCreate = schedule?.commands.find((command) => command.name() === "create");
|
||||||
|
|
||||||
|
expect(run?.helpInformation()).toContain("--thinking <id>");
|
||||||
|
expect(update?.helpInformation()).toContain("--thinking <id>");
|
||||||
|
expect(scheduleCreate?.helpInformation()).toContain("--thinking <id>");
|
||||||
|
});
|
||||||
|
|
||||||
it("offers opening an existing agent in the desktop app", () => {
|
it("offers opening an existing agent in the desktop app", () => {
|
||||||
const agent = createCli().commands.find((command) => command.name() === "agent");
|
const agent = createCli().commands.find((command) => command.name() === "agent");
|
||||||
const open = agent?.commands.find((command) => command.name() === "open");
|
const open = agent?.commands.find((command) => command.name() === "open");
|
||||||
|
|||||||
@@ -92,9 +92,10 @@ export function createAgentCommand(): Command {
|
|||||||
addJsonAndDaemonHostOptions(
|
addJsonAndDaemonHostOptions(
|
||||||
agent
|
agent
|
||||||
.command("update")
|
.command("update")
|
||||||
.description("Update an agent's metadata")
|
.description("Update an agent's settings or metadata")
|
||||||
.argument("<id>", "Agent ID (or prefix)")
|
.argument("<id>", "Agent ID (or prefix)")
|
||||||
.option("--name <name>", "Update the agent's display name")
|
.option("--name <name>", "Update the agent's display name")
|
||||||
|
.option("--thinking <id>", "Update the agent's thinking option ID")
|
||||||
.option(
|
.option(
|
||||||
"--label <label>",
|
"--label <label>",
|
||||||
"Add/set label(s) on the agent (can be used multiple times or comma-separated)",
|
"Add/set label(s) on the agent (can be used multiple times or comma-separated)",
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
|
||||||
|
import {
|
||||||
|
applyAgentChanges,
|
||||||
|
toAgentUpdateResult,
|
||||||
|
type AgentMetadataChanges,
|
||||||
|
type AgentUpdateClient,
|
||||||
|
} from "./update.js";
|
||||||
|
|
||||||
|
class RecordingAgentUpdateClient implements AgentUpdateClient {
|
||||||
|
readonly metadataUpdates: Array<{
|
||||||
|
agentId: string;
|
||||||
|
updates: AgentMetadataChanges;
|
||||||
|
}> = [];
|
||||||
|
readonly thinkingUpdates: Array<{ agentId: string; thinkingOptionId: string }> = [];
|
||||||
|
thinkingNotice: AgentProviderNotice | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly supportsThinkingUpdate = true) {}
|
||||||
|
|
||||||
|
getLastServerInfoMessage() {
|
||||||
|
return { features: { agentThinkingUpdate: this.supportsThinkingUpdate } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAgent(agentId: string, updates: AgentMetadataChanges): Promise<void> {
|
||||||
|
this.metadataUpdates.push({ agentId, updates });
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAgentThinkingOption(
|
||||||
|
agentId: string,
|
||||||
|
thinkingOptionId: string,
|
||||||
|
): Promise<AgentProviderNotice | null> {
|
||||||
|
this.thinkingUpdates.push({ agentId, thinkingOptionId });
|
||||||
|
return this.thinkingNotice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("applyAgentChanges", () => {
|
||||||
|
it("updates an agent's thinking without issuing an empty metadata update", async () => {
|
||||||
|
const client = new RecordingAgentUpdateClient();
|
||||||
|
|
||||||
|
const result = await applyAgentChanges(client, "agent-1", {
|
||||||
|
type: "thinking",
|
||||||
|
thinkingOptionId: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.metadataUpdates).toEqual([]);
|
||||||
|
expect(client.thinkingUpdates).toEqual([{ agentId: "agent-1", thinkingOptionId: "high" }]);
|
||||||
|
expect(result).toEqual({
|
||||||
|
notice: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a daemon that advertises thinking updates", async () => {
|
||||||
|
const client = new RecordingAgentUpdateClient(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
applyAgentChanges(client, "agent-1", { type: "thinking", thinkingOptionId: "high" }),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
code: "DAEMON_UPDATE_REQUIRED",
|
||||||
|
message: "Update the host to use agent thinking updates.",
|
||||||
|
});
|
||||||
|
expect(client.metadataUpdates).toEqual([]);
|
||||||
|
expect(client.thinkingUpdates).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the provider notice from a thinking update", async () => {
|
||||||
|
const client = new RecordingAgentUpdateClient();
|
||||||
|
client.thinkingNotice = {
|
||||||
|
type: "warning",
|
||||||
|
message: "Thinking changes apply to the next turn.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const notice = await applyAgentChanges(client, "agent-1", {
|
||||||
|
type: "thinking",
|
||||||
|
thinkingOptionId: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notice).toEqual({
|
||||||
|
notice: {
|
||||||
|
type: "warning",
|
||||||
|
message: "Thinking changes apply to the next turn.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toAgentUpdateResult", () => {
|
||||||
|
it("reports the current thinking option after a metadata update", () => {
|
||||||
|
const result = toAgentUpdateResult(
|
||||||
|
{
|
||||||
|
id: "agent-1",
|
||||||
|
title: "Renamed agent",
|
||||||
|
labels: { team: "platform" },
|
||||||
|
effectiveThinkingOptionId: "high",
|
||||||
|
},
|
||||||
|
{ notice: null },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
agentId: "agent-1",
|
||||||
|
name: "Renamed agent",
|
||||||
|
labels: "team=platform",
|
||||||
|
thinkingOptionId: "high",
|
||||||
|
noticeType: null,
|
||||||
|
notice: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
|
||||||
|
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
|
||||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||||
import type {
|
import type {
|
||||||
CommandOptions,
|
CommandOptions,
|
||||||
@@ -12,6 +14,9 @@ export interface AgentUpdateResult {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
labels: string;
|
labels: string;
|
||||||
|
thinkingOptionId: string | null;
|
||||||
|
noticeType: AgentProviderNotice["type"] | null;
|
||||||
|
notice: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Schema for update command output */
|
/** Schema for update command output */
|
||||||
@@ -21,17 +26,80 @@ export const updateSchema: OutputSchema<AgentUpdateResult> = {
|
|||||||
{ header: "AGENT ID", field: "agentId" },
|
{ header: "AGENT ID", field: "agentId" },
|
||||||
{ header: "NAME", field: "name" },
|
{ header: "NAME", field: "name" },
|
||||||
{ header: "LABELS", field: "labels" },
|
{ header: "LABELS", field: "labels" },
|
||||||
|
{ header: "THINKING", field: "thinkingOptionId" },
|
||||||
|
{ header: "NOTICE", field: "notice" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AgentUpdateOptions extends CommandOptions {
|
export interface AgentUpdateOptions extends CommandOptions {
|
||||||
name?: string;
|
name?: string;
|
||||||
label?: string[];
|
label?: string[];
|
||||||
|
thinking?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>;
|
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>;
|
||||||
|
|
||||||
|
export interface AgentMetadataChanges {
|
||||||
|
name?: string;
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentUpdateServerInfo {
|
||||||
|
features?: { agentThinkingUpdate?: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentUpdateClient {
|
||||||
|
getLastServerInfoMessage(): AgentUpdateServerInfo | null;
|
||||||
|
updateAgent(agentId: string, updates: AgentMetadataChanges): Promise<void>;
|
||||||
|
setAgentThinkingOption(
|
||||||
|
agentId: string,
|
||||||
|
thinkingOptionId: string,
|
||||||
|
): Promise<AgentProviderNotice | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentChanges =
|
||||||
|
| { type: "metadata"; updates: AgentMetadataChanges }
|
||||||
|
| { type: "thinking"; thinkingOptionId: string };
|
||||||
|
|
||||||
|
export interface AppliedAgentChanges {
|
||||||
|
notice: AgentProviderNotice | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toAgentUpdateResult(
|
||||||
|
agent: Pick<AgentSnapshotPayload, "id" | "title" | "labels" | "effectiveThinkingOptionId">,
|
||||||
|
appliedChanges: AppliedAgentChanges,
|
||||||
|
): AgentUpdateResult {
|
||||||
|
return {
|
||||||
|
agentId: agent.id,
|
||||||
|
name: agent.title,
|
||||||
|
labels: formatLabels(agent.labels),
|
||||||
|
thinkingOptionId: agent.effectiveThinkingOptionId ?? null,
|
||||||
|
noticeType: appliedChanges.notice?.type ?? null,
|
||||||
|
notice: appliedChanges.notice?.message ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyAgentChanges(
|
||||||
|
client: AgentUpdateClient,
|
||||||
|
agentId: string,
|
||||||
|
changes: AgentChanges,
|
||||||
|
): Promise<AppliedAgentChanges> {
|
||||||
|
if (changes.type === "thinking") {
|
||||||
|
// COMPAT(agentThinkingUpdate): added in v0.2.4, remove gate after 2027-01-28.
|
||||||
|
if (client.getLastServerInfoMessage()?.features?.agentThinkingUpdate !== true) {
|
||||||
|
throw {
|
||||||
|
code: "DAEMON_UPDATE_REQUIRED",
|
||||||
|
message: "Update the host to use agent thinking updates.",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
const notice = await client.setAgentThinkingOption(agentId, changes.thinkingOptionId);
|
||||||
|
return { notice };
|
||||||
|
}
|
||||||
|
await client.updateAgent(agentId, changes.updates);
|
||||||
|
return { notice: null };
|
||||||
|
}
|
||||||
|
|
||||||
function parseLabelOptions(labels: string[] | undefined): Record<string, string> {
|
function parseLabelOptions(labels: string[] | undefined): Record<string, string> {
|
||||||
const parsed: Record<string, string> = {};
|
const parsed: Record<string, string> = {};
|
||||||
if (!labels) {
|
if (!labels) {
|
||||||
@@ -81,6 +149,55 @@ function formatLabels(labels: Record<string, string>): string {
|
|||||||
return entries.map(([key, value]) => `${key}=${value}`).join(",");
|
return entries.map(([key, value]) => `${key}=${value}`).join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAgentChanges(options: AgentUpdateOptions): AgentChanges {
|
||||||
|
const name = options.name?.trim();
|
||||||
|
if (options.name !== undefined && !name) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_NAME",
|
||||||
|
message: "Name cannot be empty",
|
||||||
|
details: "Use --name <name> with a non-empty value",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = parseLabelOptions(options.label);
|
||||||
|
const thinkingOptionId = options.thinking?.trim();
|
||||||
|
if (options.thinking !== undefined && !thinkingOptionId) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_THINKING_OPTION",
|
||||||
|
message: "--thinking cannot be empty",
|
||||||
|
details:
|
||||||
|
'Provide a thinking option ID. Use "paseo provider models <provider> --thinking" to list valid IDs.',
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMetadataUpdates = Boolean(name) || Object.keys(labels).length > 0;
|
||||||
|
if (hasMetadataUpdates && thinkingOptionId) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_OPTIONS",
|
||||||
|
message: "--thinking cannot be combined with --name or --label",
|
||||||
|
details: "Run separate agent update commands for runtime settings and metadata.",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
if (!hasMetadataUpdates && !thinkingOptionId) {
|
||||||
|
throw {
|
||||||
|
code: "NO_CHANGES_PROVIDED",
|
||||||
|
message: "Nothing to update",
|
||||||
|
details: "Provide at least one of: --name <name>, --label <key=value>, --thinking <id>",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thinkingOptionId) {
|
||||||
|
return { type: "thinking", thinkingOptionId };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "metadata",
|
||||||
|
updates: {
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function runUpdateCommand(
|
export async function runUpdateCommand(
|
||||||
agentIdArg: string,
|
agentIdArg: string,
|
||||||
options: AgentUpdateOptions,
|
options: AgentUpdateOptions,
|
||||||
@@ -98,25 +215,7 @@ export async function runUpdateCommand(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = options.name?.trim();
|
const changes = parseAgentChanges(options);
|
||||||
if (options.name !== undefined && !name) {
|
|
||||||
const error: CommandError = {
|
|
||||||
code: "INVALID_NAME",
|
|
||||||
message: "Name cannot be empty",
|
|
||||||
details: "Use --name <name> with a non-empty value",
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = parseLabelOptions(options.label);
|
|
||||||
if (!name && Object.keys(labels).length === 0) {
|
|
||||||
const error: CommandError = {
|
|
||||||
code: "NO_CHANGES_PROVIDED",
|
|
||||||
message: "Nothing to update",
|
|
||||||
details: "Provide at least one of: --name <name>, --label <key=value>",
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
let client;
|
||||||
try {
|
try {
|
||||||
@@ -143,10 +242,7 @@ export async function runUpdateCommand(
|
|||||||
}
|
}
|
||||||
const agentId = fetchResult.agent.id;
|
const agentId = fetchResult.agent.id;
|
||||||
|
|
||||||
await client.updateAgent(agentId, {
|
const appliedChanges = await applyAgentChanges(client, agentId, changes);
|
||||||
...(name ? { name } : {}),
|
|
||||||
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedResult = await client.fetchAgent({ agentId });
|
const updatedResult = await client.fetchAgent({ agentId });
|
||||||
if (!updatedResult) {
|
if (!updatedResult) {
|
||||||
@@ -157,11 +253,7 @@ export async function runUpdateCommand(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: "single",
|
type: "single",
|
||||||
data: {
|
data: toAgentUpdateResult(updatedResult.agent, appliedChanges),
|
||||||
agentId,
|
|
||||||
name: updatedResult.agent.title,
|
|
||||||
labels: formatLabels(updatedResult.agent.labels),
|
|
||||||
},
|
|
||||||
schema: updateSchema,
|
schema: updateSchema,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions {
|
|||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
thinking?: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
@@ -40,6 +41,7 @@ export async function runCreateCommand(
|
|||||||
target: options.target,
|
target: options.target,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
mode: options.mode,
|
mode: options.mode,
|
||||||
|
thinking: options.thinking,
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
maxRuns: options.maxRuns,
|
maxRuns: options.maxRuns,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function createScheduleCommand(): Command {
|
|||||||
"--mode <mode>",
|
"--mode <mode>",
|
||||||
"Provider-specific mode (e.g. claude bypassPermissions, opencode build)",
|
"Provider-specific mode (e.g. claude bypassPermissions, opencode build)",
|
||||||
)
|
)
|
||||||
|
.option("--thinking <id>", "Thinking option ID for new-agent runs")
|
||||||
.option("--cwd <path>", "Working directory (default: current; required with --host)")
|
.option("--cwd <path>", "Working directory (default: current; required with --host)")
|
||||||
.option("--run-now", "Fire one immediate run on creation")
|
.option("--run-now", "Fire one immediate run on creation")
|
||||||
.option("--max-runs <n>", "Maximum number of runs")
|
.option("--max-runs <n>", "Maximum number of runs")
|
||||||
|
|||||||
@@ -116,6 +116,25 @@ describe("parseScheduleCreateInput first-run timing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parseScheduleCreateInput thinking", () => {
|
||||||
|
test("sets the thinking option for each scheduled new-agent run", () => {
|
||||||
|
const input = parseScheduleCreateInput({
|
||||||
|
...baseOptions,
|
||||||
|
cwd: "/project",
|
||||||
|
thinking: " high ",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(input.target).toEqual({
|
||||||
|
type: "new-agent",
|
||||||
|
config: {
|
||||||
|
provider: "claude",
|
||||||
|
cwd: "/project",
|
||||||
|
thinkingOptionId: "high",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("parseScheduleUpdateInput", () => {
|
describe("parseScheduleUpdateInput", () => {
|
||||||
test("rejects calls with no fields to update", () => {
|
test("rejects calls with no fields to update", () => {
|
||||||
expect(() => parseScheduleUpdateInput({ id: "abc" })).toThrow(
|
expect(() => parseScheduleUpdateInput({ id: "abc" })).toThrow(
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ function resolveScheduleTarget(args: {
|
|||||||
if (hasExplicitNewAgentOption) {
|
if (hasExplicitNewAgentOption) {
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_TARGET",
|
code: "INVALID_TARGET",
|
||||||
message: "--provider/--mode can only be used with a new-agent target",
|
message: "--provider/--mode/--thinking can only be used with a new-agent target",
|
||||||
details: "Use --target new-agent or omit --target to create a new agent schedule",
|
details: "Use --target new-agent or omit --target to create a new agent schedule",
|
||||||
} satisfies CommandError;
|
} satisfies CommandError;
|
||||||
}
|
}
|
||||||
@@ -144,6 +144,7 @@ export function parseScheduleCreateInput(options: {
|
|||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
thinking?: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
@@ -179,7 +180,15 @@ export function parseScheduleCreateInput(options: {
|
|||||||
|
|
||||||
const targetValue = options.target?.trim();
|
const targetValue = options.target?.trim();
|
||||||
const modeId = options.mode?.trim();
|
const modeId = options.mode?.trim();
|
||||||
const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined;
|
const thinkingOptionId = options.thinking?.trim();
|
||||||
|
if (options.thinking !== undefined && !thinkingOptionId) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_THINKING_OPTION",
|
||||||
|
message: "--thinking cannot be empty",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
const hasExplicitNewAgentOption =
|
||||||
|
options.provider !== undefined || options.mode !== undefined || options.thinking !== undefined;
|
||||||
const createNewAgentTarget = (): ScheduleTarget => {
|
const createNewAgentTarget = (): ScheduleTarget => {
|
||||||
const resolvedProviderModel = resolveProviderAndModel({
|
const resolvedProviderModel = resolveProviderAndModel({
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
@@ -191,6 +200,7 @@ export function parseScheduleCreateInput(options: {
|
|||||||
cwd: cwdInput ?? process.cwd(),
|
cwd: cwdInput ?? process.cwd(),
|
||||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||||
...(modeId ? { modeId } : {}),
|
...(modeId ? { modeId } : {}),
|
||||||
|
...(thinkingOptionId ? { thinkingOptionId } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ try {
|
|||||||
assert.strictEqual(result.exitCode, 0, "agent update --help should exit 0");
|
assert.strictEqual(result.exitCode, 0, "agent update --help should exit 0");
|
||||||
assert(result.stdout.includes("--name"), "help should mention --name flag");
|
assert(result.stdout.includes("--name"), "help should mention --name flag");
|
||||||
assert(result.stdout.includes("--label"), "help should mention --label flag");
|
assert(result.stdout.includes("--label"), "help should mention --label flag");
|
||||||
|
assert(result.stdout.includes("--thinking"), "help should mention --thinking flag");
|
||||||
assert(result.stdout.includes("--host"), "help should mention --host option");
|
assert(result.stdout.includes("--host"), "help should mention --host option");
|
||||||
assert(result.stdout.includes("<id>"), "help should mention required id argument");
|
assert(result.stdout.includes("<id>"), "help should mention required id argument");
|
||||||
console.log("✓ agent update --help shows options\n");
|
console.log("✓ agent update --help shows options\n");
|
||||||
@@ -103,6 +104,30 @@ try {
|
|||||||
assert(result.stdout.includes("update"), "help should mention update subcommand");
|
assert(result.stdout.includes("update"), "help should mention update subcommand");
|
||||||
console.log("✓ agent --help shows update subcommand\n");
|
console.log("✓ agent --help shows update subcommand\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test 7: agent update accepts thinking as an update field
|
||||||
|
{
|
||||||
|
console.log("Test 7: agent update accepts thinking as an update field");
|
||||||
|
const result =
|
||||||
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --thinking high`.nothrow();
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
assert(!output.includes("Nothing to update"), "should treat --thinking as an update field");
|
||||||
|
console.log("✓ agent update accepts thinking as an update field\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 8: thinking cannot be combined with metadata updates
|
||||||
|
{
|
||||||
|
console.log("Test 8: thinking cannot be combined with metadata updates");
|
||||||
|
const result =
|
||||||
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --name renamed --thinking high`.nothrow();
|
||||||
|
assert.notStrictEqual(result.exitCode, 0, "should reject a combined update");
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
assert(
|
||||||
|
output.includes("--thinking cannot be combined with --name or --label"),
|
||||||
|
"should explain that thinking and metadata updates are separate operations",
|
||||||
|
);
|
||||||
|
console.log("✓ thinking cannot be combined with metadata updates\n");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temp directory
|
// Clean up temp directory
|
||||||
await rm(paseoHome, { recursive: true, force: true });
|
await rm(paseoHome, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ try {
|
|||||||
"10m",
|
"10m",
|
||||||
"--provider",
|
"--provider",
|
||||||
"codex/gpt-5.4",
|
"codex/gpt-5.4",
|
||||||
|
"--thinking",
|
||||||
|
"high",
|
||||||
"--json",
|
"--json",
|
||||||
],
|
],
|
||||||
{ timeout: 30000 },
|
{ timeout: 30000 },
|
||||||
@@ -113,6 +115,7 @@ try {
|
|||||||
const inspectedJson = JSON.parse(inspected.stdout);
|
const inspectedJson = JSON.parse(inspected.stdout);
|
||||||
assert.strictEqual(inspectedJson.target.config.provider, "codex");
|
assert.strictEqual(inspectedJson.target.config.provider, "codex");
|
||||||
assert.strictEqual(inspectedJson.target.config.model, "gpt-5.4");
|
assert.strictEqual(inspectedJson.target.config.model, "gpt-5.4");
|
||||||
|
assert.strictEqual(inspectedJson.target.config.thinkingOptionId, "high");
|
||||||
|
|
||||||
const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]);
|
const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]);
|
||||||
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
|
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
|
||||||
|
|||||||
@@ -168,4 +168,14 @@ describe("project command-center protocol", () => {
|
|||||||
expect(parsed.features?.projectGithubClone).toBeUndefined();
|
expect(parsed.features?.projectGithubClone).toBeUndefined();
|
||||||
expect(parsed.features?.projectCreateDirectory).toBeUndefined();
|
expect(parsed.features?.projectCreateDirectory).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses the agent thinking update capability", () => {
|
||||||
|
const parsed = parseServerInfoStatusPayload({
|
||||||
|
status: "server_info",
|
||||||
|
serverId: "server-new",
|
||||||
|
features: { agentThinkingUpdate: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed.features?.agentThinkingUpdate).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2792,6 +2792,8 @@ export const ServerInfoStatusPayloadSchema = z
|
|||||||
providerUsageList: z.boolean().optional(),
|
providerUsageList: z.boolean().optional(),
|
||||||
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
||||||
agentDetach: z.boolean().optional(),
|
agentDetach: z.boolean().optional(),
|
||||||
|
// COMPAT(agentThinkingUpdate): added in v0.2.4, remove gate after 2027-01-28.
|
||||||
|
agentThinkingUpdate: z.boolean().optional(),
|
||||||
// COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
|
// COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
|
||||||
daemonDiagnostics: z.boolean().optional(),
|
daemonDiagnostics: z.boolean().optional(),
|
||||||
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
|
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
|
||||||
|
|||||||
@@ -1540,6 +1540,8 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
providerUsageList: true,
|
providerUsageList: true,
|
||||||
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
|
||||||
agentDetach: true,
|
agentDetach: true,
|
||||||
|
// COMPAT(agentThinkingUpdate): added in v0.2.4, remove gate after 2027-01-28.
|
||||||
|
agentThinkingUpdate: true,
|
||||||
// COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
|
// COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
|
||||||
daemonDiagnostics: true,
|
daemonDiagnostics: true,
|
||||||
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
|
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
|
||||||
|
|||||||
Reference in New Issue
Block a user