mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 15:34:39 +00:00
fix(plugins): a running agent's composer validates and refreshes against the agent's own catalog
setAgentModel and setAgentThinkingOption checked the provider's global catalog, while the picker reads the catalog of the agent's directory, so a model only the project lists was rejected. The validation input now carries the directory. The composer read the catalog once with no observer, so reconnect repair, which refetches only active queries, never refreshed it and a failed load never retried. While anything watches, the composer now observes the catalog query. The shared composer map kept every agent's composer for the session. It is now dropped when nothing watches it and no composer is mounted.
This commit is contained in:
@@ -6,6 +6,8 @@ import { filterSelectableAgentModels } from "./agent-sdk-types.js";
|
||||
|
||||
export interface AgentConfigurationValidationInput {
|
||||
provider: string;
|
||||
/** The directory whose catalog lists the choices; omitted, the provider's global catalog does. */
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
modeId?: string;
|
||||
thinkingOptionId?: string;
|
||||
|
||||
@@ -5436,6 +5436,32 @@ test("a running agent rejects a model or thinking option its provider does not l
|
||||
expect(manager.getAgent(snapshot.id)?.config.model).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a selection is validated against the catalog of the agent's directory", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-selection-cwd-"));
|
||||
const asked: Array<string | undefined> = [];
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new TestAgentClient() },
|
||||
registry: new AgentStorage(join(workdir, "agents"), logger),
|
||||
validateAgentSelection: async (input) => {
|
||||
asked.push(input.cwd);
|
||||
return [];
|
||||
},
|
||||
logger,
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000137",
|
||||
});
|
||||
const snapshot = await manager.createAgent(
|
||||
{ provider: "codex", cwd: workdir, model: "gpt-5.4" },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
|
||||
await manager.setAgentModel(snapshot.id, "gpt-5.4");
|
||||
await manager.setAgentThinkingOption(snapshot.id, "low");
|
||||
|
||||
expect(asked.length).toBeGreaterThan(0);
|
||||
expect(asked.every((cwd) => cwd === workdir)).toBe(true);
|
||||
});
|
||||
|
||||
test("a thinking option is checked against the model the agent runs, as the picker chooses it", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-thinking-runtime-model-"));
|
||||
const provider: ProviderSnapshotEntry = {
|
||||
|
||||
@@ -2299,6 +2299,7 @@ export class AgentManager {
|
||||
const issues =
|
||||
(await this.validateAgentSelection?.({
|
||||
provider: agent.provider,
|
||||
cwd: agent.cwd,
|
||||
model,
|
||||
thinkingOptionId,
|
||||
})) ?? [];
|
||||
@@ -2308,7 +2309,11 @@ export class AgentManager {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.assertSelectionListed({ provider: agent.provider, thinkingOptionId });
|
||||
await this.assertSelectionListed({
|
||||
provider: agent.provider,
|
||||
cwd: agent.cwd,
|
||||
thinkingOptionId,
|
||||
});
|
||||
}
|
||||
|
||||
async setAgentModel(agentId: string, modelId: string | null): Promise<void> {
|
||||
@@ -2318,6 +2323,7 @@ export class AgentManager {
|
||||
if (normalizedModelId) {
|
||||
await this.assertSelectionListed({
|
||||
provider: agent.provider,
|
||||
cwd: agent.cwd,
|
||||
model: normalizedModelId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,6 +311,58 @@ describe("ProviderSnapshotManager public surface", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("validates a selection against the catalog of the directory it is for", async () => {
|
||||
const manager = new ProviderSnapshotManager({
|
||||
logger: createTestLogger(),
|
||||
providerOverrides: {
|
||||
claude: { enabled: false },
|
||||
codex: { enabled: false },
|
||||
copilot: { enabled: false },
|
||||
pi: { enabled: false },
|
||||
},
|
||||
extraClients: {
|
||||
opencode: createExtraClient("opencode", {
|
||||
isAvailable: async () => true,
|
||||
// A project can configure models its global catalog does not list.
|
||||
fetchCatalog: async (options) => ({
|
||||
models: [
|
||||
{
|
||||
provider: "opencode",
|
||||
id: options.scope === "workspace" ? "project/custom" : "global/default",
|
||||
label: "Model",
|
||||
thinkingOptions: [
|
||||
{ id: options.scope === "workspace" ? "deep" : "plain", label: "Level" },
|
||||
],
|
||||
},
|
||||
],
|
||||
modes: [],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(
|
||||
manager.validateAgentConfiguration({
|
||||
provider: "opencode",
|
||||
cwd: "/tmp/project",
|
||||
model: "project/custom",
|
||||
thinkingOptionId: "deep",
|
||||
}),
|
||||
).resolves.toEqual([]);
|
||||
await expect(
|
||||
manager.validateAgentConfiguration({ provider: "opencode", model: "project/custom" }),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
path: ["model"],
|
||||
message: "Model 'project/custom' is not available for provider 'opencode'",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
manager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test("reports an unavailable Hub agent provider at the authored provider field", async () => {
|
||||
const manager = new ProviderSnapshotManager({ logger: createTestLogger() });
|
||||
try {
|
||||
|
||||
@@ -498,6 +498,7 @@ export class ProviderSnapshotManager {
|
||||
|
||||
const provider = await this.getProvider({
|
||||
provider: input.provider,
|
||||
cwd: input.cwd,
|
||||
wait: true,
|
||||
});
|
||||
if (!provider.enabled) {
|
||||
|
||||
Reference in New Issue
Block a user