Add --mode to schedule and loop CLI, default background runs to unattended mode
Plumb --mode/--verify-mode through paseo schedule create and paseo loop run. Mark each provider's unattended mode (claude bypassPermissions, codex/opencode full-access, copilot autopilot) so loop and schedule services pick it by default when no explicit modeId is given.
This commit is contained in:
@@ -21,8 +21,10 @@ export interface LoopRunRow {
|
|||||||
export interface LoopRunOptions extends CommandOptions {
|
export interface LoopRunOptions extends CommandOptions {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
mode?: string;
|
||||||
verifyProvider?: string;
|
verifyProvider?: string;
|
||||||
verifyModel?: string;
|
verifyModel?: string;
|
||||||
|
verifyMode?: string;
|
||||||
verify?: string;
|
verify?: string;
|
||||||
verifyCheck?: string[];
|
verifyCheck?: string[];
|
||||||
archive?: boolean;
|
archive?: boolean;
|
||||||
@@ -48,8 +50,13 @@ export function addLoopRunOptions(command: Command): Command {
|
|||||||
.argument("<prompt>", "Prompt for each fresh worker iteration")
|
.argument("<prompt>", "Prompt for each fresh worker iteration")
|
||||||
.option("--provider <provider>", "Default provider for worker and verifier agents")
|
.option("--provider <provider>", "Default provider for worker and verifier agents")
|
||||||
.option("--model <model>", "Default model for worker and verifier agents")
|
.option("--model <model>", "Default model for worker and verifier agents")
|
||||||
|
.option(
|
||||||
|
"--mode <mode>",
|
||||||
|
"Provider-specific mode for the worker agent (e.g. claude bypassPermissions, opencode build)",
|
||||||
|
)
|
||||||
.option("--verify-provider <provider>", "Provider for the verifier agent")
|
.option("--verify-provider <provider>", "Provider for the verifier agent")
|
||||||
.option("--verify-model <model>", "Model for the verifier agent")
|
.option("--verify-model <model>", "Model for the verifier agent")
|
||||||
|
.option("--verify-mode <mode>", "Provider-specific mode for the verifier agent")
|
||||||
.option("--verify <prompt>", "Verifier agent prompt")
|
.option("--verify <prompt>", "Verifier agent prompt")
|
||||||
.option(
|
.option(
|
||||||
"--verify-check <command>",
|
"--verify-check <command>",
|
||||||
@@ -116,6 +123,10 @@ export function buildLoopRunInput(prompt: string, options: LoopRunOptions): Loop
|
|||||||
result.model = options.model.trim();
|
result.model = options.model.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.mode?.trim()) {
|
||||||
|
result.modeId = options.mode.trim();
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve verifier provider/model
|
// Resolve verifier provider/model
|
||||||
if (options.verifyProvider) {
|
if (options.verifyProvider) {
|
||||||
const { provider, model } = resolveProviderAndModel({ provider: options.verifyProvider });
|
const { provider, model } = resolveProviderAndModel({ provider: options.verifyProvider });
|
||||||
@@ -130,6 +141,10 @@ export function buildLoopRunInput(prompt: string, options: LoopRunOptions): Loop
|
|||||||
result.verifierModel = options.verifyModel.trim();
|
result.verifierModel = options.verifyModel.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.verifyMode?.trim()) {
|
||||||
|
result.verifierModeId = options.verifyMode.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (verifyPrompt) result.verifyPrompt = verifyPrompt;
|
if (verifyPrompt) result.verifyPrompt = verifyPrompt;
|
||||||
if (options.verifyCheck && options.verifyCheck.length > 0) {
|
if (options.verifyCheck && options.verifyCheck.length > 0) {
|
||||||
result.verifyChecks = options.verifyCheck;
|
result.verifyChecks = options.verifyCheck;
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ export interface LoopRecord {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
|
modeId: string | null;
|
||||||
workerProvider: string | null;
|
workerProvider: string | null;
|
||||||
workerModel: string | null;
|
workerModel: string | null;
|
||||||
verifierProvider: string | null;
|
verifierProvider: string | null;
|
||||||
verifierModel: string | null;
|
verifierModel: string | null;
|
||||||
|
verifierModeId: string | null;
|
||||||
verifyPrompt: string | null;
|
verifyPrompt: string | null;
|
||||||
verifyChecks: string[];
|
verifyChecks: string[];
|
||||||
archive: boolean;
|
archive: boolean;
|
||||||
@@ -124,10 +126,12 @@ export interface LoopRunInput {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
modeId?: string;
|
||||||
workerProvider?: string;
|
workerProvider?: string;
|
||||||
workerModel?: string;
|
workerModel?: string;
|
||||||
verifierProvider?: string;
|
verifierProvider?: string;
|
||||||
verifierModel?: string;
|
verifierModel?: string;
|
||||||
|
verifierModeId?: string;
|
||||||
verifyPrompt?: string;
|
verifyPrompt?: string;
|
||||||
verifyChecks?: string[];
|
verifyChecks?: string[];
|
||||||
archive?: boolean;
|
archive?: boolean;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions {
|
|||||||
name?: string;
|
name?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
|
mode?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
}
|
}
|
||||||
@@ -32,6 +33,7 @@ export async function runCreateCommand(
|
|||||||
name: options.name,
|
name: options.name,
|
||||||
target: options.target,
|
target: options.target,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
|
mode: options.mode,
|
||||||
maxRuns: options.maxRuns,
|
maxRuns: options.maxRuns,
|
||||||
expiresIn: options.expiresIn,
|
expiresIn: options.expiresIn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export function createScheduleCommand(): Command {
|
|||||||
"--provider <provider>",
|
"--provider <provider>",
|
||||||
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
|
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
|
||||||
)
|
)
|
||||||
|
.option(
|
||||||
|
"--mode <mode>",
|
||||||
|
"Provider-specific mode (e.g. claude bypassPermissions, opencode build)",
|
||||||
|
)
|
||||||
.option("--max-runs <n>", "Maximum number of runs")
|
.option("--max-runs <n>", "Maximum number of runs")
|
||||||
.option("--expires-in <duration>", "Time to live for the schedule"),
|
.option("--expires-in <duration>", "Time to live for the schedule"),
|
||||||
).action(withOutput(runCreateCommand));
|
).action(withOutput(runCreateCommand));
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ export function formatDurationMs(durationMs: number): string {
|
|||||||
|
|
||||||
function resolveScheduleTarget(args: {
|
function resolveScheduleTarget(args: {
|
||||||
targetValue: string | undefined;
|
targetValue: string | undefined;
|
||||||
hasExplicitProviderSelection: boolean;
|
hasExplicitNewAgentOption: boolean;
|
||||||
createNewAgentTarget: () => ScheduleTarget;
|
createNewAgentTarget: () => ScheduleTarget;
|
||||||
}): ScheduleTarget {
|
}): ScheduleTarget {
|
||||||
const { targetValue, hasExplicitProviderSelection, createNewAgentTarget } = args;
|
const { targetValue, hasExplicitNewAgentOption, createNewAgentTarget } = args;
|
||||||
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
|
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
|
||||||
|
|
||||||
if (!targetValue) {
|
if (!targetValue) {
|
||||||
if (currentAgentId && !hasExplicitProviderSelection) {
|
if (currentAgentId && !hasExplicitNewAgentOption) {
|
||||||
return { type: "self", agentId: currentAgentId };
|
return { type: "self", agentId: currentAgentId };
|
||||||
}
|
}
|
||||||
return createNewAgentTarget();
|
return createNewAgentTarget();
|
||||||
@@ -102,10 +102,10 @@ function resolveScheduleTarget(args: {
|
|||||||
return createNewAgentTarget();
|
return createNewAgentTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasExplicitProviderSelection) {
|
if (hasExplicitNewAgentOption) {
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_TARGET",
|
code: "INVALID_TARGET",
|
||||||
message: "--provider can only be used with a new-agent target",
|
message: "--provider/--mode 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;
|
||||||
}
|
}
|
||||||
@@ -130,6 +130,7 @@ export function parseScheduleCreateInput(options: {
|
|||||||
name?: string;
|
name?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
|
mode?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
}): CreateScheduleInput {
|
}): CreateScheduleInput {
|
||||||
@@ -154,7 +155,8 @@ export function parseScheduleCreateInput(options: {
|
|||||||
: { type: "cron", expression: options.cron!.trim() };
|
: { type: "cron", expression: options.cron!.trim() };
|
||||||
|
|
||||||
const targetValue = options.target?.trim();
|
const targetValue = options.target?.trim();
|
||||||
const hasExplicitProviderSelection = options.provider !== undefined;
|
const modeId = options.mode?.trim();
|
||||||
|
const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined;
|
||||||
const createNewAgentTarget = (): ScheduleTarget => {
|
const createNewAgentTarget = (): ScheduleTarget => {
|
||||||
const resolvedProviderModel = resolveProviderAndModel({
|
const resolvedProviderModel = resolveProviderAndModel({
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
@@ -165,12 +167,13 @@ export function parseScheduleCreateInput(options: {
|
|||||||
provider: resolvedProviderModel.provider,
|
provider: resolvedProviderModel.provider,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||||
|
...(modeId ? { modeId } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const target = resolveScheduleTarget({
|
const target = resolveScheduleTarget({
|
||||||
targetValue,
|
targetValue,
|
||||||
hasExplicitProviderSelection,
|
hasExplicitNewAgentOption,
|
||||||
createNewAgentTarget,
|
createNewAgentTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -475,8 +475,10 @@ export interface RunLoopOptions {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
modeId?: string;
|
||||||
verifierProvider?: string;
|
verifierProvider?: string;
|
||||||
verifierModel?: string;
|
verifierModel?: string;
|
||||||
|
verifierModeId?: string;
|
||||||
verifyPrompt?: string | null;
|
verifyPrompt?: string | null;
|
||||||
verifyChecks?: string[];
|
verifyChecks?: string[];
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
@@ -3751,8 +3753,10 @@ export class DaemonClient {
|
|||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
...(options.provider ? { provider: options.provider } : {}),
|
...(options.provider ? { provider: options.provider } : {}),
|
||||||
...(options.model ? { model: options.model } : {}),
|
...(options.model ? { model: options.model } : {}),
|
||||||
|
...(options.modeId ? { modeId: options.modeId } : {}),
|
||||||
...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}),
|
...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}),
|
||||||
...(options.verifierModel ? { verifierModel: options.verifierModel } : {}),
|
...(options.verifierModel ? { verifierModel: options.verifierModel } : {}),
|
||||||
|
...(options.verifierModeId ? { verifierModeId: options.verifierModeId } : {}),
|
||||||
...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}),
|
...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}),
|
||||||
...(options.verifyChecks && options.verifyChecks.length > 0
|
...(options.verifyChecks && options.verifyChecks.length > 0
|
||||||
? { verifyChecks: options.verifyChecks }
|
? { verifyChecks: options.verifyChecks }
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ export interface AgentModeVisuals {
|
|||||||
colorTier: AgentModeColorTier;
|
colorTier: AgentModeColorTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> & AgentModeVisuals;
|
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> &
|
||||||
|
AgentModeVisuals & {
|
||||||
|
isUnattended?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: `modes` should not be static. Providers (especially ACP) report their
|
// TODO: `modes` should not be static. Providers (especially ACP) report their
|
||||||
// own modes at runtime via session/new. We should fetch modes from the provider
|
// own modes at runtime via session/new. We should fetch modes from the provider
|
||||||
@@ -55,6 +58,7 @@ const CLAUDE_MODES: AgentProviderModeDefinition[] = [
|
|||||||
description: "Skip all permission prompts (use with caution)",
|
description: "Skip all permission prompts (use with caution)",
|
||||||
icon: "ShieldAlert",
|
icon: "ShieldAlert",
|
||||||
colorTier: "dangerous",
|
colorTier: "dangerous",
|
||||||
|
isUnattended: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -72,6 +76,7 @@ const CODEX_MODES: AgentProviderModeDefinition[] = [
|
|||||||
description: "Edit files, run commands, and access the network without additional prompts.",
|
description: "Edit files, run commands, and access the network without additional prompts.",
|
||||||
icon: "ShieldAlert",
|
icon: "ShieldAlert",
|
||||||
colorTier: "dangerous",
|
colorTier: "dangerous",
|
||||||
|
isUnattended: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@ const COPILOT_MODES: AgentProviderModeDefinition[] = [
|
|||||||
description: "Autonomous mode that runs until task completion without user interaction",
|
description: "Autonomous mode that runs until task completion without user interaction",
|
||||||
icon: "ShieldOff",
|
icon: "ShieldOff",
|
||||||
colorTier: "dangerous",
|
colorTier: "dangerous",
|
||||||
|
isUnattended: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -113,6 +119,7 @@ const OPENCODE_MODES: AgentProviderModeDefinition[] = [
|
|||||||
description: "Automatically approves all tool permission prompts for the session",
|
description: "Automatically approves all tool permission prompts for the session",
|
||||||
icon: "ShieldAlert",
|
icon: "ShieldAlert",
|
||||||
colorTier: "dangerous",
|
colorTier: "dangerous",
|
||||||
|
isUnattended: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "plan",
|
id: "plan",
|
||||||
@@ -222,6 +229,17 @@ export function isValidAgentProvider(
|
|||||||
return Array.isArray(validIds) ? validIds.includes(value) : new Set(validIds).has(value);
|
return Array.isArray(validIds) ? validIds.includes(value) : new Set(validIds).has(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUnattendedModeId(
|
||||||
|
provider: string,
|
||||||
|
definitions: AgentProviderDefinition[] = [
|
||||||
|
...AGENT_PROVIDER_DEFINITIONS,
|
||||||
|
...DEV_AGENT_PROVIDER_DEFINITIONS,
|
||||||
|
],
|
||||||
|
): string | undefined {
|
||||||
|
const definition = definitions.find((entry) => entry.id === provider);
|
||||||
|
return definition?.modes.find((mode) => mode.isUnattended)?.id;
|
||||||
|
}
|
||||||
|
|
||||||
export function getModeVisuals(
|
export function getModeVisuals(
|
||||||
provider: string,
|
provider: string,
|
||||||
modeId: string,
|
modeId: string,
|
||||||
|
|||||||
@@ -432,6 +432,80 @@ describe("LoopService", () => {
|
|||||||
expect(logs.entries.some((entry) => entry.text.includes("Verifier result"))).toBe(true);
|
expect(logs.entries.some((entry) => entry.text.includes("Verifier result"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("defaults worker and verifier modeId to provider's unattended mode", async () => {
|
||||||
|
const workerConfigs: AgentSessionConfig[] = [];
|
||||||
|
const verifierConfigs: AgentSessionConfig[] = [];
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: {
|
||||||
|
claude: new ScriptedAgentClient("claude", {
|
||||||
|
async onRun({ config }) {
|
||||||
|
if (config.title?.includes("worker")) {
|
||||||
|
workerConfigs.push(config);
|
||||||
|
writeFileSync(path.join(workspaceDir, "done.txt"), "ok");
|
||||||
|
return "created done.txt";
|
||||||
|
}
|
||||||
|
verifierConfigs.push(config);
|
||||||
|
return '{"passed":true,"reason":"ok"}';
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
const service = new LoopService({ paseoHome, agentManager: manager, logger });
|
||||||
|
await service.initialize();
|
||||||
|
|
||||||
|
const loop = await service.runLoop({
|
||||||
|
prompt: "Create done.txt",
|
||||||
|
cwd: workspaceDir,
|
||||||
|
verifyPrompt: "Confirm that done.txt exists in the workspace.",
|
||||||
|
maxIterations: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForLoopCompletion(service, loop.id);
|
||||||
|
|
||||||
|
expect(workerConfigs[0]?.modeId).toBe("bypassPermissions");
|
||||||
|
expect(verifierConfigs[0]?.modeId).toBe("bypassPermissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explicit modeId wins over unattended default", async () => {
|
||||||
|
const workerConfigs: AgentSessionConfig[] = [];
|
||||||
|
const verifierConfigs: AgentSessionConfig[] = [];
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: {
|
||||||
|
claude: new ScriptedAgentClient("claude", {
|
||||||
|
async onRun({ config }) {
|
||||||
|
if (config.title?.includes("worker")) {
|
||||||
|
workerConfigs.push(config);
|
||||||
|
writeFileSync(path.join(workspaceDir, "done.txt"), "ok");
|
||||||
|
return "created done.txt";
|
||||||
|
}
|
||||||
|
verifierConfigs.push(config);
|
||||||
|
return '{"passed":true,"reason":"ok"}';
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
const service = new LoopService({ paseoHome, agentManager: manager, logger });
|
||||||
|
await service.initialize();
|
||||||
|
|
||||||
|
const loop = await service.runLoop({
|
||||||
|
prompt: "Create done.txt",
|
||||||
|
cwd: workspaceDir,
|
||||||
|
modeId: "acceptEdits",
|
||||||
|
verifierModeId: "plan",
|
||||||
|
verifyPrompt: "Confirm that done.txt exists in the workspace.",
|
||||||
|
maxIterations: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForLoopCompletion(service, loop.id);
|
||||||
|
|
||||||
|
expect(workerConfigs[0]?.modeId).toBe("acceptEdits");
|
||||||
|
expect(verifierConfigs[0]?.modeId).toBe("plan");
|
||||||
|
});
|
||||||
|
|
||||||
test("stops a running loop and cancels the active worker", async () => {
|
test("stops a running loop and cancels the active worker", async () => {
|
||||||
let release: (() => void) | null = null;
|
let release: (() => void) | null = null;
|
||||||
const blocker = new Promise<void>((resolve) => {
|
const blocker = new Promise<void>((resolve) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
AgentProvider,
|
AgentProvider,
|
||||||
} from "./agent/agent-sdk-types.js";
|
} from "./agent/agent-sdk-types.js";
|
||||||
import { execCommand, platformShell } from "../utils/spawn.js";
|
import { execCommand, platformShell } from "../utils/spawn.js";
|
||||||
|
import { getUnattendedModeId } from "./agent/provider-manifest.js";
|
||||||
|
|
||||||
const LOOP_ID_LENGTH = 8;
|
const LOOP_ID_LENGTH = 8;
|
||||||
const DEFAULT_LOOP_PROVIDER: AgentProvider = "claude";
|
const DEFAULT_LOOP_PROVIDER: AgentProvider = "claude";
|
||||||
@@ -71,10 +72,12 @@ const LoopRecordSchema = z.object({
|
|||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
provider: z.string(),
|
provider: z.string(),
|
||||||
model: z.string().nullable(),
|
model: z.string().nullable(),
|
||||||
|
modeId: z.string().nullable().default(null),
|
||||||
workerProvider: z.string().nullable(),
|
workerProvider: z.string().nullable(),
|
||||||
workerModel: z.string().nullable(),
|
workerModel: z.string().nullable(),
|
||||||
verifierProvider: z.string().nullable(),
|
verifierProvider: z.string().nullable(),
|
||||||
verifierModel: z.string().nullable(),
|
verifierModel: z.string().nullable(),
|
||||||
|
verifierModeId: z.string().nullable().default(null),
|
||||||
verifyPrompt: z.string().nullable(),
|
verifyPrompt: z.string().nullable(),
|
||||||
verifyChecks: z.array(z.string()),
|
verifyChecks: z.array(z.string()),
|
||||||
archive: z.boolean(),
|
archive: z.boolean(),
|
||||||
@@ -109,10 +112,12 @@ export interface LoopRunOptions {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
provider?: AgentProvider;
|
provider?: AgentProvider;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
modeId?: string;
|
||||||
workerProvider?: AgentProvider;
|
workerProvider?: AgentProvider;
|
||||||
workerModel?: string;
|
workerModel?: string;
|
||||||
verifierProvider?: AgentProvider;
|
verifierProvider?: AgentProvider;
|
||||||
verifierModel?: string;
|
verifierModel?: string;
|
||||||
|
verifierModeId?: string;
|
||||||
verifyPrompt?: string;
|
verifyPrompt?: string;
|
||||||
verifyChecks?: string[];
|
verifyChecks?: string[];
|
||||||
archive?: boolean;
|
archive?: boolean;
|
||||||
@@ -376,10 +381,12 @@ export class LoopService {
|
|||||||
cwd: path.resolve(input.cwd),
|
cwd: path.resolve(input.cwd),
|
||||||
provider: input.provider ?? DEFAULT_LOOP_PROVIDER,
|
provider: input.provider ?? DEFAULT_LOOP_PROVIDER,
|
||||||
model: normalizePrompt(input.model, "model"),
|
model: normalizePrompt(input.model, "model"),
|
||||||
|
modeId: normalizePrompt(input.modeId, "modeId"),
|
||||||
workerProvider: input.workerProvider ?? null,
|
workerProvider: input.workerProvider ?? null,
|
||||||
workerModel: normalizePrompt(input.workerModel, "workerModel"),
|
workerModel: normalizePrompt(input.workerModel, "workerModel"),
|
||||||
verifierProvider: input.verifierProvider ?? null,
|
verifierProvider: input.verifierProvider ?? null,
|
||||||
verifierModel: normalizePrompt(input.verifierModel, "verifierModel"),
|
verifierModel: normalizePrompt(input.verifierModel, "verifierModel"),
|
||||||
|
verifierModeId: normalizePrompt(input.verifierModeId, "verifierModeId"),
|
||||||
verifyPrompt,
|
verifyPrompt,
|
||||||
verifyChecks,
|
verifyChecks,
|
||||||
archive: input.archive ?? false,
|
archive: input.archive ?? false,
|
||||||
@@ -789,10 +796,12 @@ export class LoopService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildWorkerConfig(loop: LoopRecord, iteration: LoopIterationRecord): AgentSessionConfig {
|
private buildWorkerConfig(loop: LoopRecord, iteration: LoopIterationRecord): AgentSessionConfig {
|
||||||
|
const provider = loop.workerProvider ?? loop.provider;
|
||||||
return {
|
return {
|
||||||
provider: loop.workerProvider ?? loop.provider,
|
provider,
|
||||||
cwd: loop.cwd,
|
cwd: loop.cwd,
|
||||||
model: loop.workerModel ?? loop.model ?? undefined,
|
model: loop.workerModel ?? loop.model ?? undefined,
|
||||||
|
modeId: loop.modeId ?? getUnattendedModeId(provider),
|
||||||
title: buildWorkerTitle(loop, iteration.index),
|
title: buildWorkerTitle(loop, iteration.index),
|
||||||
internal: true,
|
internal: true,
|
||||||
};
|
};
|
||||||
@@ -802,10 +811,12 @@ export class LoopService {
|
|||||||
loop: LoopRecord,
|
loop: LoopRecord,
|
||||||
iteration: LoopIterationRecord,
|
iteration: LoopIterationRecord,
|
||||||
): AgentSessionConfig {
|
): AgentSessionConfig {
|
||||||
|
const provider = loop.verifierProvider ?? loop.provider;
|
||||||
return {
|
return {
|
||||||
provider: loop.verifierProvider ?? loop.provider,
|
provider,
|
||||||
cwd: loop.cwd,
|
cwd: loop.cwd,
|
||||||
model: loop.verifierModel ?? loop.model ?? undefined,
|
model: loop.verifierModel ?? loop.model ?? undefined,
|
||||||
|
modeId: loop.verifierModeId ?? loop.modeId ?? getUnattendedModeId(provider),
|
||||||
title: buildVerifierTitle(loop, iteration.index),
|
title: buildVerifierTitle(loop, iteration.index),
|
||||||
internal: true,
|
internal: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ export const LoopRecordSchema = z.object({
|
|||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
model: z.string().nullable(),
|
model: z.string().nullable(),
|
||||||
|
modeId: z.string().nullable().default(null),
|
||||||
workerProvider: AgentProviderSchema.nullable(),
|
workerProvider: AgentProviderSchema.nullable(),
|
||||||
workerModel: z.string().nullable(),
|
workerModel: z.string().nullable(),
|
||||||
verifierProvider: AgentProviderSchema.nullable(),
|
verifierProvider: AgentProviderSchema.nullable(),
|
||||||
verifierModel: z.string().nullable(),
|
verifierModel: z.string().nullable(),
|
||||||
|
verifierModeId: z.string().nullable().default(null),
|
||||||
verifyPrompt: z.string().nullable(),
|
verifyPrompt: z.string().nullable(),
|
||||||
verifyChecks: z.array(z.string()),
|
verifyChecks: z.array(z.string()),
|
||||||
archive: z.boolean(),
|
archive: z.boolean(),
|
||||||
@@ -89,10 +91,12 @@ export const LoopRunRequestSchema = z.object({
|
|||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
provider: AgentProviderSchema.optional(),
|
provider: AgentProviderSchema.optional(),
|
||||||
model: z.string().trim().min(1).optional(),
|
model: z.string().trim().min(1).optional(),
|
||||||
|
modeId: z.string().trim().min(1).optional(),
|
||||||
workerProvider: AgentProviderSchema.optional(),
|
workerProvider: AgentProviderSchema.optional(),
|
||||||
workerModel: z.string().trim().min(1).optional(),
|
workerModel: z.string().trim().min(1).optional(),
|
||||||
verifierProvider: AgentProviderSchema.optional(),
|
verifierProvider: AgentProviderSchema.optional(),
|
||||||
verifierModel: z.string().trim().min(1).optional(),
|
verifierModel: z.string().trim().min(1).optional(),
|
||||||
|
verifierModeId: z.string().trim().min(1).optional(),
|
||||||
verifyPrompt: z.string().trim().min(1).optional(),
|
verifyPrompt: z.string().trim().min(1).optional(),
|
||||||
verifyChecks: z.array(z.string().trim().min(1)).optional(),
|
verifyChecks: z.array(z.string().trim().min(1)).optional(),
|
||||||
archive: z.boolean().optional(),
|
archive: z.boolean().optional(),
|
||||||
|
|||||||
@@ -176,6 +176,44 @@ describe("ScheduleService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("defaults new-agent modeId to provider's unattended mode", async () => {
|
||||||
|
const manager = new AgentManager({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
clients: createTestAgentClients(),
|
||||||
|
registry: agentStorage,
|
||||||
|
});
|
||||||
|
const service = new ScheduleService({
|
||||||
|
paseoHome: tempDir,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
agentManager: manager,
|
||||||
|
agentStorage,
|
||||||
|
now: () => now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await service.create({
|
||||||
|
prompt: "Respond with exactly hello",
|
||||||
|
cadence: { type: "every", everyMs: 60_000 },
|
||||||
|
target: {
|
||||||
|
type: "new-agent",
|
||||||
|
config: {
|
||||||
|
provider: "claude",
|
||||||
|
cwd: tempDir,
|
||||||
|
approvalPolicy: "never",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maxRuns: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
now = new Date("2026-01-01T00:01:00.000Z");
|
||||||
|
await service.tick();
|
||||||
|
|
||||||
|
const inspected = await service.inspect(created.id);
|
||||||
|
const agentId = inspected.runs[0]?.agentId;
|
||||||
|
expect(agentId).toBeTruthy();
|
||||||
|
const agent = manager.getAgent(agentId!);
|
||||||
|
expect(agent?.currentModeId).toBe("bypassPermissions");
|
||||||
|
});
|
||||||
|
|
||||||
test("advances stale nextRunAt on daemon restart", async () => {
|
test("advances stale nextRunAt on daemon restart", async () => {
|
||||||
const service1 = new ScheduleService({
|
const service1 = new ScheduleService({
|
||||||
paseoHome: tempDir,
|
paseoHome: tempDir,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AgentStorage } from "../agent/agent-storage.js";
|
|||||||
import type { AgentPromptInput, AgentSessionConfig } from "../agent/agent-sdk-types.js";
|
import type { AgentPromptInput, AgentSessionConfig } from "../agent/agent-sdk-types.js";
|
||||||
import { curateAgentActivity } from "../agent/activity-curator.js";
|
import { curateAgentActivity } from "../agent/activity-curator.js";
|
||||||
import { ensureAgentLoaded } from "../agent/agent-loading.js";
|
import { ensureAgentLoaded } from "../agent/agent-loading.js";
|
||||||
|
import { getUnattendedModeId } from "../agent/provider-manifest.js";
|
||||||
import { ScheduleStore } from "./store.js";
|
import { ScheduleStore } from "./store.js";
|
||||||
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
|
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -411,7 +412,7 @@ export class ScheduleService {
|
|||||||
const config: AgentSessionConfig = {
|
const config: AgentSessionConfig = {
|
||||||
provider: schedule.target.config.provider,
|
provider: schedule.target.config.provider,
|
||||||
cwd: schedule.target.config.cwd,
|
cwd: schedule.target.config.cwd,
|
||||||
modeId: schedule.target.config.modeId,
|
modeId: schedule.target.config.modeId ?? getUnattendedModeId(schedule.target.config.provider),
|
||||||
model: schedule.target.config.model,
|
model: schedule.target.config.model,
|
||||||
thinkingOptionId: schedule.target.config.thinkingOptionId,
|
thinkingOptionId: schedule.target.config.thinkingOptionId,
|
||||||
title: schedule.target.config.title,
|
title: schedule.target.config.title,
|
||||||
|
|||||||
@@ -8599,10 +8599,12 @@ export class Session {
|
|||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
provider: request.provider,
|
provider: request.provider,
|
||||||
model: request.model,
|
model: request.model,
|
||||||
|
modeId: request.modeId,
|
||||||
workerProvider: request.workerProvider,
|
workerProvider: request.workerProvider,
|
||||||
workerModel: request.workerModel,
|
workerModel: request.workerModel,
|
||||||
verifierProvider: request.verifierProvider,
|
verifierProvider: request.verifierProvider,
|
||||||
verifierModel: request.verifierModel,
|
verifierModel: request.verifierModel,
|
||||||
|
verifierModeId: request.verifierModeId,
|
||||||
verifyPrompt: request.verifyPrompt,
|
verifyPrompt: request.verifyPrompt,
|
||||||
verifyChecks: request.verifyChecks,
|
verifyChecks: request.verifyChecks,
|
||||||
archive: request.archive,
|
archive: request.archive,
|
||||||
|
|||||||
Reference in New Issue
Block a user