Merge branch 'main' of github.com:getpaseo/paseo
This commit is contained in:
@@ -362,6 +362,33 @@ describe("createLoggedNdJsonStream", () => {
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test("normalizes stringified numeric ACP response ids", async () => {
|
||||
const input = new TransformStream<Uint8Array, Uint8Array>();
|
||||
const output = new TransformStream<Uint8Array, Uint8Array>();
|
||||
const logger = {
|
||||
warn: vi.fn(),
|
||||
};
|
||||
|
||||
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
|
||||
logger: asInternals<ReturnType<typeof createTestLogger>>(logger),
|
||||
provider: "deepseek-tui",
|
||||
});
|
||||
const reader = stream.readable.getReader();
|
||||
const writer = input.writable.getWriter();
|
||||
|
||||
await writer.write(
|
||||
new TextEncoder().encode('{"jsonrpc":"2.0","id":"0","result":{"ok":true}}\n'),
|
||||
);
|
||||
|
||||
const parsed = await reader.read();
|
||||
|
||||
expect(parsed.value).toEqual({ jsonrpc: "2.0", id: 0, result: { ok: true } });
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
|
||||
await writer.close();
|
||||
reader.releaseLock();
|
||||
});
|
||||
|
||||
test("does not log terminal control sequences from malformed ACP stdout", async () => {
|
||||
const input = new TransformStream<Uint8Array, Uint8Array>();
|
||||
const output = new TransformStream<Uint8Array, Uint8Array>();
|
||||
@@ -704,6 +731,31 @@ describe("ACPAgentSession Zed parity", () => {
|
||||
expect(setSessionConfigOption).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not fail session start when configured model cannot be applied by ACP", async () => {
|
||||
const logger = createTestLogger();
|
||||
const childLogger = { trace: vi.fn(), warn: vi.fn() };
|
||||
vi.spyOn(logger, "child").mockReturnValue(asInternals<typeof logger>(childLogger));
|
||||
const session = createSessionWithConfig(
|
||||
{ provider: "deepseek-tui", model: "deepseek/v4" },
|
||||
logger,
|
||||
);
|
||||
const { internals, setSessionConfigOption, unstableSetSessionModel } =
|
||||
prepareConfiguredOverrideSession(session, {
|
||||
currentModel: null,
|
||||
availableModels: null,
|
||||
configOptions: [],
|
||||
connection: { unstable_setSessionModel: undefined },
|
||||
});
|
||||
|
||||
await expect(internals.applyConfiguredOverrides()).resolves.toBeUndefined();
|
||||
expect(unstableSetSessionModel).not.toHaveBeenCalled();
|
||||
expect(setSessionConfigOption).not.toHaveBeenCalled();
|
||||
expect(childLogger.warn).toHaveBeenCalledWith(
|
||||
{ value: "deepseek/v4" },
|
||||
"deepseek-tui does not expose ACP model selection; using provider default model",
|
||||
);
|
||||
});
|
||||
|
||||
test("routes config_option_update and refreshes derived mode, model, and thinking state", async () => {
|
||||
const session = createSession();
|
||||
const internals = asInternals<ACPSessionInternals>(session);
|
||||
|
||||
@@ -180,6 +180,26 @@ function summarizeMalformedACPStdoutError(error: unknown): { type: string; messa
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeACPIncomingMessage(message: AnyMessage): AnyMessage {
|
||||
if (
|
||||
"id" in message &&
|
||||
!("method" in message) &&
|
||||
typeof message.id === "string" &&
|
||||
/^\d+$/.test(message.id)
|
||||
) {
|
||||
const numericId = Number(message.id);
|
||||
if (Number.isSafeInteger(numericId)) {
|
||||
return {
|
||||
...message,
|
||||
// COMPAT(deepseek-tui-acp-id): added v0.1.78, remove after 2026-11-19
|
||||
// once the ACP SDK accepts stringified numeric response IDs.
|
||||
id: numericId,
|
||||
} as AnyMessage;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export function createLoggedNdJsonStream(
|
||||
output: NodeWritableStream,
|
||||
input: NodeReadableStream,
|
||||
@@ -214,7 +234,7 @@ export function createLoggedNdJsonStream(
|
||||
|
||||
try {
|
||||
const message: AnyMessage = JSON.parse(trimmedLine);
|
||||
controller.enqueue(message);
|
||||
controller.enqueue(normalizeACPIncomingMessage(message));
|
||||
} catch (error) {
|
||||
options.logger.warn(
|
||||
{
|
||||
@@ -1356,7 +1376,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
}
|
||||
|
||||
if (typeof this.connection.unstable_setSessionModel !== "function") {
|
||||
throw new Error(`${this.provider} does not expose ACP model selection`);
|
||||
throw new Error(this.modelSelectionUnavailableMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1378,7 +1398,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
|
||||
const modelOption = selection.configOption;
|
||||
if (!modelOption) {
|
||||
throw new Error(`${this.provider} does not expose ACP model selection`);
|
||||
throw new Error(this.modelSelectionUnavailableMessage());
|
||||
}
|
||||
if (!selection.configChoice) {
|
||||
this.warnInvalidSelection(
|
||||
@@ -1884,7 +1904,17 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
availableModels: this.availableModels,
|
||||
configOptions: this.configOptions,
|
||||
});
|
||||
await this.setModelWithSelection({ modelId: configuredModelId, selection });
|
||||
try {
|
||||
await this.setModelWithSelection({ modelId: configuredModelId, selection });
|
||||
} catch (error) {
|
||||
if (!this.isModelSelectionUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.warn(
|
||||
{ value: configuredModelId },
|
||||
`${this.provider} does not expose ACP model selection; using provider default model`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.config.thinkingOptionId && this.config.thinkingOptionId !== this.thinkingOptionId) {
|
||||
await this.setThinkingOption(this.config.thinkingOptionId);
|
||||
@@ -1895,6 +1925,14 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
this.logger.warn({ value }, message);
|
||||
}
|
||||
|
||||
private modelSelectionUnavailableMessage(): string {
|
||||
return `${this.provider} does not expose ACP model selection`;
|
||||
}
|
||||
|
||||
private isModelSelectionUnavailableError(error: unknown): boolean {
|
||||
return error instanceof Error && error.message === this.modelSelectionUnavailableMessage();
|
||||
}
|
||||
|
||||
private translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[] {
|
||||
switch (update.sessionUpdate) {
|
||||
case "user_message_chunk": {
|
||||
|
||||
Reference in New Issue
Block a user