mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Keep structured turn runtime resolution in-process
This commit is contained in:
parent
3f9a54af38
commit
7e5136423a
10 changed files with 76 additions and 76 deletions
|
|
@ -1,23 +0,0 @@
|
|||
import { AppServerClient } from "./client";
|
||||
import { panelModelOptionsFromAppServerModels } from "./catalog-model";
|
||||
import type { PanelModelOption } from "../domain/catalog/metadata";
|
||||
|
||||
export async function loadPanelModelOptions(codexPath: string, cwd: string, includeHidden = false): Promise<PanelModelOption[]> {
|
||||
let client!: AppServerClient;
|
||||
client = new AppServerClient(codexPath, cwd, {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: (request) => {
|
||||
client.rejectServerRequest(request.id, -32601, "Model option loading does not handle server requests.");
|
||||
},
|
||||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const response = await client.listModels(includeHidden);
|
||||
return panelModelOptionsFromAppServerModels(response.data);
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import type { RequestId } from "../generated/app-server/RequestId";
|
|||
import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
|
||||
import type { ServerNotification } from "../generated/app-server/ServerNotification";
|
||||
import type { JsonValue } from "../generated/app-server/serde_json/JsonValue";
|
||||
import type { ModelListResponse } from "../generated/app-server/v2/ModelListResponse";
|
||||
import type { ThreadItem } from "../generated/app-server/v2/ThreadItem";
|
||||
import type { ThreadStartResponse } from "../generated/app-server/v2/ThreadStartResponse";
|
||||
import type { Turn } from "../generated/app-server/v2/Turn";
|
||||
|
|
@ -38,11 +39,17 @@ export interface StructuredEphemeralTurnClient {
|
|||
): Promise<TurnStartResponse>;
|
||||
}
|
||||
|
||||
export interface StructuredEphemeralTurnRuntimeClient {
|
||||
listModels(includeHidden?: boolean): Promise<ModelListResponse>;
|
||||
}
|
||||
|
||||
type StructuredEphemeralTurnRuntimeCapableClient = StructuredEphemeralTurnClient & StructuredEphemeralTurnRuntimeClient;
|
||||
|
||||
export type StructuredEphemeralTurnClientFactory = (
|
||||
codexPath: string,
|
||||
cwd: string,
|
||||
handlers: AppServerClientHandlers,
|
||||
) => StructuredEphemeralTurnClient;
|
||||
) => StructuredEphemeralTurnRuntimeCapableClient;
|
||||
|
||||
export interface RunStructuredEphemeralTurnOptions {
|
||||
codexPath: string;
|
||||
|
|
@ -57,6 +64,7 @@ export interface RunStructuredEphemeralTurnOptions {
|
|||
timedOutMessage: string;
|
||||
abortMessage?: string;
|
||||
runtime?: StructuredTurnRuntimeOverride | undefined;
|
||||
resolveRuntime?: ((client: StructuredEphemeralTurnRuntimeClient) => Promise<StructuredTurnRuntimeOverride>) | undefined;
|
||||
signal?: AbortSignal | undefined;
|
||||
onProgress?: (event: StructuredTurnProgressEvent) => void;
|
||||
clientFactory?: StructuredEphemeralTurnClientFactory | undefined;
|
||||
|
|
@ -85,7 +93,7 @@ export async function runStructuredEphemeralTurn(options: RunStructuredEphemeral
|
|||
};
|
||||
});
|
||||
|
||||
let client!: StructuredEphemeralTurnClient;
|
||||
let client!: StructuredEphemeralTurnRuntimeCapableClient;
|
||||
const clientFactory = options.clientFactory ?? ((codexPath, cwd, handlers) => new AppServerClient(codexPath, cwd, handlers));
|
||||
client = clientFactory(options.codexPath, options.cwd, {
|
||||
onNotification: (notification) => {
|
||||
|
|
@ -104,7 +112,9 @@ export async function runStructuredEphemeralTurn(options: RunStructuredEphemeral
|
|||
|
||||
try {
|
||||
await abortable(client.connect(), options.signal, options.abortMessage);
|
||||
const runtime = options.runtime ?? {};
|
||||
const runtime = options.resolveRuntime
|
||||
? await abortable(options.resolveRuntime(client), options.signal, options.abortMessage)
|
||||
: (options.runtime ?? {});
|
||||
const threadResponse = await abortable(
|
||||
client.startEphemeralThread(options.cwd, options.serviceName, options.developerInstructions),
|
||||
options.signal,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import {
|
|||
runStructuredEphemeralTurn,
|
||||
type StructuredEphemeralTurnClient,
|
||||
type StructuredEphemeralTurnClientFactory,
|
||||
type StructuredEphemeralTurnRuntimeClient,
|
||||
type StructuredTurnOutputSchema,
|
||||
} from "../../app-server/structured-ephemeral-turn";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import { loadPanelModelOptions } from "../../app-server/model-options";
|
||||
import { panelModelOptionsFromAppServerModels } from "../../app-server/catalog-model";
|
||||
import type { PanelModelOption } from "../../domain/catalog/metadata";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelOptions } from "../../domain/catalog/runtime-overrides";
|
||||
import type { SelectionRewriteRuntimeSettings } from "./model";
|
||||
|
|
@ -44,9 +45,6 @@ export type SelectionRewriteClientFactory = StructuredEphemeralTurnClientFactory
|
|||
export async function runSelectionRewrite(options: RunSelectionRewriteOptions): Promise<SelectionRewriteOutput> {
|
||||
let preview = "";
|
||||
const runtimeSettings = options.runtimeSettings;
|
||||
const runtime = runtimeSettings
|
||||
? await selectionRewriteRuntimeOverrideForCodex(options.codexPath, options.cwd, runtimeSettings)
|
||||
: undefined;
|
||||
const turn = await runStructuredEphemeralTurn({
|
||||
codexPath: options.codexPath,
|
||||
cwd: options.cwd,
|
||||
|
|
@ -60,7 +58,7 @@ export async function runSelectionRewrite(options: RunSelectionRewriteOptions):
|
|||
timedOutMessage: "Timed out while rewriting the selection.",
|
||||
abortMessage: "Selection rewrite cancelled.",
|
||||
signal: options.signal,
|
||||
runtime,
|
||||
resolveRuntime: runtimeSettings ? (client) => selectionRewriteRuntimeOverrideForClient(client, runtimeSettings) : undefined,
|
||||
clientFactory: options.clientFactory,
|
||||
onProgress: (event) => {
|
||||
if (event.type === "reasoning-activity") {
|
||||
|
|
@ -96,15 +94,15 @@ export function validatedSelectionRewriteRuntimeOverride(
|
|||
);
|
||||
}
|
||||
|
||||
async function selectionRewriteRuntimeOverrideForCodex(
|
||||
codexPath: string,
|
||||
cwd: string,
|
||||
async function selectionRewriteRuntimeOverrideForClient(
|
||||
client: StructuredEphemeralTurnRuntimeClient,
|
||||
settings: SelectionRewriteRuntimeSettings,
|
||||
): Promise<SelectionRewriteRuntimeOverride> {
|
||||
const runtime = selectionRewriteRuntimeOverride(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
return validatedSelectionRewriteRuntimeOverride(settings, await loadPanelModelOptions(codexPath, cwd, false));
|
||||
const response = await client.listModels(false);
|
||||
return validatedSelectionRewriteRuntimeOverride(settings, panelModelOptionsFromAppServerModels(response.data));
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Notice, Platform, SuggestModal, type App } from "obsidian";
|
||||
|
||||
import { withShortLivedAppServerClient } from "../../app-server/short-lived-client";
|
||||
import { panelThreadsFromAppServerThreads } from "../../app-server/thread-model";
|
||||
import { getThreadTitle } from "../../domain/threads/model";
|
||||
import type { PanelThread } from "../../domain/threads/model";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
import { withShortLivedFallbackAppServerClient } from "../../workspace/short-lived-app-server-client";
|
||||
import { shortThreadId } from "../../utils";
|
||||
|
||||
export interface ThreadPickerHost {
|
||||
|
|
@ -82,17 +82,17 @@ async function loadThreadPickerThreads(host: ThreadPickerHost): Promise<readonly
|
|||
const cached = host.cachedThreadList();
|
||||
if (cached) return cached;
|
||||
|
||||
return withShortLivedFallbackAppServerClient(
|
||||
{
|
||||
codexPath: host.settings.codexPath,
|
||||
cwd: host.vaultPath,
|
||||
unhandledServerRequestMessage: "Codex thread picker does not handle server requests.",
|
||||
},
|
||||
return withShortLivedAppServerClient(
|
||||
host.settings.codexPath,
|
||||
host.vaultPath,
|
||||
async (client) =>
|
||||
host.refreshThreadList(async () => {
|
||||
const response = await client.listThreads(host.vaultPath);
|
||||
return panelThreadsFromAppServerThreads(response.data);
|
||||
}),
|
||||
{
|
||||
unhandledServerRequestMessage: "Codex thread picker does not handle server requests.",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { type App, Notice, type Plugin, PluginSettingTab, Setting, setIcon } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../app-server/client";
|
||||
import { withShortLivedAppServerClient } from "../app-server/short-lived-client";
|
||||
import { DEFAULT_CODEX_PATH } from "../constants";
|
||||
import type { ReasoningEffort } from "../domain/catalog/metadata";
|
||||
import type { PanelHookItem, PanelModelOption } from "../domain/catalog/metadata";
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
transitionSettingsDataRefreshLifecycle,
|
||||
type SettingsDataRefreshLifecycleState,
|
||||
} from "./data";
|
||||
import { withShortLivedFallbackAppServerClient } from "../workspace/short-lived-app-server-client";
|
||||
import { loadHookData, loadSettingsData, restoreArchivedPanelThread, setPanelHookEnabled, trustPanelHook } from "./app-server-data";
|
||||
import { renderArchivedThreadSection, renderHookSection } from "./dynamic-sections";
|
||||
import type { CodexPanelSettings } from "./model";
|
||||
|
|
@ -498,14 +498,9 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private async withSettingsConnection<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> {
|
||||
return withShortLivedFallbackAppServerClient(
|
||||
{
|
||||
codexPath: this.plugin.settings.codexPath,
|
||||
cwd: this.plugin.vaultPath,
|
||||
unhandledServerRequestMessage: "Codex Panel settings does not handle server requests.",
|
||||
},
|
||||
operation,
|
||||
);
|
||||
return withShortLivedAppServerClient(this.plugin.settings.codexPath, this.plugin.vaultPath, operation, {
|
||||
unhandledServerRequestMessage: "Codex Panel settings does not handle server requests.",
|
||||
});
|
||||
}
|
||||
|
||||
private nextSettingsDynamicOperationId(): number {
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import type { AppServerClient } from "../app-server/client";
|
||||
import { withShortLivedAppServerClient } from "../app-server/short-lived-client";
|
||||
|
||||
export interface ShortLivedFallbackAppServerClientOptions {
|
||||
codexPath: string;
|
||||
cwd: string;
|
||||
unhandledServerRequestMessage: string;
|
||||
}
|
||||
|
||||
export async function withShortLivedFallbackAppServerClient<T>(
|
||||
options: ShortLivedFallbackAppServerClientOptions,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return withShortLivedAppServerClient(options.codexPath, options.cwd, operation, {
|
||||
unhandledServerRequestMessage: options.unhandledServerRequestMessage,
|
||||
});
|
||||
}
|
||||
|
|
@ -2,9 +2,10 @@ import {
|
|||
runStructuredEphemeralTurn,
|
||||
type StructuredEphemeralTurnClient,
|
||||
type StructuredEphemeralTurnClientFactory,
|
||||
type StructuredEphemeralTurnRuntimeClient,
|
||||
type StructuredTurnOutputSchema,
|
||||
} from "../app-server/structured-ephemeral-turn";
|
||||
import { loadPanelModelOptions } from "../app-server/model-options";
|
||||
import { panelModelOptionsFromAppServerModels } from "../app-server/catalog-model";
|
||||
import type { PanelModelOption, ReasoningEffort } from "../domain/catalog/metadata";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelOptions } from "../domain/catalog/runtime-overrides";
|
||||
import { namingPrompt, titleFromNamingTurn, type ThreadNamingContext } from "../domain/threads/naming";
|
||||
|
|
@ -48,7 +49,6 @@ export async function generateThreadTitleWithCodex(
|
|||
runtimeSettings: ThreadNamingRuntimeSettings,
|
||||
clientFactory?: ThreadNamingClientFactory,
|
||||
): Promise<string | null> {
|
||||
const runtime = await threadNamingRuntimeOverrideForCodex(codexPath, cwd, runtimeSettings);
|
||||
const turn = await runStructuredEphemeralTurn({
|
||||
codexPath,
|
||||
cwd,
|
||||
|
|
@ -60,7 +60,7 @@ export async function generateThreadTitleWithCodex(
|
|||
unhandledServerRequestMessage: "Thread title generation does not handle server requests.",
|
||||
exitedMessage: "Codex title generation app-server exited.",
|
||||
timedOutMessage: "Timed out while generating a Codex thread title.",
|
||||
runtime,
|
||||
resolveRuntime: (client) => threadNamingRuntimeOverrideForClient(client, runtimeSettings),
|
||||
clientFactory,
|
||||
});
|
||||
return titleFromNamingTurn(turn);
|
||||
|
|
@ -82,15 +82,15 @@ export function validatedThreadNamingRuntimeOverride(
|
|||
return validatedRuntimeOverrideForModelOptions({ model: settings.threadNamingModel, effort: settings.threadNamingEffort }, models);
|
||||
}
|
||||
|
||||
async function threadNamingRuntimeOverrideForCodex(
|
||||
codexPath: string,
|
||||
cwd: string,
|
||||
async function threadNamingRuntimeOverrideForClient(
|
||||
client: StructuredEphemeralTurnRuntimeClient,
|
||||
settings: ThreadNamingRuntimeSettings,
|
||||
): Promise<ThreadNamingRuntimeOverride> {
|
||||
const runtime = threadNamingRuntimeOverride(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
return validatedThreadNamingRuntimeOverride(settings, await loadPanelModelOptions(codexPath, cwd, false));
|
||||
const response = await client.listModels(false);
|
||||
return validatedThreadNamingRuntimeOverride(settings, panelModelOptionsFromAppServerModels(response.data));
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { RequestId } from "../../src/generated/app-server/RequestId";
|
|||
import type { ServerNotification } from "../../src/generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
import type { ReasoningEffort } from "../../src/generated/app-server/ReasoningEffort";
|
||||
import type { ModelListResponse } from "../../src/generated/app-server/v2/ModelListResponse";
|
||||
import type { Thread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem";
|
||||
import type { ThreadStartResponse } from "../../src/generated/app-server/v2/ThreadStartResponse";
|
||||
|
|
@ -92,6 +93,30 @@ describe("runStructuredEphemeralTurn", () => {
|
|||
"Structured test does not handle server requests.",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves runtime on the same client before starting the ephemeral thread", async () => {
|
||||
const callOrder: string[] = [];
|
||||
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
||||
fake.startEphemeralThreadImpl = async () => {
|
||||
callOrder.push("start-thread");
|
||||
return threadStartResponse("thread");
|
||||
};
|
||||
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
||||
});
|
||||
|
||||
await runStructuredEphemeralTurn({
|
||||
...runOptions(clientFactory),
|
||||
resolveRuntime: async (runtimeClient) => {
|
||||
callOrder.push("resolve-runtime");
|
||||
expect(runtimeClient).toBe(expectPresent(client.current));
|
||||
await runtimeClient.listModels(false);
|
||||
return { model: "gpt-5.1", effort: "low" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual(["resolve-runtime", "start-thread"]);
|
||||
expect(expectPresent(client.current).listModels).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
function runOptions(clientFactory: StructuredEphemeralTurnClientFactory): Parameters<typeof runStructuredEphemeralTurn>[0] {
|
||||
|
|
@ -127,7 +152,9 @@ function fakeStructuredTurnClientFactory(configure?: (client: FakeStructuredTurn
|
|||
|
||||
class FakeStructuredTurnClient implements StructuredEphemeralTurnClient {
|
||||
connectImpl: (() => Promise<InitializeResponse>) | null = null;
|
||||
startEphemeralThreadImpl: (() => Promise<ThreadStartResponse>) | null = null;
|
||||
startStructuredTurnImpl: (() => Promise<TurnStartResponse>) | null = null;
|
||||
readonly listModels = vi.fn(async (): Promise<ModelListResponse> => ({ data: [], nextCursor: null }));
|
||||
readonly rejectServerRequest = vi.fn();
|
||||
readonly structuredTurnStarted: Promise<void>;
|
||||
private resolveStructuredTurnStarted!: () => void;
|
||||
|
|
@ -147,7 +174,7 @@ class FakeStructuredTurnClient implements StructuredEphemeralTurnClient {
|
|||
}
|
||||
|
||||
async startEphemeralThread(): Promise<ThreadStartResponse> {
|
||||
return threadStartResponse("thread");
|
||||
return this.startEphemeralThreadImpl ? this.startEphemeralThreadImpl() : threadStartResponse("thread");
|
||||
}
|
||||
|
||||
async startStructuredTurn(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { JsonValue } from "../../../../src/generated/app-server/serde_json/
|
|||
import type { RequestId } from "../../../../src/generated/app-server/RequestId";
|
||||
import type { ReasoningEffort } from "../../../../src/generated/app-server/ReasoningEffort";
|
||||
import type { Model } from "../../../../src/generated/app-server/v2/Model";
|
||||
import type { ModelListResponse } from "../../../../src/generated/app-server/v2/ModelListResponse";
|
||||
import type { ServerNotification } from "../../../../src/generated/app-server/ServerNotification";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadItem } from "../../../../src/generated/app-server/v2/ThreadItem";
|
||||
|
|
@ -329,6 +330,10 @@ class FakeThreadNamingClient implements ThreadNamingClient {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
async listModels(): Promise<ModelListResponse> {
|
||||
return { data: [], nextCursor: null };
|
||||
}
|
||||
|
||||
rejectServerRequest(_requestId: RequestId, _code: number, _message: string): void {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import type { AppServerClientHandlers } from "../../../src/app-server/client";
|
||||
import type { PanelModelOption } from "../../../src/domain/catalog/metadata";
|
||||
import type { InitializeResponse } from "../../../src/generated/app-server/InitializeResponse";
|
||||
import type { ModelListResponse } from "../../../src/generated/app-server/v2/ModelListResponse";
|
||||
import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification";
|
||||
import type { JsonValue } from "../../../src/generated/app-server/serde_json/JsonValue";
|
||||
import type { RequestId } from "../../../src/generated/app-server/RequestId";
|
||||
|
|
@ -664,6 +665,10 @@ class FakeSelectionRewriteClient implements SelectionRewriteClient {
|
|||
this.disconnected = true;
|
||||
}
|
||||
|
||||
async listModels(): Promise<ModelListResponse> {
|
||||
return { data: [], nextCursor: null };
|
||||
}
|
||||
|
||||
rejectServerRequest(_requestId: RequestId, _code: number, _message: string): void {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue