Add dev mock load-test provider
This commit is contained in:
@@ -116,6 +116,16 @@ const OPENCODE_MODES: AgentProviderModeDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_LOAD_TEST_MODES: AgentProviderModeDefinition[] = [
|
||||
{
|
||||
id: "load-test",
|
||||
label: "Load Test",
|
||||
description: "Streams repeated markdown, reasoning, and tool calls for app stress testing",
|
||||
icon: "ShieldOff",
|
||||
colorTier: "dangerous",
|
||||
},
|
||||
];
|
||||
|
||||
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
||||
{
|
||||
id: "claude",
|
||||
@@ -168,9 +178,23 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEV_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
||||
{
|
||||
id: "mock",
|
||||
label: "Mock Load Test",
|
||||
description:
|
||||
"Development-only provider that emits synthetic agent traffic for performance tests",
|
||||
defaultModeId: "load-test",
|
||||
modes: MOCK_LOAD_TEST_MODES,
|
||||
},
|
||||
];
|
||||
|
||||
export function getAgentProviderDefinition(
|
||||
provider: string,
|
||||
definitions: AgentProviderDefinition[] = AGENT_PROVIDER_DEFINITIONS,
|
||||
definitions: AgentProviderDefinition[] = [
|
||||
...AGENT_PROVIDER_DEFINITIONS,
|
||||
...DEV_AGENT_PROVIDER_DEFINITIONS,
|
||||
],
|
||||
): AgentProviderDefinition {
|
||||
const definition = definitions.find((entry) => entry.id === provider);
|
||||
if (!definition) {
|
||||
|
||||
@@ -332,6 +332,19 @@ describe("buildProviderRegistry", () => {
|
||||
expect(Object.keys(registry)).toHaveLength(AGENT_PROVIDER_DEFINITIONS.length);
|
||||
});
|
||||
|
||||
test("includes mock provider only for development builds", () => {
|
||||
expect(buildProviderRegistry(logger).mock).toBeUndefined();
|
||||
expect(buildProviderRegistry(logger, { isDev: false }).mock).toBeUndefined();
|
||||
|
||||
const registry = buildProviderRegistry(logger, { isDev: true });
|
||||
|
||||
expect(registry.mock).toMatchObject({
|
||||
id: "mock",
|
||||
label: "Mock Load Test",
|
||||
defaultModeId: "load-test",
|
||||
});
|
||||
});
|
||||
|
||||
test("built-in override applies command", () => {
|
||||
buildProviderRegistry(logger, {
|
||||
providerOverrides: {
|
||||
|
||||
@@ -27,9 +27,11 @@ import { CopilotACPAgentClient } from "./providers/copilot-acp-agent.js";
|
||||
import { GenericACPAgentClient } from "./providers/generic-acp-agent.js";
|
||||
import { OpenCodeAgentClient, OpenCodeServerManager } from "./providers/opencode-agent.js";
|
||||
import { PiDirectAgentClient } from "./providers/pi-direct-agent.js";
|
||||
import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js";
|
||||
import {
|
||||
AGENT_PROVIDER_DEFINITIONS,
|
||||
BUILTIN_PROVIDER_IDS,
|
||||
DEV_AGENT_PROVIDER_DEFINITIONS,
|
||||
getAgentProviderDefinition,
|
||||
type AgentProviderDefinition,
|
||||
} from "./provider-manifest.js";
|
||||
@@ -48,6 +50,7 @@ export type BuildProviderRegistryOptions = {
|
||||
runtimeSettings?: AgentProviderRuntimeSettingsMap;
|
||||
providerOverrides?: Record<string, ProviderOverride>;
|
||||
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
|
||||
isDev?: boolean;
|
||||
};
|
||||
|
||||
type ProviderClientFactory = (
|
||||
@@ -85,6 +88,7 @@ const PROVIDER_CLIENT_FACTORIES: Record<string, ProviderClientFactory> = {
|
||||
logger,
|
||||
runtimeSettings,
|
||||
}),
|
||||
mock: (logger) => new MockLoadTestAgentClient(logger),
|
||||
};
|
||||
|
||||
function getProviderClientFactory(provider: string): ProviderClientFactory {
|
||||
@@ -349,10 +353,15 @@ function buildResolvedBuiltinProviders(
|
||||
providerOverrides: Record<string, ProviderOverride>,
|
||||
runtimeSettings: AgentProviderRuntimeSettingsMap | undefined,
|
||||
options: Pick<BuildProviderRegistryOptions, "workspaceGitService">,
|
||||
isDev: boolean,
|
||||
): Map<string, ResolvedProvider> {
|
||||
const resolvedProviders = new Map<string, ResolvedProvider>();
|
||||
|
||||
for (const definition of AGENT_PROVIDER_DEFINITIONS) {
|
||||
const definitions = isDev
|
||||
? [...AGENT_PROVIDER_DEFINITIONS, ...DEV_AGENT_PROVIDER_DEFINITIONS]
|
||||
: AGENT_PROVIDER_DEFINITIONS;
|
||||
|
||||
for (const definition of definitions) {
|
||||
const override = providerOverrides[definition.id];
|
||||
const factory = getProviderClientFactory(definition.id);
|
||||
const mergedRuntimeSettings = mergeRuntimeSettings(
|
||||
@@ -380,7 +389,7 @@ function addDerivedProviders(
|
||||
providerOverrides: Record<string, ProviderOverride>,
|
||||
): void {
|
||||
for (const [providerId, override] of Object.entries(providerOverrides)) {
|
||||
if (BUILTIN_PROVIDER_IDS.includes(providerId)) {
|
||||
if (resolvedProviders.has(providerId) || BUILTIN_PROVIDER_IDS.includes(providerId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -448,9 +457,14 @@ export function buildProviderRegistry(
|
||||
): Record<AgentProvider, ProviderDefinition> {
|
||||
const runtimeSettings = options?.runtimeSettings;
|
||||
const providerOverrides = options?.providerOverrides ?? {};
|
||||
const resolvedProviders = buildResolvedBuiltinProviders(providerOverrides, runtimeSettings, {
|
||||
workspaceGitService: options?.workspaceGitService,
|
||||
});
|
||||
const resolvedProviders = buildResolvedBuiltinProviders(
|
||||
providerOverrides,
|
||||
runtimeSettings,
|
||||
{
|
||||
workspaceGitService: options?.workspaceGitService,
|
||||
},
|
||||
options?.isDev === true,
|
||||
);
|
||||
addDerivedProviders(resolvedProviders, providerOverrides);
|
||||
|
||||
return Object.fromEntries(
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import { AgentManager } from "../agent-manager.js";
|
||||
import type { AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
|
||||
import {
|
||||
MOCK_LOAD_TEST_DEFAULT_MODEL_ID,
|
||||
MockLoadTestAgentClient,
|
||||
} from "./mock-load-test-agent.js";
|
||||
|
||||
describe("MockLoadTestAgentClient", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("default model is a five minute foreground stream", async () => {
|
||||
const client = new MockLoadTestAgentClient();
|
||||
|
||||
const models = await client.listModels();
|
||||
|
||||
expect(models[0]).toMatchObject({
|
||||
id: MOCK_LOAD_TEST_DEFAULT_MODEL_ID,
|
||||
isDefault: true,
|
||||
metadata: {
|
||||
durationMs: 300_000,
|
||||
intervalMs: 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("emits repeated markdown, reasoning, and tool calls during a foreground turn", async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = new MockLoadTestAgentClient();
|
||||
const session = await client.createSession({
|
||||
provider: "mock",
|
||||
cwd: process.cwd(),
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const events: AgentStreamEvent[] = [];
|
||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||
|
||||
const resultPromise = session.run("Exercise the app while terminals are busy.");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
const result = await resultPromise;
|
||||
unsubscribe();
|
||||
|
||||
expect(
|
||||
events.map((event) => event.type).filter((type) => type === "turn_started"),
|
||||
).toHaveLength(1);
|
||||
expect(events.at(-1)).toMatchObject({
|
||||
type: "turn_completed",
|
||||
provider: "mock",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
sessionId: session.id,
|
||||
finalText: "Synthetic load test complete",
|
||||
canceled: false,
|
||||
});
|
||||
|
||||
const timelineItems = events.flatMap((event): AgentTimelineItem[] =>
|
||||
event.type === "timeline" ? [event.item] : [],
|
||||
);
|
||||
expect(timelineItems.find((item) => item.type === "reasoning")).toMatchObject({
|
||||
type: "reasoning",
|
||||
text: expect.stringContaining("Thinking chunk 1"),
|
||||
});
|
||||
expect(
|
||||
timelineItems.filter(
|
||||
(item) =>
|
||||
item.type === "assistant_message" && item.text.startsWith("## Synthetic Load Cycle 1\n"),
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
timelineItems.filter((item) => item.type === "assistant_message" && item.text.length > 0)
|
||||
.length,
|
||||
).toBeGreaterThan(4);
|
||||
expect(
|
||||
timelineItems.filter(
|
||||
(item) =>
|
||||
item.type === "tool_call" &&
|
||||
item.name === "edit" &&
|
||||
item.status === "running" &&
|
||||
item.detail.type === "edit",
|
||||
),
|
||||
).toHaveLength(40);
|
||||
expect(
|
||||
timelineItems.filter(
|
||||
(item) =>
|
||||
item.type === "tool_call" &&
|
||||
item.name === "bash" &&
|
||||
item.status === "completed" &&
|
||||
item.detail.type === "shell" &&
|
||||
item.detail.output?.includes("mock load cycle"),
|
||||
),
|
||||
).toHaveLength(40);
|
||||
});
|
||||
|
||||
test("interrupt cancels the active foreground turn and stops future chunks", async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = new MockLoadTestAgentClient();
|
||||
const session = await client.createSession({
|
||||
provider: "mock",
|
||||
cwd: process.cwd(),
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const events: AgentStreamEvent[] = [];
|
||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||
|
||||
await session.startTurn("Cancel the synthetic stream.");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
await session.interrupt();
|
||||
const eventCountAfterInterrupt = events.length;
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
unsubscribe();
|
||||
|
||||
expect(events.at(-1)).toMatchObject({
|
||||
type: "turn_canceled",
|
||||
provider: "mock",
|
||||
reason: "Interrupted",
|
||||
});
|
||||
expect(events).toHaveLength(eventCountAfterInterrupt);
|
||||
});
|
||||
|
||||
test("agent manager coalesces adjacent markdown chunks from the mock provider", async () => {
|
||||
vi.useFakeTimers();
|
||||
const workdir = mkdtempSync(join(tmpdir(), "paseo-mock-load-test-"));
|
||||
try {
|
||||
const client = new MockLoadTestAgentClient();
|
||||
const manager = new AgentManager({
|
||||
clients: { mock: client },
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000001",
|
||||
logger: createTestLogger(),
|
||||
});
|
||||
const agent = await manager.createAgent(
|
||||
{
|
||||
provider: "mock",
|
||||
cwd: workdir,
|
||||
model: "ten-second-stream",
|
||||
},
|
||||
"00000000-0000-4000-8000-000000000001",
|
||||
);
|
||||
|
||||
const resultPromise = manager.runAgent(
|
||||
agent.id,
|
||||
"Stress the agent stream while terminal panes are active.",
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await resultPromise;
|
||||
|
||||
const timeline = manager.getTimeline(agent.id);
|
||||
const loadCycleDocuments = timeline.filter(
|
||||
(item): item is Extract<AgentTimelineItem, { type: "assistant_message" }> =>
|
||||
item.type === "assistant_message" && item.text.startsWith("## Synthetic Load Cycle "),
|
||||
);
|
||||
const editToolCalls = timeline.filter(
|
||||
(item) => item.type === "tool_call" && item.name === "edit",
|
||||
);
|
||||
|
||||
expect(loadCycleDocuments).toHaveLength(40);
|
||||
expect(loadCycleDocuments[0]?.text).toContain("This paragraph is intentionally stable");
|
||||
expect(editToolCalls).toHaveLength(40);
|
||||
expect(
|
||||
editToolCalls.every((item) => item.type === "tool_call" && item.status === "completed"),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,549 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { Logger } from "pino";
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentFeature,
|
||||
AgentLaunchContext,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPersistenceHandle,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPermissionResult,
|
||||
AgentPromptInput,
|
||||
AgentProvider,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
PersistedAgentDescriptor,
|
||||
ToolCallTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
import { getAgentProviderDefinition } from "../provider-manifest.js";
|
||||
|
||||
export const MOCK_LOAD_TEST_PROVIDER_ID = "mock";
|
||||
export const MOCK_LOAD_TEST_DEFAULT_MODEL_ID = "five-minute-stream";
|
||||
const MOCK_LOAD_TEST_MODE_ID = "load-test";
|
||||
const MOCK_LOAD_TEST_DURATION_MS = 5 * 60 * 1000;
|
||||
const MOCK_LOAD_TEST_INTERVAL_MS = 1000;
|
||||
|
||||
const CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsStreaming: true,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: true,
|
||||
supportsToolInvocations: true,
|
||||
};
|
||||
|
||||
const MODELS: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: MOCK_LOAD_TEST_PROVIDER_ID,
|
||||
id: MOCK_LOAD_TEST_DEFAULT_MODEL_ID,
|
||||
label: "Five minute stream",
|
||||
description: "Repeats synthetic markdown and tool-call timeline traffic for five minutes.",
|
||||
isDefault: true,
|
||||
metadata: {
|
||||
durationMs: MOCK_LOAD_TEST_DURATION_MS,
|
||||
intervalMs: MOCK_LOAD_TEST_INTERVAL_MS,
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: MOCK_LOAD_TEST_PROVIDER_ID,
|
||||
id: "one-minute-stream",
|
||||
label: "One minute stream",
|
||||
description: "Shorter synthetic load stream for quick manual checks.",
|
||||
metadata: {
|
||||
durationMs: 60_000,
|
||||
intervalMs: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: MOCK_LOAD_TEST_PROVIDER_ID,
|
||||
id: "ten-second-stream",
|
||||
label: "Ten second stream",
|
||||
description: "Fast synthetic load stream for tests and smoke checks.",
|
||||
metadata: {
|
||||
durationMs: 10_000,
|
||||
intervalMs: 250,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type ActiveTurn = {
|
||||
turnId: string;
|
||||
prompt: AgentPromptInput;
|
||||
startedAt: number;
|
||||
iteration: number;
|
||||
durationMs: number;
|
||||
intervalMs: number;
|
||||
timer: ReturnType<typeof setTimeout> | null;
|
||||
resolve: (result: AgentRunResult) => void;
|
||||
completed: Promise<AgentRunResult>;
|
||||
};
|
||||
|
||||
function resolveModelProfile(modelId: string | null | undefined): {
|
||||
modelId: string;
|
||||
durationMs: number;
|
||||
intervalMs: number;
|
||||
} {
|
||||
const model = MODELS.find((entry) => entry.id === modelId) ?? MODELS[0]!;
|
||||
const metadata = model.metadata ?? {};
|
||||
return {
|
||||
modelId: model.id,
|
||||
durationMs:
|
||||
typeof metadata.durationMs === "number" ? metadata.durationMs : MOCK_LOAD_TEST_DURATION_MS,
|
||||
intervalMs:
|
||||
typeof metadata.intervalMs === "number" ? metadata.intervalMs : MOCK_LOAD_TEST_INTERVAL_MS,
|
||||
};
|
||||
}
|
||||
|
||||
function promptToText(prompt: AgentPromptInput): string {
|
||||
if (typeof prompt === "string") {
|
||||
return prompt;
|
||||
}
|
||||
return prompt
|
||||
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function buildMarkdownDocument(iteration: number, prompt: AgentPromptInput): string {
|
||||
const promptPreview = promptToText(prompt).slice(0, 160) || "No prompt text supplied.";
|
||||
return [
|
||||
`## Synthetic Load Cycle ${iteration}`,
|
||||
"",
|
||||
`Prompt preview: ${promptPreview}`,
|
||||
"",
|
||||
"| Signal | Value |",
|
||||
"| --- | --- |",
|
||||
`| cycle | ${iteration} |`,
|
||||
"| stream | markdown + reasoning + tools |",
|
||||
"",
|
||||
"```ts",
|
||||
`const cycle = ${iteration};`,
|
||||
'const purpose = "exercise app parsing and terminal rendering under load";',
|
||||
"```",
|
||||
"",
|
||||
"This paragraph is intentionally stable so repeated cycles produce predictable UI pressure.",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function splitText(text: string, chunks: number): string[] {
|
||||
const size = Math.ceil(text.length / chunks);
|
||||
const parts: string[] = [];
|
||||
for (let index = 0; index < text.length; index += size) {
|
||||
parts.push(text.slice(index, index + size));
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function createToolCall(input: {
|
||||
turnId: string;
|
||||
iteration: number;
|
||||
name: string;
|
||||
status: ToolCallTimelineItem["status"];
|
||||
detail: ToolCallTimelineItem["detail"];
|
||||
}): ToolCallTimelineItem {
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: `${input.turnId}:${input.name}:${input.iteration}`,
|
||||
name: input.name,
|
||||
status: input.status,
|
||||
error: null,
|
||||
detail: input.detail,
|
||||
};
|
||||
}
|
||||
|
||||
export class MockLoadTestAgentClient implements AgentClient {
|
||||
readonly provider: AgentProvider = MOCK_LOAD_TEST_PROVIDER_ID;
|
||||
readonly capabilities = CAPABILITIES;
|
||||
|
||||
constructor(private readonly logger?: Logger) {}
|
||||
|
||||
async createSession(
|
||||
config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
return new MockLoadTestAgentSession({
|
||||
config,
|
||||
sessionId: randomUUID(),
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
handle: AgentPersistenceHandle,
|
||||
overrides?: Partial<AgentSessionConfig>,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
||||
return new MockLoadTestAgentSession({
|
||||
config: {
|
||||
cwd: String(metadata.cwd ?? overrides?.cwd ?? process.cwd()),
|
||||
...metadata,
|
||||
...overrides,
|
||||
provider: MOCK_LOAD_TEST_PROVIDER_ID,
|
||||
},
|
||||
sessionId: handle.sessionId,
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
return MODELS;
|
||||
}
|
||||
|
||||
async listModes(): Promise<AgentMode[]> {
|
||||
return getAgentProviderDefinition(MOCK_LOAD_TEST_PROVIDER_ID).modes;
|
||||
}
|
||||
|
||||
async listPersistedAgents(
|
||||
_options?: ListPersistedAgentsOptions,
|
||||
): Promise<PersistedAgentDescriptor[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||
return {
|
||||
diagnostic: "Mock load-test provider is available in development builds.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MockLoadTestAgentSession implements AgentSession {
|
||||
readonly provider: AgentProvider = MOCK_LOAD_TEST_PROVIDER_ID;
|
||||
readonly capabilities = CAPABILITIES;
|
||||
readonly features: AgentFeature[] = [];
|
||||
readonly id: string;
|
||||
private readonly listeners = new Set<(event: AgentStreamEvent) => void>();
|
||||
private readonly history: AgentStreamEvent[] = [];
|
||||
private readonly logger?: Logger;
|
||||
private activeTurn: ActiveTurn | null = null;
|
||||
private modeId: string | null;
|
||||
private modelId: string | null;
|
||||
|
||||
constructor(options: {
|
||||
config: AgentSessionConfig;
|
||||
sessionId: string;
|
||||
logger?: Logger;
|
||||
}) {
|
||||
this.id = options.sessionId;
|
||||
this.logger = options.logger;
|
||||
this.modeId = options.config.modeId ?? MOCK_LOAD_TEST_MODE_ID;
|
||||
this.modelId = options.config.model ?? MOCK_LOAD_TEST_DEFAULT_MODEL_ID;
|
||||
}
|
||||
|
||||
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
||||
const { turnId } = await this.startTurn(prompt, options);
|
||||
const turn = this.activeTurn;
|
||||
if (!turn || turn.turnId !== turnId) {
|
||||
throw new Error("Mock load-test turn did not start");
|
||||
}
|
||||
return turn.completed;
|
||||
}
|
||||
|
||||
async startTurn(
|
||||
prompt: AgentPromptInput,
|
||||
_options?: AgentRunOptions,
|
||||
): Promise<{ turnId: string }> {
|
||||
if (this.activeTurn) {
|
||||
throw new Error("Mock load-test provider already has an active turn");
|
||||
}
|
||||
|
||||
const profile = resolveModelProfile(this.modelId);
|
||||
const turnId = randomUUID();
|
||||
let resolve!: (result: AgentRunResult) => void;
|
||||
const completed = new Promise<AgentRunResult>((promiseResolve) => {
|
||||
resolve = promiseResolve;
|
||||
});
|
||||
const turn: ActiveTurn = {
|
||||
turnId,
|
||||
prompt,
|
||||
startedAt: Date.now(),
|
||||
iteration: 0,
|
||||
durationMs: profile.durationMs,
|
||||
intervalMs: profile.intervalMs,
|
||||
timer: null,
|
||||
resolve,
|
||||
completed,
|
||||
};
|
||||
this.activeTurn = turn;
|
||||
|
||||
this.schedule(turn, 0);
|
||||
return { turnId };
|
||||
}
|
||||
|
||||
subscribe(callback: (event: AgentStreamEvent) => void): () => void {
|
||||
this.listeners.add(callback);
|
||||
return () => {
|
||||
this.listeners.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
|
||||
for (const event of this.history) {
|
||||
yield event;
|
||||
}
|
||||
}
|
||||
|
||||
async getRuntimeInfo(): Promise<AgentRuntimeInfo> {
|
||||
return {
|
||||
provider: this.provider,
|
||||
sessionId: this.id,
|
||||
model: this.modelId,
|
||||
modeId: this.modeId,
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableModes(): Promise<AgentMode[]> {
|
||||
return getAgentProviderDefinition(MOCK_LOAD_TEST_PROVIDER_ID).modes;
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<string | null> {
|
||||
return this.modeId;
|
||||
}
|
||||
|
||||
async setMode(modeId: string): Promise<void> {
|
||||
this.modeId = modeId;
|
||||
}
|
||||
|
||||
getPendingPermissions(): AgentPermissionRequest[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async respondToPermission(
|
||||
_requestId: string,
|
||||
_response: AgentPermissionResponse,
|
||||
): Promise<AgentPermissionResult | void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describePersistence(): AgentPersistenceHandle | null {
|
||||
return {
|
||||
provider: this.provider,
|
||||
sessionId: this.id,
|
||||
metadata: {
|
||||
model: this.modelId,
|
||||
modeId: this.modeId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async interrupt(): Promise<void> {
|
||||
const turn = this.activeTurn;
|
||||
if (!turn) {
|
||||
return;
|
||||
}
|
||||
this.clearTurnTimer(turn);
|
||||
this.activeTurn = null;
|
||||
const event: AgentStreamEvent = {
|
||||
type: "turn_canceled",
|
||||
provider: this.provider,
|
||||
reason: "Interrupted",
|
||||
turnId: turn.turnId,
|
||||
};
|
||||
this.emit(event);
|
||||
turn.resolve({
|
||||
sessionId: this.id,
|
||||
finalText: "",
|
||||
timeline: [],
|
||||
canceled: true,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.interrupt();
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
async setModel(modelId: string | null): Promise<void> {
|
||||
this.modelId = modelId ?? MOCK_LOAD_TEST_DEFAULT_MODEL_ID;
|
||||
}
|
||||
|
||||
private schedule(turn: ActiveTurn, delayMs: number): void {
|
||||
turn.timer = setTimeout(() => {
|
||||
this.tick(turn);
|
||||
}, delayMs);
|
||||
turn.timer.unref?.();
|
||||
}
|
||||
|
||||
private tick(turn: ActiveTurn): void {
|
||||
if (this.activeTurn !== turn) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearTurnTimer(turn);
|
||||
if (turn.iteration === 0) {
|
||||
this.emit({
|
||||
type: "turn_started",
|
||||
provider: this.provider,
|
||||
turnId: turn.turnId,
|
||||
});
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - turn.startedAt;
|
||||
if (elapsedMs >= turn.durationMs) {
|
||||
this.finishTurn(turn);
|
||||
return;
|
||||
}
|
||||
|
||||
turn.iteration += 1;
|
||||
this.emitIteration(turn);
|
||||
this.schedule(turn, turn.intervalMs);
|
||||
}
|
||||
|
||||
private emitIteration(turn: ActiveTurn): void {
|
||||
const { turnId, iteration, prompt } = turn;
|
||||
this.emitTimeline(turnId, {
|
||||
type: "reasoning",
|
||||
text: `Thinking chunk ${iteration}: simulate planning pressure before rendering markdown.\n`,
|
||||
});
|
||||
|
||||
for (const chunk of splitText(buildMarkdownDocument(iteration, prompt), 4)) {
|
||||
this.emitTimeline(turnId, {
|
||||
type: "assistant_message",
|
||||
text: chunk,
|
||||
});
|
||||
}
|
||||
|
||||
const editDetail = {
|
||||
type: "edit" as const,
|
||||
filePath: `src/load-test-${iteration}.ts`,
|
||||
oldString: "before",
|
||||
newString: "after",
|
||||
unifiedDiff: [
|
||||
`diff --git a/src/load-test-${iteration}.ts b/src/load-test-${iteration}.ts`,
|
||||
"@@",
|
||||
"-before",
|
||||
"+after",
|
||||
].join("\n"),
|
||||
};
|
||||
this.emitTimeline(
|
||||
turnId,
|
||||
createToolCall({
|
||||
turnId,
|
||||
iteration,
|
||||
name: "edit",
|
||||
status: "running",
|
||||
detail: editDetail,
|
||||
}),
|
||||
);
|
||||
this.emitTimeline(
|
||||
turnId,
|
||||
createToolCall({
|
||||
turnId,
|
||||
iteration,
|
||||
name: "edit",
|
||||
status: "completed",
|
||||
detail: editDetail,
|
||||
}),
|
||||
);
|
||||
|
||||
const shellDetail = {
|
||||
type: "shell" as const,
|
||||
command: `printf 'mock load cycle ${iteration}\\n'`,
|
||||
cwd: "/tmp/paseo-mock-load",
|
||||
output: `mock load cycle ${iteration}\n`,
|
||||
exitCode: 0,
|
||||
};
|
||||
this.emitTimeline(
|
||||
turnId,
|
||||
createToolCall({
|
||||
turnId,
|
||||
iteration,
|
||||
name: "bash",
|
||||
status: "running",
|
||||
detail: shellDetail,
|
||||
}),
|
||||
);
|
||||
this.emitTimeline(
|
||||
turnId,
|
||||
createToolCall({
|
||||
turnId,
|
||||
iteration,
|
||||
name: "bash",
|
||||
status: "completed",
|
||||
detail: shellDetail,
|
||||
}),
|
||||
);
|
||||
|
||||
this.emit({
|
||||
type: "usage_updated",
|
||||
provider: this.provider,
|
||||
turnId,
|
||||
usage: {
|
||||
inputTokens: iteration * 32,
|
||||
outputTokens: iteration * 128,
|
||||
contextWindowUsedTokens: iteration * 160,
|
||||
contextWindowMaxTokens: 128_000,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private finishTurn(turn: ActiveTurn): void {
|
||||
this.activeTurn = null;
|
||||
this.emitTimeline(turn.turnId, {
|
||||
type: "assistant_message",
|
||||
text: "## Synthetic load test complete\n\nThe mock provider finished its foreground turn.\n",
|
||||
});
|
||||
const usage = {
|
||||
inputTokens: turn.iteration * 32,
|
||||
outputTokens: turn.iteration * 128,
|
||||
contextWindowUsedTokens: turn.iteration * 160,
|
||||
contextWindowMaxTokens: 128_000,
|
||||
};
|
||||
this.emit({
|
||||
type: "turn_completed",
|
||||
provider: this.provider,
|
||||
turnId: turn.turnId,
|
||||
usage,
|
||||
});
|
||||
turn.resolve({
|
||||
sessionId: this.id,
|
||||
finalText: "Synthetic load test complete",
|
||||
usage,
|
||||
timeline: [],
|
||||
canceled: false,
|
||||
});
|
||||
}
|
||||
|
||||
private emitTimeline(turnId: string, item: AgentTimelineItem): void {
|
||||
this.emit({
|
||||
type: "timeline",
|
||||
provider: this.provider,
|
||||
turnId,
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
private emit(event: AgentStreamEvent): void {
|
||||
this.history.push(event);
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
this.logger?.warn({ err: error }, "Mock load-test listener failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private clearTurnTimer(turn: ActiveTurn): void {
|
||||
if (!turn.timer) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(turn.timer);
|
||||
turn.timer = null;
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,7 @@ export type PaseoDaemonConfig = {
|
||||
mcpInjectIntoAgents?: boolean;
|
||||
staticDir: string;
|
||||
mcpDebug: boolean;
|
||||
isDev?: boolean;
|
||||
agentClients: Partial<Record<AgentProvider, AgentClient>>;
|
||||
agentStoragePath: string;
|
||||
relayEnabled?: boolean;
|
||||
@@ -425,6 +426,7 @@ export async function createPaseoDaemon(
|
||||
runtimeSettings: config.agentProviderSettings,
|
||||
providerOverrides: config.providerOverrides,
|
||||
workspaceGitService,
|
||||
isDev: config.isDev === true,
|
||||
}),
|
||||
...config.agentClients,
|
||||
},
|
||||
@@ -435,6 +437,7 @@ export async function createPaseoDaemon(
|
||||
runtimeSettings: config.agentProviderSettings,
|
||||
providerOverrides: config.providerOverrides,
|
||||
workspaceGitService,
|
||||
isDev: config.isDev === true,
|
||||
});
|
||||
|
||||
const detachAgentStoragePersistence = attachAgentStoragePersistence(
|
||||
@@ -707,6 +710,7 @@ export async function createPaseoDaemon(
|
||||
},
|
||||
config.agentProviderSettings,
|
||||
config.providerOverrides,
|
||||
config.isDev === true,
|
||||
daemonVersion,
|
||||
(intent) => {
|
||||
try {
|
||||
|
||||
@@ -181,6 +181,7 @@ export function loadConfig(
|
||||
mcpEnabled,
|
||||
mcpInjectIntoAgents,
|
||||
mcpDebug: env.MCP_DEBUG === "1",
|
||||
isDev: env.NODE_ENV === "development",
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
staticDir: "public",
|
||||
agentClients: {},
|
||||
|
||||
@@ -526,6 +526,7 @@ export type SessionOptions = {
|
||||
};
|
||||
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
|
||||
providerOverrides?: Record<string, ProviderOverride>;
|
||||
isDev?: boolean;
|
||||
};
|
||||
|
||||
export type SessionLifecycleIntent =
|
||||
@@ -720,6 +721,7 @@ export class Session {
|
||||
private readonly getSpeechReadiness?: () => SpeechReadinessSnapshot;
|
||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
|
||||
private readonly isDev: boolean;
|
||||
private voiceModeAgentId: string | null = null;
|
||||
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
||||
|
||||
@@ -761,6 +763,7 @@ export class Session {
|
||||
dictation,
|
||||
agentProviderRuntimeSettings,
|
||||
providerOverrides,
|
||||
isDev,
|
||||
} = options;
|
||||
this.clientId = clientId;
|
||||
this.appVersion = appVersion ?? null;
|
||||
@@ -829,11 +832,13 @@ export class Session {
|
||||
this.getSpeechReadiness = dictation?.getSpeechReadiness;
|
||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||
this.providerOverrides = providerOverrides;
|
||||
this.isDev = isDev === true;
|
||||
this.abortController = new AbortController();
|
||||
this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
|
||||
runtimeSettings: this.agentProviderRuntimeSettings,
|
||||
providerOverrides: this.providerOverrides,
|
||||
workspaceGitService: this.workspaceGitService,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
|
||||
// Initialize per-session managers
|
||||
|
||||
@@ -86,6 +86,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
"1.2.3-test",
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -181,6 +181,7 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
TEST_DAEMON_VERSION,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -94,6 +94,7 @@ function createServer(logger: ReturnType<typeof createLogger>) {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
"1.2.3-test",
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -319,6 +319,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
|
||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
|
||||
private readonly isDev: boolean;
|
||||
private readonly providerSnapshotManager: ProviderSnapshotManager;
|
||||
private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null;
|
||||
private readonly onBranchChanged:
|
||||
@@ -374,6 +375,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
},
|
||||
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap,
|
||||
providerOverrides?: Record<string, ProviderOverride>,
|
||||
isDev?: boolean,
|
||||
daemonVersion?: string,
|
||||
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void,
|
||||
projectRegistry?: ProjectRegistry,
|
||||
@@ -432,11 +434,13 @@ export class VoiceAssistantWebSocketServer {
|
||||
this.dictation = dictation ?? null;
|
||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||
this.providerOverrides = providerOverrides;
|
||||
this.isDev = isDev === true;
|
||||
const providerSnapshotLogger = this.logger.child({ module: "provider-snapshot-manager" });
|
||||
this.providerSnapshotManager = new ProviderSnapshotManager(
|
||||
buildProviderRegistry(providerSnapshotLogger, {
|
||||
runtimeSettings: this.agentProviderRuntimeSettings,
|
||||
providerOverrides: this.providerOverrides,
|
||||
isDev: this.isDev,
|
||||
}),
|
||||
providerSnapshotLogger,
|
||||
);
|
||||
@@ -793,6 +797,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
: undefined,
|
||||
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
||||
providerOverrides: this.providerOverrides,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
|
||||
connection = {
|
||||
|
||||
Reference in New Issue
Block a user