mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 03:32:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02537727a | ||
|
|
870c8dc9c5 | ||
|
|
d3c1cc54bb |
+60
@@ -12,6 +12,7 @@ interface QueryMock {
|
|||||||
close: ReturnType<typeof vi.fn>;
|
close: ReturnType<typeof vi.fn>;
|
||||||
setPermissionMode: ReturnType<typeof vi.fn>;
|
setPermissionMode: ReturnType<typeof vi.fn>;
|
||||||
setModel: ReturnType<typeof vi.fn>;
|
setModel: ReturnType<typeof vi.fn>;
|
||||||
|
applyFlagSettings: ReturnType<typeof vi.fn>;
|
||||||
supportedModels: ReturnType<typeof vi.fn>;
|
supportedModels: ReturnType<typeof vi.fn>;
|
||||||
supportedCommands: ReturnType<typeof vi.fn>;
|
supportedCommands: ReturnType<typeof vi.fn>;
|
||||||
rewindFiles: ReturnType<typeof vi.fn>;
|
rewindFiles: ReturnType<typeof vi.fn>;
|
||||||
@@ -148,6 +149,7 @@ function createScriptedQuery(params: {
|
|||||||
close: vi.fn(() => undefined),
|
close: vi.fn(() => undefined),
|
||||||
setPermissionMode: vi.fn(async () => undefined),
|
setPermissionMode: vi.fn(async () => undefined),
|
||||||
setModel: vi.fn(async () => undefined),
|
setModel: vi.fn(async () => undefined),
|
||||||
|
applyFlagSettings: vi.fn(async () => undefined),
|
||||||
supportedModels: vi.fn(async () => [{ value: "opus", displayName: "Opus" }]),
|
supportedModels: vi.fn(async () => [{ value: "opus", displayName: "Opus" }]),
|
||||||
supportedCommands: vi.fn(async () => []),
|
supportedCommands: vi.fn(async () => []),
|
||||||
rewindFiles: vi.fn(async () => ({ canRewind: true })),
|
rewindFiles: vi.fn(async () => ({ canRewind: true })),
|
||||||
@@ -276,6 +278,64 @@ afterEach(() => {
|
|||||||
queryFactory.mockReset();
|
queryFactory.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function createThinkingRestartSession() {
|
||||||
|
const queries: ScriptedQuery[] = [];
|
||||||
|
queryFactory.mockImplementation(({ prompt }: { prompt: AsyncIterable<unknown> }) => {
|
||||||
|
const scriptedQuery = createScriptedQuery({
|
||||||
|
prompt,
|
||||||
|
sessionId: "thinking-restart-session",
|
||||||
|
handlePrompt: ({ query }) => query.emit(buildSuccessResult("thinking-restart-session")),
|
||||||
|
});
|
||||||
|
queries.push(scriptedQuery);
|
||||||
|
return scriptedQuery;
|
||||||
|
});
|
||||||
|
const session = await new ClaudeAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
queryFactory,
|
||||||
|
resolveBinary: async () => "/test/claude/bin",
|
||||||
|
}).createSession({ provider: "claude", cwd: process.cwd() });
|
||||||
|
return { session, queries };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("submits after a thinking change before the first turn", async () => {
|
||||||
|
const { session, queries } = await createThinkingRestartSession();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.setThinkingOption!("medium");
|
||||||
|
const events = await collectUntilTerminal(
|
||||||
|
streamSession(session, "continue", { clientMessageId: "client-continue" }),
|
||||||
|
);
|
||||||
|
expect(events.filter((event) => event.type === "turn_failed")).toEqual([]);
|
||||||
|
expect(events.at(-1)?.type).toBe("turn_completed");
|
||||||
|
expect(queryFactory).toHaveBeenCalledTimes(1);
|
||||||
|
expect(queries[0].prompts.map((prompt) => prompt.text)).toEqual(["continue"]);
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("submits after a thinking change once the previous query ended", async () => {
|
||||||
|
const { session, queries } = await createThinkingRestartSession();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await collectUntilTerminal(streamSession(session, "first prompt"));
|
||||||
|
queries[0].end();
|
||||||
|
// Let the pump retire the old query before changing thinking.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
await session.setThinkingOption!("medium");
|
||||||
|
const events = await collectUntilTerminal(
|
||||||
|
streamSession(session, "continue", { clientMessageId: "client-continue" }),
|
||||||
|
);
|
||||||
|
expect(events.filter((event) => event.type === "turn_failed")).toEqual([]);
|
||||||
|
expect(events.at(-1)?.type).toBe("turn_completed");
|
||||||
|
expect(queryFactory).toHaveBeenCalledTimes(2);
|
||||||
|
expect(queries[1].prompts.map((prompt) => prompt.text)).toEqual(["continue"]);
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("interrupt only calls query.interrupt and leaves the query open", async () => {
|
test("interrupt only calls query.interrupt and leaves the query open", async () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
const queries: ScriptedQuery[] = [];
|
const queries: ScriptedQuery[] = [];
|
||||||
|
|||||||
@@ -3275,6 +3275,9 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A fresh query already uses the current settings, even when there was no old query to retire.
|
||||||
|
this.queryRestartNeeded = false;
|
||||||
|
|
||||||
// Preserve claudeSessionId across query recreation so buildOptions() passes
|
// Preserve claudeSessionId across query recreation so buildOptions() passes
|
||||||
// resume: sessionId and the new query continues the existing conversation.
|
// resume: sessionId and the new query continues the existing conversation.
|
||||||
this.persistence = null;
|
this.persistence = null;
|
||||||
@@ -3877,11 +3880,12 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startQueryPump(): void {
|
private startQueryPump(): void {
|
||||||
if (this.closed || this.queryPumpPromise) {
|
if (this.closed || this.queryPumpPromise || !this.query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pump = this.runQueryPump().catch((error) => {
|
// Drain the initialized query; a pending settings change belongs to the next turn.
|
||||||
|
const pump = this.runQueryPump(this.query).catch((error) => {
|
||||||
this.logger.trace(
|
this.logger.trace(
|
||||||
{
|
{
|
||||||
agentId: this.agentId,
|
agentId: this.agentId,
|
||||||
@@ -3902,25 +3906,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runQueryPump(): Promise<void> {
|
private async runQueryPump(activeQuery: Query): Promise<void> {
|
||||||
let activeQuery: Query;
|
|
||||||
try {
|
|
||||||
activeQuery = await this.ensureQuery();
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.trace(
|
|
||||||
{
|
|
||||||
agentId: this.agentId,
|
|
||||||
provider: "claude",
|
|
||||||
sessionId: this.claudeSessionId,
|
|
||||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
|
||||||
err: error,
|
|
||||||
},
|
|
||||||
"provider.claude.query_pump.init_failed",
|
|
||||||
);
|
|
||||||
this.failActiveTurns(error instanceof Error ? error.message : "Claude stream failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let consecutiveInterruptAbortRecoveries = 0;
|
let consecutiveInterruptAbortRecoveries = 0;
|
||||||
const logRawMessage = (message: SDKMessage): void => {
|
const logRawMessage = (message: SDKMessage): void => {
|
||||||
this.logger.trace(
|
this.logger.trace(
|
||||||
|
|||||||
@@ -115,4 +115,43 @@ describe("daemon E2E (real claude) - thinking effort memory", () => {
|
|||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}, 420_000);
|
}, 420_000);
|
||||||
|
|
||||||
|
test("a thinking change before the first turn does not fail that turn", async () => {
|
||||||
|
const logger = pino({ level: "silent" });
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const daemon = await createTestPaseoDaemon({
|
||||||
|
agentClients: createRealProviderClients(["claude"], logger),
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.fetchAgents({ subscribe: {} });
|
||||||
|
|
||||||
|
const modelList = await client.listProviderModels("claude", { cwd });
|
||||||
|
const model = modelList.models.find(modelHasLowThinkingOption);
|
||||||
|
if (!model) {
|
||||||
|
throw new Error("No Claude Sonnet model with low thinking effort returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await client.createAgent({
|
||||||
|
cwd,
|
||||||
|
title: "claude-thinking-before-first-turn-real",
|
||||||
|
...getRealProviderConfig("claude"),
|
||||||
|
model: model.id,
|
||||||
|
});
|
||||||
|
await client.setAgentThinkingOption(agent.id, "low");
|
||||||
|
|
||||||
|
await client.sendMessage(agent.id, "Reply exactly: ACK_FIRST");
|
||||||
|
const finish = await client.waitForFinish(agent.id, 180_000);
|
||||||
|
expect(finish.final?.lastError).toBeUndefined();
|
||||||
|
expect(finish.status).toBe("idle");
|
||||||
|
expect(compactText(await getAssistantText(client, agent.id))).toContain("ack_first");
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
await daemon.close().catch(() => undefined);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 300_000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user