mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb96c02cc |
Generated
+10
@@ -39240,6 +39240,7 @@
|
|||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "^7.30.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"which": "^5.0.0",
|
"which": "^5.0.0",
|
||||||
"ws": "^8.14.2",
|
"ws": "^8.14.2",
|
||||||
@@ -39970,6 +39971,15 @@
|
|||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/server/node_modules/undici": {
|
||||||
|
"version": "7.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
|
||||||
|
"integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/server/node_modules/vitest": {
|
"packages/server/node_modules/vitest": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import type { UsageReportEntry } from "@getpaseo/protocol/messages";
|
import type { UsageReportEntry } from "@getpaseo/protocol/messages";
|
||||||
import { expect, test, type Page } from "../support/fixtures";
|
import { expect, test, type Page } from "../support/fixtures";
|
||||||
import { expectComposerVisible, submitMessage } from "../support/helpers/composer";
|
import { expectComposerVisible, submitMessage } from "../support/helpers/composer";
|
||||||
import {
|
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||||
openAgentRoute,
|
|
||||||
seedMockAgentWorkspace,
|
|
||||||
seedRunningMockAgentWorkspace,
|
|
||||||
} from "../support/helpers/mock-agent";
|
|
||||||
import { installUsageReportsFixture } from "../support/helpers/usage-reports";
|
import { installUsageReportsFixture } from "../support/helpers/usage-reports";
|
||||||
|
|
||||||
const REPORT_ID = "fixture:fixture-account";
|
const REPORT_ID = "fixture:fixture-account";
|
||||||
@@ -46,30 +42,6 @@ async function openMockAgent(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe("usage composer pill", () => {
|
test.describe("usage composer pill", () => {
|
||||||
test("shows the report when an existing running chat opens", async ({ page }) => {
|
|
||||||
test.setTimeout(180_000);
|
|
||||||
const usage = await installUsageReportsFixture(page, {
|
|
||||||
agentReportIds: [REPORT_ID],
|
|
||||||
lists: [[agentEntry(42)]],
|
|
||||||
});
|
|
||||||
const session = await seedRunningMockAgentWorkspace({
|
|
||||||
repoPrefix: "usage-running-",
|
|
||||||
title: "Running usage pill",
|
|
||||||
initialPrompt: "Show usage during the turn.",
|
|
||||||
model: "one-minute-stream",
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await openAgentRoute(page, session);
|
|
||||||
await expectComposerVisible(page);
|
|
||||||
await expect(page.getByTestId("usage-composer-pill")).toContainText("42%", {
|
|
||||||
timeout: 20_000,
|
|
||||||
});
|
|
||||||
expect(usage.agentRequests()[0]).toEqual({ agentId: session.agentId });
|
|
||||||
} finally {
|
|
||||||
await session.cleanup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("opens the cached report without a request and shows when it was fetched", async ({
|
test("opens the cached report without a request and shows when it was fetched", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -31,13 +31,8 @@ function usageReportsQueryKey(serverId: string) {
|
|||||||
return ["usage", "reports", serverId] as const;
|
return ["usage", "reports", serverId] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentUsageQueryKey(
|
function agentUsageQueryKey(serverId: string, agentId: string, model: string | null) {
|
||||||
serverId: string,
|
return ["usage", "agent", serverId, agentId, model] as const;
|
||||||
agentId: string,
|
|
||||||
model: string | null,
|
|
||||||
isRunning: boolean,
|
|
||||||
) {
|
|
||||||
return ["usage", "agent", serverId, agentId, model, isRunning] as const;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireClient(serverId: string) {
|
function requireClient(serverId: string) {
|
||||||
@@ -165,7 +160,8 @@ export function useUsageByHost(): {
|
|||||||
/**
|
/**
|
||||||
* The usage report for the account an agent is spending. The daemon resolves the
|
* The usage report for the account an agent is spending. The daemon resolves the
|
||||||
* report ID, which is re-resolved when the model changes (a new key) and when a
|
* report ID, which is re-resolved when the model changes (a new key) and when a
|
||||||
* turn completes (a new query key). The report itself is read from the shared cache.
|
* turn completes (the query is paused while the agent runs, and its data is always
|
||||||
|
* stale, so resuming refetches). The report itself is read from the shared cache.
|
||||||
*/
|
*/
|
||||||
export function useAgentUsage(
|
export function useAgentUsage(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
@@ -180,10 +176,10 @@ export function useAgentUsage(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const identity = useFetchQuery({
|
const identity = useFetchQuery({
|
||||||
queryKey: agentUsageQueryKey(serverId, agentId, model, isRunning),
|
queryKey: agentUsageQueryKey(serverId, agentId, model),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(await requireClient(serverId).resolveAgentUsageReport({ agentId })).reportId,
|
(await requireClient(serverId).resolveAgentUsageReport({ agentId })).reportId,
|
||||||
enabled: isConnected && isSupported,
|
enabled: isConnected && isSupported && !isRunning,
|
||||||
dataShape: "value",
|
dataShape: "value",
|
||||||
staleTimeMs: 0,
|
staleTimeMs: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "^7.30.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"which": "^5.0.0",
|
"which": "^5.0.0",
|
||||||
"ws": "^8.14.2",
|
"ws": "^8.14.2",
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import {
|
|||||||
type AgentTimelineItem,
|
type AgentTimelineItem,
|
||||||
type AgentUsage,
|
type AgentUsage,
|
||||||
type AgentRuntimeInfo,
|
type AgentRuntimeInfo,
|
||||||
type UsageReference,
|
|
||||||
type ImportedTimelineEntry,
|
type ImportedTimelineEntry,
|
||||||
type ImportableProviderSession,
|
type ImportableProviderSession,
|
||||||
type ListImportableSessionsOptions,
|
type ListImportableSessionsOptions,
|
||||||
@@ -1168,24 +1167,6 @@ export class AgentManager {
|
|||||||
return agent ? { ...agent } : null;
|
return agent ? { ...agent } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference(
|
|
||||||
id: string,
|
|
||||||
): Promise<{ found: boolean; reference: UsageReference | null }> {
|
|
||||||
const agent = this.getAgent(id);
|
|
||||||
const stored = agent?.session ? null : await this.registry?.get(id);
|
|
||||||
const source = stored ?? agent;
|
|
||||||
if (!source) return { found: false, reference: null };
|
|
||||||
const config = stored ? buildStoredAgentConfig(stored) : agent!.config;
|
|
||||||
const reference =
|
|
||||||
(await this.clients.get(source.provider)?.resolveUsageReference?.({
|
|
||||||
config,
|
|
||||||
runtimeInfo: source.runtimeInfo ?? null,
|
|
||||||
persistence: source.persistence ?? null,
|
|
||||||
session: agent?.session ?? null,
|
|
||||||
})) ?? null;
|
|
||||||
return { found: true, reference };
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForAgentClose(agentId: string): Promise<void> {
|
async waitForAgentClose(agentId: string): Promise<void> {
|
||||||
// Loading during reload must wait for the replacement, not resume another writer.
|
// Loading during reload must wait for the replacement, not resume another writer.
|
||||||
await this.lifecycleMutationTails.get(agentId);
|
await this.lifecycleMutationTails.get(agentId);
|
||||||
|
|||||||
@@ -673,6 +673,8 @@ export interface AgentSession {
|
|||||||
/** New provider-owned rows to commit on registration. streamHistory must also
|
/** New provider-owned rows to commit on registration. streamHistory must also
|
||||||
* replay them at their original timestamps; restored sessions omit old rows. */
|
* replay them at their original timestamps; restored sessions omit old rows. */
|
||||||
readonly initialTimeline?: ImportedTimelineEntry[];
|
readonly initialTimeline?: ImportedTimelineEntry[];
|
||||||
|
/** Resolved at fetch time because model and credentials may change during a session. */
|
||||||
|
getUsageReference?(): Promise<UsageReference | null>;
|
||||||
run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
|
run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
|
||||||
startTurn(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<{ turnId: string }>;
|
startTurn(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<{ turnId: string }>;
|
||||||
steerActiveTurn?(prompt: AgentPromptInput, options: SteerActiveTurnOptions): Promise<SteerResult>;
|
steerActiveTurn?(prompt: AgentPromptInput, options: SteerActiveTurnOptions): Promise<SteerResult>;
|
||||||
@@ -748,13 +750,6 @@ export interface ResolveAgentDefaultModeInput {
|
|||||||
export interface AgentClient {
|
export interface AgentClient {
|
||||||
readonly provider: AgentProvider;
|
readonly provider: AgentProvider;
|
||||||
readonly capabilities: AgentCapabilityFlags;
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
/** Account used by the agent. Resolving this must not start or load a session. */
|
|
||||||
resolveUsageReference?(input: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
runtimeInfo: AgentRuntimeInfo | null;
|
|
||||||
persistence: AgentPersistenceHandle | null;
|
|
||||||
session: AgentSession | null;
|
|
||||||
}): Promise<UsageReference | null>;
|
|
||||||
createSession(
|
createSession(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
|
|||||||
@@ -406,12 +406,7 @@ describe("PluginAgentClientRegistry", () => {
|
|||||||
cwd: "/workspace",
|
cwd: "/workspace",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
expect(
|
expect(await session.getUsageReference?.()).toBeNull();
|
||||||
await client.resolveUsageReference?.({
|
|
||||||
config: { provider: harness.registration.id, cwd: "/workspace" },
|
|
||||||
session,
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
expect(harness.inputs.some((input) => input.type === "session.usage_reference")).toBe(false);
|
expect(harness.inputs.some((input) => input.type === "session.usage_reference")).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await session.close();
|
await session.close();
|
||||||
|
|||||||
@@ -897,15 +897,6 @@ class PluginAgentClient implements AgentClient {
|
|||||||
return agentCapabilities(this.runtime.negotiatedCapabilities);
|
return agentCapabilities(this.runtime.negotiatedCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference({
|
|
||||||
session,
|
|
||||||
}: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
session: AgentSession | null;
|
|
||||||
}) {
|
|
||||||
return session instanceof PluginAgentSession ? session.getPluginUsageReference() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
@@ -1105,7 +1096,7 @@ class PluginAgentSession implements AgentSession {
|
|||||||
return agentCapabilities(this.bridge.negotiatedCapabilities);
|
return agentCapabilities(this.bridge.negotiatedCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPluginUsageReference(): Promise<UsageReference | null> {
|
getUsageReference(): Promise<UsageReference | null> {
|
||||||
return this.bridge.getUsageReference();
|
return this.bridge.getUsageReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getRuntimeInfo: async () => mapRuntimeInfo(provider, await inner.getRuntimeInfo()),
|
getRuntimeInfo: async () => mapRuntimeInfo(provider, await inner.getRuntimeInfo()),
|
||||||
|
getUsageReference: inner.getUsageReference?.bind(inner),
|
||||||
getAvailableModes: () => inner.getAvailableModes(),
|
getAvailableModes: () => inner.getAvailableModes(),
|
||||||
getCurrentMode: () => inner.getCurrentMode(),
|
getCurrentMode: () => inner.getCurrentMode(),
|
||||||
setMode: (modeId) => inner.setMode(modeId),
|
setMode: (modeId) => inner.setMode(modeId),
|
||||||
@@ -489,28 +490,13 @@ function wrapClientProvider(
|
|||||||
const listFeatures = inner.listFeatures?.bind(inner);
|
const listFeatures = inner.listFeatures?.bind(inner);
|
||||||
const archiveNativeSession = inner.archiveNativeSession?.bind(inner);
|
const archiveNativeSession = inner.archiveNativeSession?.bind(inner);
|
||||||
const unarchiveNativeSession = inner.unarchiveNativeSession?.bind(inner);
|
const unarchiveNativeSession = inner.unarchiveNativeSession?.bind(inner);
|
||||||
// An alias's live session can carry launch env that is fresher than stored config.
|
|
||||||
const sessions = new WeakMap<AgentSession, AgentSession>();
|
|
||||||
const wrapSession = (session: AgentSession) => {
|
|
||||||
const wrapped = wrapSessionProvider(provider, session);
|
|
||||||
sessions.set(wrapped, session);
|
|
||||||
return wrapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
capabilities: inner.capabilities,
|
capabilities: inner.capabilities,
|
||||||
resolveUsageReference: inner.resolveUsageReference
|
|
||||||
? ({ config, runtimeInfo, persistence, session }) =>
|
|
||||||
inner.resolveUsageReference!({
|
|
||||||
config: { ...config, provider: inner.provider },
|
|
||||||
runtimeInfo,
|
|
||||||
persistence,
|
|
||||||
session: session ? (sessions.get(session) ?? null) : null,
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
createSession: async (config, launchContext) =>
|
createSession: async (config, launchContext) =>
|
||||||
wrapSession(
|
wrapSessionProvider(
|
||||||
|
provider,
|
||||||
await inner.createSession(
|
await inner.createSession(
|
||||||
{
|
{
|
||||||
...config,
|
...config,
|
||||||
@@ -520,7 +506,8 @@ function wrapClientProvider(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
resumeSession: async (handle, overrides, launchContext, options) =>
|
resumeSession: async (handle, overrides, launchContext, options) =>
|
||||||
wrapSession(
|
wrapSessionProvider(
|
||||||
|
provider,
|
||||||
await inner.resumeSession(
|
await inner.resumeSession(
|
||||||
{
|
{
|
||||||
...handle,
|
...handle,
|
||||||
@@ -582,7 +569,7 @@ function wrapClientProvider(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...imported,
|
...imported,
|
||||||
session: wrapSession(imported.session),
|
session: wrapSessionProvider(provider, imported.session),
|
||||||
config: {
|
config: {
|
||||||
...imported.config,
|
...imported.config,
|
||||||
provider,
|
provider,
|
||||||
|
|||||||
@@ -224,17 +224,8 @@ function createSessionWithConfig(
|
|||||||
|
|
||||||
test("ACP usage reference uses the provider ID", async () => {
|
test("ACP usage reference uses the provider ID", async () => {
|
||||||
for (const provider of ["copilot", "cursor", "kimi", "custom-source"]) {
|
for (const provider of ["copilot", "cursor", "kimi", "custom-source"]) {
|
||||||
const client = new ACPAgentClient({
|
const session = createSessionWithConfig({ provider });
|
||||||
provider,
|
expect(await session.getUsageReference()).toEqual({ source: provider, input: {} });
|
||||||
logger: createTestLogger(),
|
|
||||||
defaultCommand: ["unused"],
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
await client.resolveUsageReference({
|
|
||||||
config: { provider, cwd: "/tmp/paseo-acp-test" },
|
|
||||||
session: null,
|
|
||||||
}),
|
|
||||||
).toEqual({ source: provider, input: {} });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -882,9 +882,6 @@ function isACPCreateConfigUnattended(input: AgentCreateConfigUnattendedInput): b
|
|||||||
export class ACPAgentClient implements AgentClient {
|
export class ACPAgentClient implements AgentClient {
|
||||||
readonly provider: string;
|
readonly provider: string;
|
||||||
readonly capabilities: AgentCapabilityFlags;
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
async resolveUsageReference() {
|
|
||||||
return { source: this.provider, input: {} };
|
|
||||||
}
|
|
||||||
readonly resolveCreateConfig = resolveACPCreateConfig;
|
readonly resolveCreateConfig = resolveACPCreateConfig;
|
||||||
readonly isCreateConfigUnattended = isACPCreateConfigUnattended;
|
readonly isCreateConfigUnattended = isACPCreateConfigUnattended;
|
||||||
|
|
||||||
@@ -1636,6 +1633,10 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
readonly provider: string;
|
readonly provider: string;
|
||||||
readonly capabilities: AgentCapabilityFlags;
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
|
|
||||||
|
async getUsageReference() {
|
||||||
|
return { source: this.provider, input: {} };
|
||||||
|
}
|
||||||
|
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||||
private readonly defaultCommand: [string, ...string[]];
|
private readonly defaultCommand: [string, ...string[]];
|
||||||
|
|||||||
@@ -531,16 +531,16 @@ describe("ClaudeAgentClient binary resolution", () => {
|
|||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
test("Claude usage reference follows CLAUDE_CONFIG_DIR and excludes API overrides", async () => {
|
test("Claude usage reference follows CLAUDE_CONFIG_DIR and excludes API overrides", async () => {
|
||||||
const config = { provider: "claude", cwd: process.cwd() };
|
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||||
const client = new ClaudeAgentClient({
|
const session = await client.createSession(
|
||||||
logger,
|
{ provider: "claude", cwd: process.cwd() },
|
||||||
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: "/accounts/second" } },
|
{ env: { CLAUDE_CONFIG_DIR: "/accounts/second" } },
|
||||||
resolveBinary: async () => "/test/claude/bin",
|
);
|
||||||
});
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
expect(await client.resolveUsageReference?.({ config, session: null })).toEqual({
|
|
||||||
source: "claude",
|
source: "claude",
|
||||||
input: { configDir: "/accounts/second" },
|
input: { configDir: "/accounts/second" },
|
||||||
});
|
});
|
||||||
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"])(
|
test.each(["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"])(
|
||||||
@@ -548,15 +548,14 @@ describe("ClaudeAgentClient binary resolution", () => {
|
|||||||
async (name) => {
|
async (name) => {
|
||||||
const client = new ClaudeAgentClient({
|
const client = new ClaudeAgentClient({
|
||||||
logger,
|
logger,
|
||||||
runtimeSettings: { env: { [name]: "override" } },
|
|
||||||
resolveBinary: async () => "/test/claude/bin",
|
resolveBinary: async () => "/test/claude/bin",
|
||||||
});
|
});
|
||||||
expect(
|
const session = await client.createSession(
|
||||||
await client.resolveUsageReference?.({
|
{ provider: "claude", cwd: process.cwd() },
|
||||||
config: { provider: "claude", cwd: process.cwd() },
|
{ env: { [name]: "override" } },
|
||||||
session: null,
|
);
|
||||||
}),
|
expect(await session.getUsageReference?.()).toBeNull();
|
||||||
).toBeNull();
|
await session.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -570,16 +569,15 @@ describe("ClaudeAgentClient binary resolution", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const client = registry["work-claude"].createClient(logger);
|
const session = await registry["work-claude"].createClient(logger).createSession({
|
||||||
expect(
|
provider: "work-claude",
|
||||||
await client.resolveUsageReference?.({
|
cwd: process.cwd(),
|
||||||
config: { provider: "work-claude", cwd: process.cwd() },
|
});
|
||||||
session: null,
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
source: "claude",
|
source: "claude",
|
||||||
input: { configDir: "/accounts/work" },
|
input: { configDir: "/accounts/work" },
|
||||||
});
|
});
|
||||||
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolves the installed Claude Code version", async () => {
|
test("resolves the installed Claude Code version", async () => {
|
||||||
|
|||||||
@@ -1519,15 +1519,6 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
this.rewindSdk = options.rewindSdk ?? realClaudeRewindSdk;
|
this.rewindSdk = options.rewindSdk ?? realClaudeRewindSdk;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference() {
|
|
||||||
const env = createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings });
|
|
||||||
if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) return null;
|
|
||||||
return {
|
|
||||||
source: "claude",
|
|
||||||
input: { configDir: env.CLAUDE_CONFIG_DIR || path.join(env.HOME || homedir(), ".claude") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveConfiguredModel(model: AgentModelDefinition): AgentModelDefinition {
|
resolveConfiguredModel(model: AgentModelDefinition): AgentModelDefinition {
|
||||||
return resolveConfiguredClaudeModel(model);
|
return resolveConfiguredClaudeModel(model);
|
||||||
}
|
}
|
||||||
@@ -2051,6 +2042,15 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
readonly provider = "claude" as const;
|
readonly provider = "claude" as const;
|
||||||
readonly capabilities = CLAUDE_CAPABILITIES;
|
readonly capabilities = CLAUDE_CAPABILITIES;
|
||||||
|
|
||||||
|
async getUsageReference() {
|
||||||
|
const env = this.buildSdkEnv();
|
||||||
|
if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) return null;
|
||||||
|
return {
|
||||||
|
source: "claude",
|
||||||
|
input: { configDir: env.CLAUDE_CONFIG_DIR || path.join(env.HOME || homedir(), ".claude") },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private readonly config: ClaudeAgentConfig;
|
private readonly config: ClaudeAgentConfig;
|
||||||
private readonly launchEnv?: Record<string, string>;
|
private readonly launchEnv?: Record<string, string>;
|
||||||
private readonly agentId?: string;
|
private readonly agentId?: string;
|
||||||
|
|||||||
@@ -172,18 +172,41 @@ function createSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("Codex usage reference follows CODEX_HOME and excludes custom base URLs", async () => {
|
test("Codex usage reference follows CODEX_HOME and excludes custom base URLs", async () => {
|
||||||
const config = createConfig();
|
const session = new CodexAppServerAgentSession(
|
||||||
const client = new CodexAppServerAgentClient(createTestLogger(), {
|
createConfig(),
|
||||||
env: { CODEX_HOME: "/accounts/second" },
|
null,
|
||||||
});
|
createTestLogger(),
|
||||||
expect(await client.resolveUsageReference({ config, session: null })).toEqual({
|
() => {
|
||||||
|
throw new Error("unused");
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
"interactive",
|
||||||
|
{ CODEX_HOME: "/accounts/second" },
|
||||||
|
);
|
||||||
|
expect(await session.getUsageReference()).toEqual({
|
||||||
source: "codex",
|
source: "codex",
|
||||||
input: { codexHome: "/accounts/second" },
|
input: { codexHome: "/accounts/second" },
|
||||||
});
|
});
|
||||||
const custom = new CodexAppServerAgentClient(createTestLogger(), {
|
const custom = new CodexAppServerAgentSession(
|
||||||
env: { CODEX_HOME: "/accounts/second", OPENAI_BASE_URL: "https://example.test" },
|
createConfig(),
|
||||||
});
|
null,
|
||||||
expect(await custom.resolveUsageReference({ config, session: null })).toBeNull();
|
createTestLogger(),
|
||||||
|
() => {
|
||||||
|
throw new Error("unused");
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
"interactive",
|
||||||
|
{ CODEX_HOME: "/accounts/second", OPENAI_BASE_URL: "https://example.test" },
|
||||||
|
);
|
||||||
|
expect(await custom.getUsageReference()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
function createProviderWithFakeAppServer(
|
function createProviderWithFakeAppServer(
|
||||||
|
|||||||
@@ -3456,6 +3456,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
private readonly autoReviewEnabled: boolean = false,
|
private readonly autoReviewEnabled: boolean = false,
|
||||||
private readonly agentId?: string,
|
private readonly agentId?: string,
|
||||||
private readonly initialResumePurpose: "interactive" | "history" = "interactive",
|
private readonly initialResumePurpose: "interactive" | "history" = "interactive",
|
||||||
|
private readonly usageEnv: NodeJS.ProcessEnv = process.env,
|
||||||
) {
|
) {
|
||||||
this.logger = logger.child({
|
this.logger = logger.child({
|
||||||
module: "agent",
|
module: "agent",
|
||||||
@@ -3485,6 +3486,17 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUsageReference() {
|
||||||
|
if (this.usageEnv.OPENAI_BASE_URL) return null;
|
||||||
|
return {
|
||||||
|
source: "codex",
|
||||||
|
input: {
|
||||||
|
codexHome:
|
||||||
|
this.usageEnv.CODEX_HOME || path.join(this.usageEnv.HOME || os.homedir(), ".codex"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get id(): string | null {
|
get id(): string | null {
|
||||||
return this.currentThreadId;
|
return this.currentThreadId;
|
||||||
}
|
}
|
||||||
@@ -7028,15 +7040,6 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
private readonly deps: CodexAppServerAgentDeps = {},
|
private readonly deps: CodexAppServerAgentDeps = {},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async resolveUsageReference() {
|
|
||||||
const env = buildCodexAppServerEnv(this.runtimeSettings);
|
|
||||||
if (env.OPENAI_BASE_URL) return null;
|
|
||||||
return {
|
|
||||||
source: "codex",
|
|
||||||
input: { codexHome: env.CODEX_HOME || path.join(env.HOME || os.homedir(), ".codex") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private sessionDeps(launchEnv: Record<string, string> | undefined): CodexAppServerAgentDeps {
|
private sessionDeps(launchEnv: Record<string, string> | undefined): CodexAppServerAgentDeps {
|
||||||
return {
|
return {
|
||||||
...this.deps,
|
...this.deps,
|
||||||
@@ -7158,6 +7161,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
autoReviewEnabled,
|
autoReviewEnabled,
|
||||||
launchContext?.agentId,
|
launchContext?.agentId,
|
||||||
"interactive",
|
"interactive",
|
||||||
|
buildCodexAppServerEnv(this.runtimeSettings, launchContext?.env),
|
||||||
);
|
);
|
||||||
await session.connect();
|
await session.connect();
|
||||||
return session;
|
return session;
|
||||||
@@ -7190,6 +7194,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
autoReviewEnabled,
|
autoReviewEnabled,
|
||||||
launchContext?.agentId,
|
launchContext?.agentId,
|
||||||
options?.purpose ?? "interactive",
|
options?.purpose ?? "interactive",
|
||||||
|
buildCodexAppServerEnv(this.runtimeSettings, launchContext?.env),
|
||||||
);
|
);
|
||||||
await session.connect();
|
await session.connect();
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@@ -194,20 +194,14 @@ describe("OMP agent client and session", () => {
|
|||||||
expires: Date.now() + 60_000,
|
expires: Date.now() + 60_000,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
db.prepare("INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?)").run(
|
|
||||||
"session:sticky:openai-codex:omp-session-1",
|
|
||||||
JSON.stringify({ type: "oauth", credentialId: 1 }),
|
|
||||||
Math.floor(Date.now() / 1000) + 100,
|
|
||||||
);
|
|
||||||
db.close();
|
db.close();
|
||||||
try {
|
try {
|
||||||
const env = {
|
const omp = new OmpHarness();
|
||||||
|
await omp.start({}, undefined, {
|
||||||
OMP_PROFILE: "",
|
OMP_PROFILE: "",
|
||||||
PI_CODING_AGENT_DIR: agentDir,
|
PI_CODING_AGENT_DIR: agentDir,
|
||||||
XDG_DATA_HOME: "",
|
XDG_DATA_HOME: "",
|
||||||
};
|
});
|
||||||
const omp = new OmpHarness({ env });
|
|
||||||
await omp.start({}, undefined, env);
|
|
||||||
const runtime = omp.runtime();
|
const runtime = omp.runtime();
|
||||||
runtime.state = { ...runtime.state, model: { provider: "openai-codex", id: "codex-model" } };
|
runtime.state = { ...runtime.state, model: { provider: "openai-codex", id: "codex-model" } };
|
||||||
const requestsBefore = runtime.getStateRequestCount;
|
const requestsBefore = runtime.getStateRequestCount;
|
||||||
@@ -222,20 +216,6 @@ describe("OMP agent client and session", () => {
|
|||||||
input: { accessToken: "claude-fixture" },
|
input: { accessToken: "claude-fixture" },
|
||||||
});
|
});
|
||||||
expect(runtime.getStateRequestCount).toBe(requestsBefore + 2);
|
expect(runtime.getStateRequestCount).toBe(requestsBefore + 2);
|
||||||
const beforeClosed = runtime.getStateRequestCount;
|
|
||||||
expect(
|
|
||||||
await omp.getStoredUsageReference("openai-codex/codex-model", "omp-session-1"),
|
|
||||||
).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "codex-fixture", accountId: "codex-account" },
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
await omp.getStoredUsageReference("openai-codex/codex-model", "omp-session-1", null),
|
|
||||||
).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "codex-fixture", accountId: "codex-account" },
|
|
||||||
});
|
|
||||||
expect(runtime.getStateRequestCount).toBe(beforeClosed);
|
|
||||||
await omp.close();
|
await omp.close();
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(home, { recursive: true, force: true });
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -969,8 +969,9 @@ export class OmpAgentSession implements AgentSession {
|
|||||||
private readonly paseoTools?: PaseoToolCatalog;
|
private readonly paseoTools?: PaseoToolCatalog;
|
||||||
private readonly usageEnv: NodeJS.ProcessEnv;
|
private readonly usageEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
getUsageEnvironment() {
|
async getUsageReference() {
|
||||||
return this.usageEnv;
|
const state = await this.runtimeSession.getState();
|
||||||
|
return resolveOmpUsageReference(state.sessionId, state.model?.provider ?? "", this.usageEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
get id(): string | null {
|
get id(): string | null {
|
||||||
@@ -2273,27 +2274,6 @@ export class OmpAgentClient implements AgentClient {
|
|||||||
options.runtime ?? createRuntime(options.logger, runtimeSettings, this.providerParams);
|
options.runtime ?? createRuntime(options.logger, runtimeSettings, this.providerParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference({
|
|
||||||
config,
|
|
||||||
runtimeInfo,
|
|
||||||
persistence,
|
|
||||||
session,
|
|
||||||
}: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
runtimeInfo: AgentRuntimeInfo | null;
|
|
||||||
persistence: AgentPersistenceHandle | null;
|
|
||||||
session: AgentSession | null;
|
|
||||||
}) {
|
|
||||||
const liveInfo = session ? await session.getRuntimeInfo() : null;
|
|
||||||
const model = liveInfo?.model ?? config.model ?? runtimeInfo?.model;
|
|
||||||
const sessionId = liveInfo?.sessionId ?? persistence?.sessionId;
|
|
||||||
const env =
|
|
||||||
session instanceof OmpAgentSession
|
|
||||||
? session.getUsageEnvironment()
|
|
||||||
: { ...process.env, ...this.runtimeSettings?.env };
|
|
||||||
return resolveOmpUsageReference(sessionId ?? "", model?.split("/")[0] ?? "", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async configureNativePaseoTools(
|
private async configureNativePaseoTools(
|
||||||
runtimeSession: OmpRuntimeSession,
|
runtimeSession: OmpRuntimeSession,
|
||||||
catalog: PaseoToolCatalog | undefined,
|
catalog: PaseoToolCatalog | undefined,
|
||||||
|
|||||||
@@ -71,13 +71,11 @@ export class OmpHarness {
|
|||||||
providerIdleScheduler?: OmpProviderIdleScheduler;
|
providerIdleScheduler?: OmpProviderIdleScheduler;
|
||||||
noTurnScheduler?: OmpNoTurnScheduler;
|
noTurnScheduler?: OmpNoTurnScheduler;
|
||||||
usagePollScheduler?: OmpUsagePollScheduler;
|
usagePollScheduler?: OmpUsagePollScheduler;
|
||||||
env?: Record<string, string>;
|
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
this.client = new OmpAgentClient({
|
this.client = new OmpAgentClient({
|
||||||
logger: pino({ level: "silent" }),
|
logger: pino({ level: "silent" }),
|
||||||
runtime: this.omp,
|
runtime: this.omp,
|
||||||
runtimeSettings: options.env ? { env: options.env } : undefined,
|
|
||||||
providerIdleScheduler: options.providerIdleScheduler,
|
providerIdleScheduler: options.providerIdleScheduler,
|
||||||
noTurnScheduler: options.noTurnScheduler,
|
noTurnScheduler: options.noTurnScheduler,
|
||||||
usagePollScheduler: options.usagePollScheduler,
|
usagePollScheduler: options.usagePollScheduler,
|
||||||
@@ -486,24 +484,7 @@ export class OmpHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUsageReference() {
|
async getUsageReference() {
|
||||||
return this.client.resolveUsageReference({
|
return this.requireSession().getUsageReference();
|
||||||
config: { provider: "omp", cwd: CWD },
|
|
||||||
session: this.requireSession(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStoredUsageReference(
|
|
||||||
model: string,
|
|
||||||
sessionId: string,
|
|
||||||
configuredModel: string | null = model,
|
|
||||||
) {
|
|
||||||
const input = {
|
|
||||||
config: { provider: "omp", cwd: CWD, model: configuredModel },
|
|
||||||
runtimeInfo: { provider: "omp", sessionId, model },
|
|
||||||
persistence: { provider: "omp", sessionId },
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
return this.client.resolveUsageReference(input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runningToolCallIds(): string[] {
|
runningToolCallIds(): string[] {
|
||||||
|
|||||||
@@ -420,33 +420,19 @@ describe("OpenCodeAgentClient adapter smoke tests", () => {
|
|||||||
},
|
},
|
||||||
{ serverManager: runtime, createClient: runtime.createClient },
|
{ serverManager: runtime, createClient: runtime.createClient },
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
await client.resolveUsageReference({
|
|
||||||
config: { ...buildConfig(cwd), model: "openai/gpt-5" },
|
|
||||||
session: null,
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
|
||||||
});
|
|
||||||
const session = await client.createSession(buildConfig(cwd));
|
const session = await client.createSession(buildConfig(cwd));
|
||||||
await session.setModel?.("openai/gpt-5");
|
await session.setModel?.("openai/gpt-5");
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "codex",
|
source: "codex",
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
||||||
});
|
});
|
||||||
await session.setModel?.("opencode-go/qwen");
|
await session.setModel?.("opencode-go/qwen");
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "opencode-go",
|
source: "opencode-go",
|
||||||
input: { apiKey: "go-key" },
|
input: { apiKey: "go-key" },
|
||||||
});
|
});
|
||||||
await session.setModel?.("openai/gpt-5");
|
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
|
||||||
});
|
|
||||||
await session.setModel?.("other/model");
|
await session.setModel?.("other/model");
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toBeNull();
|
expect(await session.getUsageReference?.()).toBeNull();
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
session.subscribe((event) => events.push(event));
|
session.subscribe((event) => events.push(event));
|
||||||
const sessionId = (await session.getRuntimeInfo()).sessionId;
|
const sessionId = (await session.getRuntimeInfo()).sessionId;
|
||||||
@@ -462,7 +448,7 @@ describe("OpenCodeAgentClient adapter smoke tests", () => {
|
|||||||
},
|
},
|
||||||
} as OpenCodeEvent);
|
} as OpenCodeEvent);
|
||||||
await vi.waitFor(async () =>
|
await vi.waitFor(async () =>
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "codex",
|
source: "codex",
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
||||||
}),
|
}),
|
||||||
@@ -482,46 +468,6 @@ describe("OpenCodeAgentClient adapter smoke tests", () => {
|
|||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("usage reference survives a model switch and default-model restart", async () => {
|
|
||||||
const cwd = tmpCwd();
|
|
||||||
const client = new OpenCodeAgentClient(logger, {
|
|
||||||
env: {
|
|
||||||
OPENCODE_AUTH_CONTENT: JSON.stringify({
|
|
||||||
openai: { type: "oauth", access: "oauth-token", accountId: "acct-1" },
|
|
||||||
"opencode-go": { type: "api", key: "go-key" },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const runtimeInfo = {
|
|
||||||
provider: "opencode",
|
|
||||||
sessionId: "saved-session",
|
|
||||||
model: "openai/gpt-5",
|
|
||||||
};
|
|
||||||
const closed = {
|
|
||||||
config: { ...buildConfig(cwd), model: "openai/gpt-5" },
|
|
||||||
runtimeInfo,
|
|
||||||
persistence: null,
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
expect(await client.resolveUsageReference(closed)).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
await client.resolveUsageReference({
|
|
||||||
...closed,
|
|
||||||
config: { ...buildConfig(cwd), model: null },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "oauth-token", accountId: "acct-1" },
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("usage reference ignores another auth entry with a different type", async () => {
|
test("usage reference ignores another auth entry with a different type", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const runtime = new TestOpenCodeHarness();
|
const runtime = new TestOpenCodeHarness();
|
||||||
@@ -541,7 +487,7 @@ describe("OpenCodeAgentClient adapter smoke tests", () => {
|
|||||||
const session = await client.createSession(buildConfig(cwd));
|
const session = await client.createSession(buildConfig(cwd));
|
||||||
try {
|
try {
|
||||||
await session.setModel?.("opencode-go/qwen");
|
await session.setModel?.("opencode-go/qwen");
|
||||||
expect(await client.resolveUsageReference({ config: buildConfig(cwd), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "opencode-go",
|
source: "opencode-go",
|
||||||
input: { apiKey: "go-key" },
|
input: { apiKey: "go-key" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1445,26 +1445,6 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
this.resolveHomeDir = deps.resolveHomeDir ?? resolveOpenCodeHomeDir;
|
this.resolveHomeDir = deps.resolveHomeDir ?? resolveOpenCodeHomeDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference({
|
|
||||||
config,
|
|
||||||
runtimeInfo,
|
|
||||||
session,
|
|
||||||
}: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
runtimeInfo: AgentRuntimeInfo | null;
|
|
||||||
session: AgentSession | null;
|
|
||||||
}): Promise<UsageReference | null> {
|
|
||||||
const model = session
|
|
||||||
? (await session.getRuntimeInfo()).model
|
|
||||||
: (config.model ?? runtimeInfo?.model);
|
|
||||||
if (!model?.startsWith("openai/") && !model?.startsWith("opencode-go/")) return null;
|
|
||||||
const env =
|
|
||||||
session instanceof OpenCodeAgentSession
|
|
||||||
? session.getUsageEnvironment()
|
|
||||||
: { ...process.env, ...this.runtimeSettings?.env };
|
|
||||||
return resolveOpenCodeUsageReference(model, await readOpenCodeUsageAuth(env));
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
@@ -3640,8 +3620,11 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsageEnvironment(): NodeJS.ProcessEnv {
|
async getUsageReference(): Promise<UsageReference | null> {
|
||||||
return this.usageEnv;
|
const model = this.config.model;
|
||||||
|
if (!model?.startsWith("openai/") && !model?.startsWith("opencode-go/")) return null;
|
||||||
|
const auth = await readOpenCodeUsageAuth(this.usageEnv);
|
||||||
|
return resolveOpenCodeUsageReference(model, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setModel(modelId: string | null): Promise<void> {
|
async setModel(modelId: string | null): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
||||||
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createTestLogger } from "../../../../../test-utils/test-logger.js";
|
||||||
|
import { V2Runtime } from "./runtime.js";
|
||||||
|
|
||||||
|
// Answers every request at once except session.wait, which it holds for WAIT_MS before answering,
|
||||||
|
// the way OpenCode holds the wait response until the session's agent loop is idle.
|
||||||
|
const FAKE_HELPER = `
|
||||||
|
import http from "node:http";
|
||||||
|
const server = http.createServer((request, response) => {
|
||||||
|
if (request.url.endsWith("/wait")) {
|
||||||
|
setTimeout(() => { response.statusCode = 204; response.end(); }, Number(process.env.WAIT_MS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.setHeader("content-type", "application/json");
|
||||||
|
response.end("{}");
|
||||||
|
});
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
console.log("server listening on http://127.0.0.1:" + server.address().port);
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
let root: string;
|
||||||
|
const defaultDispatcher = getGlobalDispatcher();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-v2-runtime-"));
|
||||||
|
vi.stubEnv("PASEO_HOME", path.join(root, "home"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
setGlobalDispatcher(defaultDispatcher);
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("session.wait outlasts the fetch header deadline until the helper reports the session idle", async () => {
|
||||||
|
// Stands in for undici's five-minute default, which ended every longer turn.
|
||||||
|
setGlobalDispatcher(new Agent({ headersTimeout: 200 }));
|
||||||
|
const helper = path.join(root, "fake-opencode.mjs");
|
||||||
|
await writeFile(helper, FAKE_HELPER);
|
||||||
|
const runtime = new V2Runtime({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
settings: {
|
||||||
|
command: { mode: "replace", argv: [process.execPath, helper] },
|
||||||
|
env: { WAIT_MS: "2500" },
|
||||||
|
},
|
||||||
|
decorateEnv: async (env) => env,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const connection = await runtime.acquire();
|
||||||
|
await expect(connection.client.session.wait({ sessionID: "ses_long" })).resolves.toBe(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await runtime.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import type { V2Api } from "./api.js";
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { mkdir } from "node:fs/promises";
|
import { mkdir } from "node:fs/promises";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import { Agent } from "undici";
|
||||||
import { spawnProcess } from "../../../../../utils/spawn.js";
|
import { spawnProcess } from "../../../../../utils/spawn.js";
|
||||||
import { terminateWithTreeKill } from "../../../../../utils/tree-kill.js";
|
import { terminateWithTreeKill } from "../../../../../utils/tree-kill.js";
|
||||||
import {
|
import {
|
||||||
@@ -174,6 +175,9 @@ export class V2Runtime {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const processAbort = new AbortController();
|
const processAbort = new AbortController();
|
||||||
|
// session.wait answers only once the session is idle, so a header deadline would fail any
|
||||||
|
// turn that outlasts it. Requests end when the helper exits or the caller aborts instead.
|
||||||
|
const dispatcher = new Agent({ headersTimeout: 0 });
|
||||||
const exited = new Promise<Error>((resolve) =>
|
const exited = new Promise<Error>((resolve) =>
|
||||||
process.once("exit", (code) => {
|
process.once("exit", (code) => {
|
||||||
const error = new Error(`OpenCode helper server exited (${code})`);
|
const error = new Error(`OpenCode helper server exited (${code})`);
|
||||||
@@ -199,6 +203,7 @@ export class V2Runtime {
|
|||||||
const stop = () => {
|
const stop = () => {
|
||||||
stopped ??= (async () => {
|
stopped ??= (async () => {
|
||||||
await terminateWithTreeKill(process, { gracefulTimeoutMs: 5_000, forceTimeoutMs: 1_000 });
|
await terminateWithTreeKill(process, { gracefulTimeoutMs: 5_000, forceTimeoutMs: 1_000 });
|
||||||
|
await dispatcher.destroy();
|
||||||
const entry = await record;
|
const entry = await record;
|
||||||
if (entry) await managedProcesses?.remove(entry.id);
|
if (entry) await managedProcesses?.remove(entry.id);
|
||||||
})();
|
})();
|
||||||
@@ -243,7 +248,9 @@ export class V2Runtime {
|
|||||||
const signal = init?.signal
|
const signal = init?.signal
|
||||||
? AbortSignal.any([init.signal, processAbort.signal])
|
? AbortSignal.any([init.signal, processAbort.signal])
|
||||||
: processAbort.signal;
|
: processAbort.signal;
|
||||||
const response = await fetch(request, { ...init, signal });
|
// Node's fetch accepts an undici dispatcher, which the DOM RequestInit type omits.
|
||||||
|
const requestInit: RequestInit & { dispatcher: Agent } = { ...init, signal, dispatcher };
|
||||||
|
const response = await fetch(request, requestInit);
|
||||||
const html = response.headers.get("content-type")?.includes("text/html") ?? false;
|
const html = response.headers.get("content-type")?.includes("text/html") ?? false;
|
||||||
if (!response.ok || html) {
|
if (!response.ok || html) {
|
||||||
const requestUrl = request instanceof Request ? request.url : String(request);
|
const requestUrl = request instanceof Request ? request.url : String(request);
|
||||||
|
|||||||
@@ -229,16 +229,15 @@ test("Pi usage reference follows the current OAuth model and agent directory", a
|
|||||||
runtime = sessionRuntime;
|
runtime = sessionRuntime;
|
||||||
runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" };
|
runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" };
|
||||||
});
|
});
|
||||||
const client = createClient(pi);
|
const session = await createClient(pi).createSession(createConfig(), {
|
||||||
const session = await client.createSession(createConfig(), {
|
|
||||||
env: { PI_CODING_AGENT_DIR: agentDir },
|
env: { PI_CODING_AGENT_DIR: agentDir },
|
||||||
});
|
});
|
||||||
expect(await client.resolveUsageReference({ config: createConfig(), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "codex",
|
source: "codex",
|
||||||
input: { accessToken: "codex-token", accountId: "account-1" },
|
input: { accessToken: "codex-token", accountId: "account-1" },
|
||||||
});
|
});
|
||||||
runtime.state.model = { provider: "anthropic", id: "claude", name: "Claude" };
|
runtime.state.model = { provider: "anthropic", id: "claude", name: "Claude" };
|
||||||
expect(await client.resolveUsageReference({ config: createConfig(), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "claude",
|
source: "claude",
|
||||||
input: { accessToken: "claude-token" },
|
input: { accessToken: "claude-token" },
|
||||||
});
|
});
|
||||||
@@ -250,7 +249,7 @@ test("Pi usage reference follows the current OAuth model and agent directory", a
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" };
|
runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" };
|
||||||
expect(await client.resolveUsageReference({ config: createConfig(), session })).toEqual({
|
expect(await session.getUsageReference?.()).toEqual({
|
||||||
source: "codex",
|
source: "codex",
|
||||||
input: { accessToken: "codex-token" },
|
input: { accessToken: "codex-token" },
|
||||||
});
|
});
|
||||||
@@ -259,65 +258,15 @@ test("Pi usage reference follows the current OAuth model and agent directory", a
|
|||||||
path.join(agentDir, "auth.json"),
|
path.join(agentDir, "auth.json"),
|
||||||
JSON.stringify({ anthropic: { type: "api_key", key: "api-key" } }),
|
JSON.stringify({ anthropic: { type: "api_key", key: "api-key" } }),
|
||||||
);
|
);
|
||||||
expect(await client.resolveUsageReference({ config: createConfig(), session })).toBeNull();
|
expect(await session.getUsageReference?.()).toBeNull();
|
||||||
runtime.state.model = { provider: "other", id: "other", name: "Other" };
|
runtime.state.model = { provider: "other", id: "other", name: "Other" };
|
||||||
expect(await client.resolveUsageReference({ config: createConfig(), session })).toBeNull();
|
expect(await session.getUsageReference?.()).toBeNull();
|
||||||
await session.close();
|
await session.close();
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(agentDir, { recursive: true, force: true });
|
rmSync(agentDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Pi resolves a stored model without opening a session", async () => {
|
|
||||||
const agentDir = mkdtempSync(path.join(tmpdir(), "paseo-pi-stored-usage-"));
|
|
||||||
try {
|
|
||||||
writeFileSync(
|
|
||||||
path.join(agentDir, "auth.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
anthropic: { type: "oauth", access: "claude-token", refresh: "refresh" },
|
|
||||||
"openai-codex": {
|
|
||||||
type: "oauth",
|
|
||||||
access: "codex-token",
|
|
||||||
accountId: "account-1",
|
|
||||||
refresh: "refresh",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const client = new PiRpcAgentClient({
|
|
||||||
logger: pino({ level: "silent" }),
|
|
||||||
runtime: new FakePi(),
|
|
||||||
runtimeSettings: { env: { PI_CODING_AGENT_DIR: agentDir } },
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
await client.resolveUsageReference({
|
|
||||||
config: createConfig({ model: "anthropic/claude-sonnet-4" }),
|
|
||||||
session: null,
|
|
||||||
}),
|
|
||||||
).toEqual({ source: "claude", input: { accessToken: "claude-token" } });
|
|
||||||
const runtimeInfo = {
|
|
||||||
provider: "pi",
|
|
||||||
sessionId: "saved-session",
|
|
||||||
model: "openai-codex/gpt-5",
|
|
||||||
};
|
|
||||||
const closed = {
|
|
||||||
config: createConfig({ model: "openai-codex/gpt-5" }),
|
|
||||||
runtimeInfo,
|
|
||||||
persistence: null,
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
expect(await client.resolveUsageReference(closed)).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "codex-token", accountId: "account-1" },
|
|
||||||
});
|
|
||||||
expect(await client.resolveUsageReference({ ...closed, config: createConfig() })).toEqual({
|
|
||||||
source: "codex",
|
|
||||||
input: { accessToken: "codex-token", accountId: "account-1" },
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
rmSync(agentDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("forwards launch-context env to the Pi process launch", async () => {
|
test("forwards launch-context env to the Pi process launch", async () => {
|
||||||
const pi = new FakePi();
|
const pi = new FakePi();
|
||||||
const client = createClient(pi);
|
const client = createClient(pi);
|
||||||
|
|||||||
@@ -1141,14 +1141,20 @@ function createRuntime(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolvePiUsageReference(provider: string | undefined, env?: Record<string, string>) {
|
export class PiRpcAgentSession implements AgentSession {
|
||||||
|
readonly provider: AgentProvider;
|
||||||
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
|
|
||||||
|
async getUsageReference() {
|
||||||
|
await this.refreshState();
|
||||||
|
const provider = this.state.model?.provider;
|
||||||
let source: string | null = null;
|
let source: string | null = null;
|
||||||
if (provider === "openai-codex") source = "codex";
|
if (provider === "openai-codex") source = "codex";
|
||||||
if (provider === "anthropic") source = "claude";
|
if (provider === "anthropic") source = "claude";
|
||||||
if (!source || !provider) return null;
|
if (!source || !provider) return null;
|
||||||
try {
|
try {
|
||||||
const auth: unknown = JSON.parse(
|
const auth: unknown = JSON.parse(
|
||||||
await readFile(join(resolvePiAgentDir(env), "auth.json"), "utf8"),
|
await readFile(join(resolvePiAgentDir(this.usageEnv), "auth.json"), "utf8"),
|
||||||
);
|
);
|
||||||
if (provider === "openai-codex") {
|
if (provider === "openai-codex") {
|
||||||
const credential = piCodexUsageAuthSchema.parse(auth)["openai-codex"];
|
const credential = piCodexUsageAuthSchema.parse(auth)["openai-codex"];
|
||||||
@@ -1168,14 +1174,6 @@ async function resolvePiUsageReference(provider: string | undefined, env?: Recor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PiRpcAgentSession implements AgentSession {
|
|
||||||
readonly provider: AgentProvider;
|
|
||||||
readonly capabilities: AgentCapabilityFlags;
|
|
||||||
|
|
||||||
getUsageEnvironment() {
|
|
||||||
return this.usageEnv;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
||||||
private readonly activeToolCalls = new Map<string, PiTrackedToolCall>();
|
private readonly activeToolCalls = new Map<string, PiTrackedToolCall>();
|
||||||
private readonly pendingExtensionUiRequests = new Map<string, AgentPermissionRequest>();
|
private readonly pendingExtensionUiRequests = new Map<string, AgentPermissionRequest>();
|
||||||
@@ -2479,25 +2477,6 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
this.usagePollScheduler = options.usagePollScheduler;
|
this.usagePollScheduler = options.usagePollScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUsageReference({
|
|
||||||
config,
|
|
||||||
runtimeInfo,
|
|
||||||
session,
|
|
||||||
}: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
runtimeInfo: AgentRuntimeInfo | null;
|
|
||||||
session: AgentSession | null;
|
|
||||||
}) {
|
|
||||||
const model = session
|
|
||||||
? (await session.getRuntimeInfo()).model
|
|
||||||
: (config.model ?? runtimeInfo?.model);
|
|
||||||
const env =
|
|
||||||
session instanceof PiRpcAgentSession
|
|
||||||
? session.getUsageEnvironment()
|
|
||||||
: this.runtimeSettings?.env;
|
|
||||||
return resolvePiUsageReference(model?.split("/")[0], env);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import pino from "pino";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { AgentStorage } from "../agent/agent-storage.js";
|
import { ACPAgentSession } from "../agent/providers/acp-agent.js";
|
||||||
import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js";
|
import type { UsageReference } from "../agent/agent-sdk-types.js";
|
||||||
import { PiRpcAgentClient } from "../agent/providers/pi/agent.js";
|
import { ClaudeAgentClient } from "../agent/providers/claude/agent.js";
|
||||||
|
import { CodexAppServerAgentSession } from "../agent/providers/codex-app-server-agent.js";
|
||||||
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { DaemonClient } from "../test-utils/daemon-client.js";
|
import { DaemonClient } from "../test-utils/daemon-client.js";
|
||||||
import { createTestAgentClient } from "../test-utils/fake-agent-client.js";
|
import { createTestAgentClient } from "../test-utils/fake-agent-client.js";
|
||||||
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
||||||
@@ -56,21 +53,75 @@ test("resolves a provider plugin session reference through its usage source", as
|
|||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
test("agent.resolve_usage_report resolves source IDs from provider clients", async () => {
|
test("agent.resolve_usage_report resolves source IDs from default built-in and ACP sessions", async () => {
|
||||||
const directory = fileURLToPath(
|
const directory = fileURLToPath(
|
||||||
new URL("./test-fixtures/session-usage-reference/", import.meta.url),
|
new URL("./test-fixtures/session-usage-reference/", import.meta.url),
|
||||||
);
|
);
|
||||||
const providers = ["claude", "codex", "copilot", "cursor", "kimi", "generic-acp"] as const;
|
const providers = ["claude", "codex", "copilot", "cursor", "kimi", "generic-acp"] as const;
|
||||||
|
const logger = createTestLogger();
|
||||||
|
const references = new Map<string, UsageReference | null>();
|
||||||
|
const claude = await new ClaudeAgentClient({
|
||||||
|
logger,
|
||||||
|
resolveBinary: async () => "/test/claude/bin",
|
||||||
|
}).createSession(
|
||||||
|
{ provider: "claude", cwd: directory },
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
HOME: directory,
|
||||||
|
CLAUDE_CONFIG_DIR: "",
|
||||||
|
ANTHROPIC_BASE_URL: "",
|
||||||
|
ANTHROPIC_API_KEY: "",
|
||||||
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
references.set("claude", (await claude.getUsageReference?.()) ?? null);
|
||||||
|
await claude.close();
|
||||||
|
const codex = new CodexAppServerAgentSession(
|
||||||
|
{ provider: "codex", cwd: directory },
|
||||||
|
null,
|
||||||
|
logger,
|
||||||
|
() => {
|
||||||
|
throw new Error("Codex runtime should not start");
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
"interactive",
|
||||||
|
{ HOME: directory, OPENAI_BASE_URL: "" },
|
||||||
|
);
|
||||||
|
references.set("codex", await codex.getUsageReference());
|
||||||
|
for (const provider of providers.slice(2)) {
|
||||||
|
const session = new ACPAgentSession(
|
||||||
|
{ provider, cwd: directory },
|
||||||
|
{
|
||||||
|
provider,
|
||||||
|
logger,
|
||||||
|
defaultCommand: ["unused"],
|
||||||
|
defaultModes: [],
|
||||||
|
capabilities: {
|
||||||
|
supportsStreaming: true,
|
||||||
|
supportsSessionPersistence: true,
|
||||||
|
supportsDynamicModes: true,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
supportsReasoningStream: true,
|
||||||
|
supportsToolInvocations: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
references.set(provider, await session.getUsageReference());
|
||||||
|
}
|
||||||
const agentClients = Object.fromEntries(
|
const agentClients = Object.fromEntries(
|
||||||
providers.map((provider) => {
|
providers.map((provider) => {
|
||||||
const client = createTestAgentClient(provider);
|
const client = createTestAgentClient(provider);
|
||||||
let input: Record<string, string> = {};
|
const createSession = client.createSession.bind(client);
|
||||||
if (provider === "claude") input = { configDir: directory };
|
client.createSession = async (...args) => {
|
||||||
if (provider === "codex") input = { codexHome: directory };
|
const session = await createSession(...args);
|
||||||
client.resolveUsageReference = async () => ({
|
session.getUsageReference = async () => references.get(provider) ?? null;
|
||||||
source: provider,
|
return session;
|
||||||
input,
|
};
|
||||||
});
|
|
||||||
return [provider, client];
|
return [provider, client];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -97,156 +148,3 @@ test("agent.resolve_usage_report resolves source IDs from provider clients", asy
|
|||||||
await daemon.close();
|
await daemon.close();
|
||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
test("stored Claude and Codex agents resolve without opening a provider session", async () => {
|
|
||||||
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-usage-closed-"));
|
|
||||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-usage-static-"));
|
|
||||||
const sessionsOpened = new Map<string, number>();
|
|
||||||
const agentClients = Object.fromEntries(
|
|
||||||
["claude", "codex"].map((provider) => {
|
|
||||||
const providerClient = createTestAgentClient(provider, {
|
|
||||||
beforeCreateSession: async () => {
|
|
||||||
sessionsOpened.set(provider, (sessionsOpened.get(provider) ?? 0) + 1);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
providerClient.resolveUsageReference = async ({ session, runtimeInfo, persistence }) => {
|
|
||||||
expect(session).toBeNull();
|
|
||||||
expect(runtimeInfo?.sessionId).toBeTruthy();
|
|
||||||
expect(runtimeInfo?.model).toBe(`${provider}/saved-model`);
|
|
||||||
expect(persistence?.sessionId).toBe(runtimeInfo?.sessionId);
|
|
||||||
return { source: "fixture-session-usage", input: { account: "from-session" } };
|
|
||||||
};
|
|
||||||
return [provider, providerClient];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const options = {
|
|
||||||
paseoHomeRoot: root,
|
|
||||||
staticDir,
|
|
||||||
cleanup: false,
|
|
||||||
pluginsEnabled: false,
|
|
||||||
builtinPlugins: fixtureBuiltins(),
|
|
||||||
agentClients,
|
|
||||||
};
|
|
||||||
let daemon = await createTestPaseoDaemon(options);
|
|
||||||
let client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
const ids: string[] = [];
|
|
||||||
for (const provider of ["claude", "codex"]) {
|
|
||||||
const agent = await client.createAgent({ provider, cwd: root });
|
|
||||||
ids.push(agent.id);
|
|
||||||
await client.sendMessage(agent.id, "Say OK.");
|
|
||||||
await client.waitForAgentUpsert(agent.id, (snapshot) => snapshot.status === "idle");
|
|
||||||
expect(sessionsOpened.get(provider)).toBe(1);
|
|
||||||
}
|
|
||||||
await client.close();
|
|
||||||
await daemon.close();
|
|
||||||
|
|
||||||
const storage = new AgentStorage(
|
|
||||||
path.join(root, ".paseo", "agents"),
|
|
||||||
pino({ level: "silent" }),
|
|
||||||
);
|
|
||||||
for (const [index, provider] of ["claude", "codex"].entries()) {
|
|
||||||
const record = await storage.get(ids[index]!);
|
|
||||||
expect(record).not.toBeNull();
|
|
||||||
await storage.upsert({
|
|
||||||
...record!,
|
|
||||||
runtimeInfo: { ...record!.runtimeInfo!, model: `${provider}/saved-model` },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
daemon = await createTestPaseoDaemon(options);
|
|
||||||
client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
|
||||||
await client.connect();
|
|
||||||
await client.fetchAgents({ subscribe: {} });
|
|
||||||
for (const [index, provider] of ["claude", "codex"].entries()) {
|
|
||||||
const result = await client.resolveAgentUsageReport({ agentId: ids[index]! });
|
|
||||||
expect(result.reportId).toBe("fixture-session-usage:from-session");
|
|
||||||
expect(
|
|
||||||
(await client.listUsageReports({ reportIds: [result.reportId!] })).reports,
|
|
||||||
).toHaveLength(1);
|
|
||||||
expect(sessionsOpened.get(provider)).toBe(1);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await client.close().catch(() => undefined);
|
|
||||||
await daemon.close().catch(() => undefined);
|
|
||||||
await rm(root, { recursive: true, force: true });
|
|
||||||
await rm(staticDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("stored default-model OpenCode and Pi agents resolve their saved runtime model", async () => {
|
|
||||||
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-usage-saved-model-"));
|
|
||||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-usage-static-"));
|
|
||||||
const piDir = path.join(root, "pi-agent");
|
|
||||||
await mkdir(piDir);
|
|
||||||
await writeFile(
|
|
||||||
path.join(piDir, "auth.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
"openai-codex": {
|
|
||||||
type: "oauth",
|
|
||||||
access: "codex-fixture-token",
|
|
||||||
accountId: "fixture-account",
|
|
||||||
refresh: "fixture-refresh",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const storage = new AgentStorage(path.join(root, ".paseo", "agents"), pino({ level: "silent" }));
|
|
||||||
const ids: string[] = [];
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
for (const provider of ["opencode", "pi"]) {
|
|
||||||
const id = randomUUID();
|
|
||||||
ids.push(id);
|
|
||||||
await storage.upsert({
|
|
||||||
id,
|
|
||||||
provider,
|
|
||||||
cwd: root,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
labels: {},
|
|
||||||
lastStatus: "closed",
|
|
||||||
config: { model: null },
|
|
||||||
runtimeInfo: {
|
|
||||||
provider,
|
|
||||||
sessionId: `${provider}-saved-session`,
|
|
||||||
model: provider === "opencode" ? "openai/gpt-5" : "openai-codex/gpt-5",
|
|
||||||
},
|
|
||||||
persistence: { provider, sessionId: `${provider}-saved-session` },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const daemon = await createTestPaseoDaemon({
|
|
||||||
paseoHomeRoot: root,
|
|
||||||
staticDir,
|
|
||||||
cleanup: false,
|
|
||||||
agentClients: {
|
|
||||||
opencode: new OpenCodeAgentClient(pino({ level: "silent" }), {
|
|
||||||
env: {
|
|
||||||
OPENCODE_AUTH_CONTENT: JSON.stringify({
|
|
||||||
openai: { type: "oauth", access: "codex-fixture-token", accountId: "fixture-account" },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
pi: new PiRpcAgentClient({
|
|
||||||
logger: pino({ level: "silent" }),
|
|
||||||
runtimeSettings: { env: { PI_CODING_AGENT_DIR: piDir } },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
builtinPlugins: new BuiltinPluginLoader(undefined, ["codex-usage-source"]),
|
|
||||||
});
|
|
||||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
await client.fetchAgents({ subscribe: {} });
|
|
||||||
for (const [index, id] of ids.entries()) {
|
|
||||||
expect(
|
|
||||||
(await client.resolveAgentUsageReport({ agentId: id })).reportId,
|
|
||||||
["opencode", "pi"][index],
|
|
||||||
).not.toBeNull();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await client.close();
|
|
||||||
await daemon.close();
|
|
||||||
await rm(root, { recursive: true, force: true });
|
|
||||||
await rm(staticDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|||||||
@@ -1000,7 +1000,7 @@ export class Session {
|
|||||||
this.usageSession = new UsageSession({
|
this.usageSession = new UsageSession({
|
||||||
emit: (msg) => this.emit(msg),
|
emit: (msg) => this.emit(msg),
|
||||||
listAgents: () => this.agentManager.listAgents(),
|
listAgents: () => this.agentManager.listAgents(),
|
||||||
resolveAgentReference: (agentId) => this.agentManager.resolveUsageReference(agentId),
|
getAgent: (agentId) => this.agentManager.getAgent(agentId),
|
||||||
runtime: pluginRuntime,
|
runtime: pluginRuntime,
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import type { AgentSession } from "../../agent/agent-sdk-types.js";
|
||||||
import type { SessionOutboundMessage } from "../../messages.js";
|
import type { SessionOutboundMessage } from "../../messages.js";
|
||||||
import { UsageSession } from "./usage-session.js";
|
import { UsageSession } from "./usage-session.js";
|
||||||
|
|
||||||
@@ -8,8 +9,9 @@ test("collects live references for usage reports and resolves an agent report",
|
|||||||
const references: unknown[] = [];
|
const references: unknown[] = [];
|
||||||
const reference = { source: "fixture", input: { account: "one" } };
|
const reference = { source: "fixture", input: { account: "one" } };
|
||||||
const agent = {
|
const agent = {
|
||||||
id: "one",
|
session: {
|
||||||
session: {},
|
getUsageReference: async () => reference,
|
||||||
|
} as AgentSession,
|
||||||
};
|
};
|
||||||
const entry = {
|
const entry = {
|
||||||
id: "fixture:one",
|
id: "fixture:one",
|
||||||
@@ -21,8 +23,8 @@ test("collects live references for usage reports and resolves an agent report",
|
|||||||
};
|
};
|
||||||
const usage = new UsageSession({
|
const usage = new UsageSession({
|
||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
listAgents: () => [agent, { id: "closed", session: null }],
|
listAgents: () => [agent, { session: null }],
|
||||||
resolveAgentReference: async (id) => ({ found: id === "one", reference }),
|
getAgent: (id) => (id === "one" ? agent : null),
|
||||||
runtime: {
|
runtime: {
|
||||||
async listUsageReports(options) {
|
async listUsageReports(options) {
|
||||||
references.push(options.references);
|
references.push(options.references);
|
||||||
@@ -57,7 +59,7 @@ test("surfaces a legacy usage-list failure as an rpc_error envelope", async () =
|
|||||||
const usage = new UsageSession({
|
const usage = new UsageSession({
|
||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
listAgents: () => [],
|
listAgents: () => [],
|
||||||
resolveAgentReference: async () => ({ found: false, reference: null }),
|
getAgent: () => null,
|
||||||
runtime: {
|
runtime: {
|
||||||
async listUsageReports() {
|
async listUsageReports() {
|
||||||
return [];
|
return [];
|
||||||
@@ -83,7 +85,7 @@ test("unknown agent returns agent_not_found", async () => {
|
|||||||
const usage = new UsageSession({
|
const usage = new UsageSession({
|
||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
listAgents: () => [],
|
listAgents: () => [],
|
||||||
resolveAgentReference: async () => ({ found: false, reference: null }),
|
getAgent: () => null,
|
||||||
runtime: {
|
runtime: {
|
||||||
listUsageReports: async () => [],
|
listUsageReports: async () => [],
|
||||||
resolveUsageReference: async () => null,
|
resolveUsageReference: async () => null,
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
import type { ProviderUsage, UsageReportEntry } from "@getpaseo/protocol/messages";
|
import type { ProviderUsage, UsageReportEntry } from "@getpaseo/protocol/messages";
|
||||||
import type { UsageReference } from "../../agent/agent-sdk-types.js";
|
import type { AgentSession, UsageReference } from "../../agent/agent-sdk-types.js";
|
||||||
import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js";
|
import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js";
|
||||||
|
|
||||||
interface UsageAgent {
|
interface UsageAgent {
|
||||||
id: string;
|
session: AgentSession | null;
|
||||||
session: object | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsageSessionOptions {
|
export interface UsageSessionOptions {
|
||||||
emit(message: SessionOutboundMessage): void;
|
emit(message: SessionOutboundMessage): void;
|
||||||
listAgents(): UsageAgent[];
|
listAgents(): UsageAgent[];
|
||||||
resolveAgentReference(agentId: string): Promise<{
|
getAgent(agentId: string): UsageAgent | null;
|
||||||
found: boolean;
|
|
||||||
reference: UsageReference | null;
|
|
||||||
}>;
|
|
||||||
runtime?: {
|
runtime?: {
|
||||||
listUsageReports(options: {
|
listUsageReports(options: {
|
||||||
forceRefresh?: boolean;
|
forceRefresh?: boolean;
|
||||||
@@ -41,12 +37,8 @@ export class UsageSession {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.options
|
this.options
|
||||||
.listAgents()
|
.listAgents()
|
||||||
.filter((agent) => agent.session !== null)
|
.map(
|
||||||
.map(async (agent) =>
|
async (agent) => agent.session?.getUsageReference?.().catch(() => null) ?? null,
|
||||||
this.options
|
|
||||||
.resolveAgentReference(agent.id)
|
|
||||||
.then((resolved) => resolved.reference)
|
|
||||||
.catch(() => null),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
).filter((reference): reference is UsageReference => reference !== null)
|
).filter((reference): reference is UsageReference => reference !== null)
|
||||||
@@ -70,12 +62,12 @@ export class UsageSession {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!this.options.runtime) throw new Error("Plugin runtime is unavailable");
|
if (!this.options.runtime) throw new Error("Plugin runtime is unavailable");
|
||||||
const resolved = await this.options.resolveAgentReference(msg.agentId);
|
const agent = this.options.getAgent(msg.agentId);
|
||||||
if (!resolved.found) {
|
if (!agent) {
|
||||||
this.emitError(msg, new Error(`Agent not found: ${msg.agentId}`), "agent_not_found");
|
this.emitError(msg, new Error(`Agent not found: ${msg.agentId}`), "agent_not_found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reference = resolved.reference;
|
const reference = (await agent.session?.getUsageReference?.()) ?? null;
|
||||||
const reportId = reference
|
const reportId = reference
|
||||||
? await this.options.runtime.resolveUsageReference(reference)
|
? await this.options.runtime.resolveUsageReference(reference)
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
Reference in New Issue
Block a user