Speed up new Pi agent startup (#1727)
* fix(providers): avoid draft feature model guessing * fix(providers): avoid draft command model guessing
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@ OpenCode owns user message IDs. Do not pass Paseo-generated IDs to OpenCode prom
|
||||
|
||||
Every provider adapter owns its canonical user-message timeline rows. When a foreground prompt is accepted, the adapter must emit exactly one `user_message` timeline item for that submitted prompt, using the same message ID it gives to or receives from the provider runtime. Optimistic client messages are UI-only and provider transcript echoes are optional; neither is allowed to be the only source of truth. If the provider later echoes the same submitted user message, dedupe it only within the active turn. Prefer provider-visible message IDs, but ACP runtimes may omit that ID or replace it with a provider-owned one; in that case suppress only echo chunks whose accumulated text is a prefix of the active submitted prompt. Do not perform global transcript text dedupe.
|
||||
|
||||
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes.
|
||||
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes. Draft feature and command listing must use the explicit draft model only; if no model is selected yet, return no metadata instead of resolving a default model through catalog discovery.
|
||||
|
||||
Provider session import has its own contract. The picker calls `listImportableSessions` and receives rows only: provider handle, cwd, title, prompt previews, and last activity. Import calls `importSession({ providerHandleId, cwd })` for the selected row and must not call listing again. The provider returns the resumed session, storage config, persistence handle, and hydrated timeline for that one native session; `AgentManager.importProviderSession` seeds the daemon timeline and publishes the Paseo agent only after it is ready.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
AgentRunResult,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
ImportProviderSessionInput,
|
||||
@@ -527,6 +528,205 @@ test("normalizeConfig strips legacy 'default' model id", async () => {
|
||||
expect(snapshot.config.modeId).toBe("auto");
|
||||
});
|
||||
|
||||
test("listDraftCommands returns no commands without guessing a missing model", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-commands-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
class DraftCommandClient extends TestAgentClient {
|
||||
fetchCatalogCalls = 0;
|
||||
createSessionCalls = 0;
|
||||
availabilityCalls = 0;
|
||||
|
||||
override async isAvailable(): Promise<boolean> {
|
||||
this.availabilityCalls += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
override async fetchCatalog() {
|
||||
this.fetchCatalogCalls += 1;
|
||||
return await super.fetchCatalog();
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
this.createSessionCalls += 1;
|
||||
return await super.createSession(config);
|
||||
}
|
||||
}
|
||||
const client = new DraftCommandClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(manager.listDraftCommands({ provider: "codex", cwd: workdir })).resolves.toEqual([]);
|
||||
|
||||
expect(client.fetchCatalogCalls).toBe(0);
|
||||
expect(client.createSessionCalls).toBe(0);
|
||||
expect(client.availabilityCalls).toBe(0);
|
||||
});
|
||||
|
||||
test("listDraftCommands uses explicit model config without default model fetching", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-commands-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
const draftCommand: AgentSlashCommand = {
|
||||
name: "review",
|
||||
description: "Review changes",
|
||||
argumentHint: "",
|
||||
kind: "command",
|
||||
};
|
||||
class DraftCommandSession extends TestAgentSession {
|
||||
override async listCommands(): Promise<AgentSlashCommand[]> {
|
||||
return [draftCommand];
|
||||
}
|
||||
}
|
||||
class DraftCommandClient extends TestAgentClient {
|
||||
fetchCatalogCalls = 0;
|
||||
createSessionCalls = 0;
|
||||
readonly commandConfigs: AgentSessionConfig[] = [];
|
||||
|
||||
override async fetchCatalog() {
|
||||
this.fetchCatalogCalls += 1;
|
||||
return await super.fetchCatalog();
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
this.createSessionCalls += 1;
|
||||
this.commandConfigs.push(config);
|
||||
return new DraftCommandSession(config);
|
||||
}
|
||||
}
|
||||
const client = new DraftCommandClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
|
||||
const commands = await manager.listDraftCommands({
|
||||
provider: "codex",
|
||||
cwd: workdir,
|
||||
model: "gpt-5.4",
|
||||
});
|
||||
|
||||
expect(commands).toEqual([draftCommand]);
|
||||
expect(client.fetchCatalogCalls).toBe(0);
|
||||
expect(client.createSessionCalls).toBe(1);
|
||||
expect(client.commandConfigs).toEqual([
|
||||
{
|
||||
provider: "codex",
|
||||
cwd: workdir,
|
||||
model: "gpt-5.4",
|
||||
modeId: "auto",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("listDraftFeatures returns no features without guessing a missing model", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-features-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
class DraftFeatureClient extends TestAgentClient {
|
||||
fetchCatalogCalls = 0;
|
||||
createSessionCalls = 0;
|
||||
availabilityCalls = 0;
|
||||
readonly featureConfigs: AgentSessionConfig[] = [];
|
||||
|
||||
override async isAvailable(): Promise<boolean> {
|
||||
this.availabilityCalls += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
override async fetchCatalog() {
|
||||
this.fetchCatalogCalls += 1;
|
||||
return await super.fetchCatalog();
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
this.createSessionCalls += 1;
|
||||
return await super.createSession(config);
|
||||
}
|
||||
|
||||
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||
this.featureConfigs.push(config);
|
||||
return [createFeature({ id: "fast_mode", label: "Fast mode", value: false })];
|
||||
}
|
||||
}
|
||||
const client = new DraftFeatureClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(manager.listDraftFeatures({ provider: "codex", cwd: workdir })).resolves.toEqual([]);
|
||||
|
||||
expect(client.fetchCatalogCalls).toBe(0);
|
||||
expect(client.createSessionCalls).toBe(0);
|
||||
expect(client.availabilityCalls).toBe(0);
|
||||
expect(client.featureConfigs).toEqual([]);
|
||||
});
|
||||
|
||||
test("listDraftFeatures uses explicit model config without default model fetching", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-features-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
const draftFeature = createFeature({ id: "fast_mode", label: "Fast mode", value: false });
|
||||
class DraftFeatureClient extends TestAgentClient {
|
||||
fetchCatalogCalls = 0;
|
||||
createSessionCalls = 0;
|
||||
readonly featureConfigs: AgentSessionConfig[] = [];
|
||||
|
||||
override async fetchCatalog() {
|
||||
this.fetchCatalogCalls += 1;
|
||||
return await super.fetchCatalog();
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
this.createSessionCalls += 1;
|
||||
return await super.createSession(config);
|
||||
}
|
||||
|
||||
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||
this.featureConfigs.push(config);
|
||||
return [draftFeature];
|
||||
}
|
||||
}
|
||||
const client = new DraftFeatureClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
|
||||
const features = await manager.listDraftFeatures({
|
||||
provider: "codex",
|
||||
cwd: workdir,
|
||||
model: "gpt-5.4",
|
||||
});
|
||||
|
||||
expect(features).toEqual([draftFeature]);
|
||||
expect(client.fetchCatalogCalls).toBe(0);
|
||||
expect(client.createSessionCalls).toBe(0);
|
||||
expect(client.featureConfigs).toEqual([
|
||||
{
|
||||
provider: "codex",
|
||||
cwd: workdir,
|
||||
model: "gpt-5.4",
|
||||
modeId: "auto",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("createAgent injects daemon append system prompt at runtime only", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
|
||||
@@ -87,6 +87,10 @@ interface PreparedSessionConfig {
|
||||
launchConfig: AgentSessionConfig;
|
||||
}
|
||||
|
||||
interface NormalizeConfigOptions {
|
||||
resolveDefaultModel?: boolean;
|
||||
}
|
||||
|
||||
interface TimeoutOptions {
|
||||
operation: Promise<void>;
|
||||
timeoutMs: number;
|
||||
@@ -817,8 +821,11 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
async listDraftCommands(config: AgentSessionConfig): Promise<AgentSlashCommand[]> {
|
||||
const normalizedConfig = await this.normalizeConfig(config);
|
||||
const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false });
|
||||
const client = this.requireClient(normalizedConfig.provider);
|
||||
if (!normalizedConfig.model) {
|
||||
return [];
|
||||
}
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
throw new Error(
|
||||
@@ -851,8 +858,11 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
async listDraftFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||
const normalizedConfig = await this.normalizeConfig(config);
|
||||
const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false });
|
||||
const client = this.requireClient(normalizedConfig.provider);
|
||||
if (!normalizedConfig.model) {
|
||||
return [];
|
||||
}
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
throw new Error(
|
||||
@@ -3632,7 +3642,10 @@ export class AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async normalizeConfig(config: AgentSessionConfig): Promise<AgentSessionConfig> {
|
||||
private async normalizeConfig(
|
||||
config: AgentSessionConfig,
|
||||
options: NormalizeConfigOptions = {},
|
||||
): Promise<AgentSessionConfig> {
|
||||
const normalized: AgentSessionConfig = { ...config };
|
||||
|
||||
// Always resolve cwd to absolute path for consistent history file lookup
|
||||
@@ -3663,18 +3676,11 @@ export class AgentManager {
|
||||
normalized.model = trimmed.length > 0 && trimmed !== "default" ? trimmed : undefined;
|
||||
}
|
||||
|
||||
if (!normalized.model) {
|
||||
const client = this.clients.get(normalized.provider);
|
||||
if (client) {
|
||||
try {
|
||||
const catalog = await client.fetchCatalog({ cwd: normalized.cwd, force: false });
|
||||
const defaultModel = catalog.models.find((model) => model.isDefault) ?? catalog.models[0];
|
||||
if (defaultModel) {
|
||||
normalized.model = defaultModel.id;
|
||||
}
|
||||
} catch {
|
||||
// Provider may not support model listing — leave model undefined
|
||||
}
|
||||
const shouldResolveDefaultModel = options.resolveDefaultModel ?? true;
|
||||
if (shouldResolveDefaultModel && !normalized.model) {
|
||||
const defaultModelId = await this.resolveDefaultModelId(normalized);
|
||||
if (defaultModelId) {
|
||||
normalized.model = defaultModelId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3690,6 +3696,20 @@ export class AgentManager {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private async resolveDefaultModelId(config: AgentSessionConfig): Promise<string | undefined> {
|
||||
const client = this.clients.get(config.provider);
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const catalog = await client.fetchCatalog({ cwd: config.cwd, force: false });
|
||||
return (catalog.models.find((model) => model.isDefault) ?? catalog.models[0])?.id;
|
||||
} catch {
|
||||
// Provider may not support model listing — leave model undefined.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareSessionConfig(
|
||||
config: AgentSessionConfig,
|
||||
agentId: string,
|
||||
|
||||
@@ -885,6 +885,17 @@ describe("PiRpcAgentClient", () => {
|
||||
expect(pi.recordedLaunches[0]).toMatchObject({ cwd: "/workspace/with-extension" });
|
||||
});
|
||||
|
||||
test("lists no draft features without starting a Pi session", async () => {
|
||||
const pi = new FakePi();
|
||||
const client = createClient(pi);
|
||||
|
||||
await expect(
|
||||
client.listFeatures(createConfig({ model: "openrouter/test/model" })),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(pi.recordedLaunches).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("maps extension, prompt, and skill commands to Paseo slash commands", async () => {
|
||||
const { pi, session } = await createSession();
|
||||
pi.latestSession().commands = [
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import {
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentFeature,
|
||||
type AgentLaunchContext,
|
||||
type AgentMetadata,
|
||||
type AgentMode,
|
||||
@@ -1981,6 +1982,10 @@ export class PiRpcAgentClient implements AgentClient {
|
||||
}
|
||||
}
|
||||
|
||||
async listFeatures(_config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async listImportableSessions(
|
||||
options?: ListImportableSessionsOptions,
|
||||
): Promise<ImportableProviderSession[]> {
|
||||
|
||||
Reference in New Issue
Block a user