Add provider/model selection for schedules and workspace fetch debug logging

- Unify resolveProviderAndModel across CLI and server, supporting
  provider/model syntax (e.g. codex/gpt-5.4) for agent runs and schedules
- Add --provider flag to schedule create CLI and both MCP schedule tools
- Add structured debug logging to workspace fetch hydrate, sidebar
  refresh, and real-time update paths
- Update orchestrate skill references
This commit is contained in:
Mohamed Boudra
2026-04-14 14:42:10 +07:00
parent 202fa3e8f3
commit dd6a396dbf
17 changed files with 1209 additions and 161 deletions
+1 -53
View File
@@ -16,6 +16,7 @@ import { resolve } from "node:path";
import { lookup } from "mime-types";
import { parseDuration } from "../../utils/duration.js";
import { collectMultiple } from "../../utils/command-options.js";
import { resolveProviderAndModel } from "../../utils/provider-model.js";
export function addRunOptions(cmd: Command): Command {
return cmd
@@ -98,11 +99,6 @@ export interface AgentRunOptions extends CommandOptions {
outputSchema?: string;
}
interface ResolvedProviderModel {
provider: string;
model: string | undefined;
}
function toRunResult(
agent: AgentSnapshotPayload,
statusOverride?: AgentRunResult["status"],
@@ -222,54 +218,6 @@ function structuredRunSchema(output: Record<string, unknown>): OutputSchema<Agen
};
}
export function resolveProviderAndModel(
options: Pick<AgentRunOptions, "provider" | "model">,
): ResolvedProviderModel {
const providerInput = options.provider?.trim() || "claude";
const modelInput = options.model?.trim();
if (options.model !== undefined && !modelInput) {
const error: CommandError = {
code: "INVALID_MODEL",
message: "--model cannot be empty",
};
throw error;
}
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return {
provider: providerInput,
model: modelInput,
};
}
const provider = providerInput.slice(0, slashIndex).trim();
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
if (!provider || !modelFromProvider) {
const error: CommandError = {
code: "INVALID_PROVIDER",
message: "Invalid --provider value",
details: "Use --provider <provider> or --provider <provider>/<model>",
};
throw error;
}
if (modelInput && modelInput !== modelFromProvider) {
const error: CommandError = {
code: "CONFLICTING_MODEL_OPTIONS",
message: "Conflicting model values provided",
details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
};
throw error;
}
return {
provider,
model: modelInput ?? modelFromProvider,
};
}
export async function runRunCommand(
prompt: string,
options: AgentRunOptions,
@@ -15,6 +15,7 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions {
cron?: string;
name?: string;
target?: string;
provider?: string;
maxRuns?: string;
expiresIn?: string;
}
@@ -30,6 +31,7 @@ export async function runCreateCommand(
cron: options.cron,
name: options.name,
target: options.target,
provider: options.provider,
maxRuns: options.maxRuns,
expiresIn: options.expiresIn,
});
@@ -21,6 +21,10 @@ export function createScheduleCommand(): Command {
.option("--cron <expr>", "Cron cadence expression")
.option("--name <name>", "Optional schedule name")
.option("--target <self|new-agent|agent-id>", "Run target")
.option(
"--provider <provider>",
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
)
.option("--max-runs <n>", "Maximum number of runs")
.option("--expires-in <duration>", "Time to live for the schedule"),
).action(withOutput(runCreateCommand));
+44 -17
View File
@@ -9,6 +9,7 @@ import type {
ScheduleTarget,
} from "./types.js";
import { parseDuration } from "../../utils/duration.js";
import { resolveProviderAndModel } from "../../utils/provider-model.js";
export interface ScheduleCommandOptions extends CommandOptions {
host?: string;
@@ -58,7 +59,8 @@ export function formatTarget(target: ScheduleTarget | ScheduleListItem["target"]
if (target.type === "agent") {
return `agent:${target.agentId.slice(0, 7)}`;
}
return `new-agent:${target.config.provider}`;
const modelSuffix = target.config.model ? `/${target.config.model}` : "";
return `new-agent:${target.config.provider}${modelSuffix}`;
}
export function formatDurationMs(durationMs: number): string {
@@ -87,6 +89,7 @@ export function parseScheduleCreateInput(options: {
cron?: string;
name?: string;
target?: string;
provider?: string;
maxRuns?: string;
expiresIn?: string;
}): CreateScheduleInput {
@@ -111,29 +114,53 @@ export function parseScheduleCreateInput(options: {
: { type: "cron", expression: options.cron!.trim() };
const targetValue = options.target?.trim();
const hasExplicitProviderSelection = options.provider !== undefined;
const resolvedProviderModel = resolveProviderAndModel({
provider: options.provider,
defaultProvider: "claude",
});
const newAgentTarget: ScheduleTarget = {
type: "new-agent",
config: {
provider: resolvedProviderModel.provider,
cwd: process.cwd(),
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
},
};
let target: ScheduleTarget;
if (!targetValue || targetValue === "self") {
if (!targetValue) {
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
if (currentAgentId) {
if (currentAgentId && !hasExplicitProviderSelection) {
target = { type: "self", agentId: currentAgentId };
} else {
target = {
type: "new-agent",
config: {
provider: "claude",
cwd: process.cwd(),
},
};
target = newAgentTarget;
}
} else if (targetValue === "self") {
if (hasExplicitProviderSelection) {
throw {
code: "INVALID_TARGET",
message: "--provider can only be used with a new-agent target",
details: "Use --target new-agent or omit --target to create a new agent schedule",
} satisfies CommandError;
}
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
if (!currentAgentId) {
throw {
code: "INVALID_TARGET",
message: "--target self requires running inside a Paseo agent",
} satisfies CommandError;
}
target = { type: "self", agentId: currentAgentId };
} else if (targetValue === "new-agent") {
target = {
type: "new-agent",
config: {
provider: "claude",
cwd: process.cwd(),
},
};
target = newAgentTarget;
} else {
if (hasExplicitProviderSelection) {
throw {
code: "INVALID_TARGET",
message: "--provider can only be used with a new-agent target",
details: "Use --target new-agent or omit --target to create a new agent schedule",
} satisfies CommandError;
}
target = {
type: "agent",
agentId: targetValue,
+60
View File
@@ -0,0 +1,60 @@
import type { CommandError } from "../output/index.js";
export interface ResolveProviderAndModelOptions {
provider?: string;
model?: string;
defaultProvider?: string;
}
export interface ResolvedProviderModel {
provider: string;
model: string | undefined;
}
export function resolveProviderAndModel(
options: ResolveProviderAndModelOptions,
): ResolvedProviderModel {
const providerInput = options.provider?.trim() || options.defaultProvider || "claude";
const modelInput = options.model?.trim();
if (options.model !== undefined && !modelInput) {
const error: CommandError = {
code: "INVALID_MODEL",
message: "--model cannot be empty",
};
throw error;
}
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return {
provider: providerInput,
model: modelInput,
};
}
const provider = providerInput.slice(0, slashIndex).trim();
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
if (!provider || !modelFromProvider) {
const error: CommandError = {
code: "INVALID_PROVIDER",
message: "Invalid --provider value",
details: "Use --provider <provider> or --provider <provider>/<model>",
};
throw error;
}
if (modelInput && modelInput !== modelFromProvider) {
const error: CommandError = {
code: "CONFLICTING_MODEL_OPTIONS",
message: "Conflicting model values provided",
details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
};
throw error;
}
return {
provider,
model: modelInput ?? modelFromProvider,
};
}
@@ -23,6 +23,11 @@ try {
const createdJson = JSON.parse(created.stdout);
assert.strictEqual(createdJson.name, "review-prs");
assert.strictEqual(createdJson.cadence, "every:5m");
assert(
typeof createdJson.target === "string" &&
(createdJson.target.startsWith("agent:") || createdJson.target === "new-agent:claude"),
created.stdout,
);
const listed = await ctx.paseo(["schedule", "ls", "--json"]);
assert.strictEqual(listed.exitCode, 0, listed.stderr);
@@ -53,6 +58,61 @@ try {
console.log("schedule commands work\n");
}
{
console.log("Test 1b: schedule create accepts provider/model syntax for new-agent runs");
const created = await ctx.paseo(
[
"schedule",
"create",
"Refactor the API layer",
"--every",
"10m",
"--provider",
"codex/gpt-5.4",
"--json",
],
{ timeout: 30000 },
);
assert.strictEqual(created.exitCode, 0, created.stderr);
const createdJson = JSON.parse(created.stdout);
assert.strictEqual(createdJson.target, "new-agent:codex/gpt-5.4");
const inspected = await ctx.paseo(["schedule", "inspect", createdJson.id, "--json"]);
assert.strictEqual(inspected.exitCode, 0, inspected.stderr);
const inspectedJson = JSON.parse(inspected.stdout);
assert.strictEqual(inspectedJson.target.config.provider, "codex");
assert.strictEqual(inspectedJson.target.config.model, "gpt-5.4");
const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]);
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
console.log("schedule provider/model syntax works\n");
}
{
console.log("Test 1c: schedule create rejects provider with self target");
const result = await ctx.paseo(
[
"schedule",
"create",
"Conflicting schedule",
"--every",
"5m",
"--target",
"self",
"--provider",
"codex/gpt-5.4",
],
{ timeout: 30000 },
);
assert.notStrictEqual(result.exitCode, 0, "should fail for self target with provider");
const output = result.stdout + result.stderr;
assert(
output.includes("--provider can only be used with a new-agent target"),
"should explain provider target mismatch",
);
console.log("schedule rejects provider with self target\n");
}
{
console.log("Test 2: loop run/ls/inspect/logs/stop work");
const run = await ctx.paseo(