mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Own executable- and vault-bound resources in one disposable runtime. On executable changes, detach stable view shells, dispose all bound work, and publish a fully attached replacement runtime.
545 lines
21 KiB
TypeScript
545 lines
21 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { AppServerClientHandlers, ClientResponseByMethod, TypedClientRequestMethod } from "../../src/app-server/connection/client";
|
|
import type { ClientRequestParams } from "../../src/app-server/connection/rpc-messages";
|
|
import type { TurnItem, TurnRecord } from "../../src/app-server/protocol/turn";
|
|
import {
|
|
type EphemeralStructuredTurnClient,
|
|
type EphemeralStructuredTurnClientFactory,
|
|
runEphemeralStructuredTurn,
|
|
} from "../../src/app-server/services/ephemeral-structured-turn";
|
|
import type { AppServerStartEphemeralThreadOptions } from "../../src/app-server/services/threads";
|
|
import type { AppServerStartStructuredTurnOptions } from "../../src/app-server/services/turns";
|
|
import type { InitializeResponse } from "../../src/generated/app-server/InitializeResponse";
|
|
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 { Thread as AppServerThread } from "../../src/generated/app-server/v2/Thread";
|
|
import type { ThreadStartResponse } from "../../src/generated/app-server/v2/ThreadStartResponse";
|
|
|
|
interface TurnStartResponse {
|
|
turn: TurnRecord;
|
|
}
|
|
|
|
describe("runEphemeralStructuredTurn", () => {
|
|
it("fills completed turn items from item completion notifications", async () => {
|
|
const { clientFactory } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => {
|
|
fake.emit(completedItemNotification("thread", "turn", agentMessage("answer", '{"ok":true}')));
|
|
fake.emit(turnCompletedNotification("thread", turn([], { id: "turn", status: "completed" })));
|
|
return { turn: turn([], { id: "turn", status: "inProgress" }) };
|
|
};
|
|
});
|
|
|
|
const result = await runEphemeralStructuredTurn(runOptions(), { clientFactory });
|
|
|
|
expect(result.items).toEqual([agentMessage("answer", '{"ok":true}')]);
|
|
expect(result.itemsView).toBe("full");
|
|
});
|
|
|
|
it("reports matched structured turn progress without exposing raw notifications", async () => {
|
|
const progress: unknown[] = [];
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory();
|
|
const running = runEphemeralStructuredTurn(
|
|
{
|
|
...runOptions(),
|
|
onProgress: (event) => {
|
|
progress.push(event);
|
|
},
|
|
},
|
|
{ clientFactory },
|
|
);
|
|
|
|
await expectPresent(client.current).structuredTurnStarted;
|
|
const fake = expectPresent(client.current);
|
|
fake.emit(agentDeltaNotification("other-thread", "turn", "ignored"));
|
|
fake.emit(reasoningNotification("thread", "turn"));
|
|
fake.emit(agentDeltaNotification("thread", "turn", "draft"));
|
|
fake.emit(completedItemNotification("thread", "turn", agentMessage("answer", '{"ok":true}')));
|
|
fake.emit(turnCompletedNotification("thread", turn([], { id: "turn", status: "completed" })));
|
|
|
|
await running;
|
|
expect(progress).toEqual([{ type: "reasoning-activity" }, { type: "agent-message-delta", delta: "draft" }]);
|
|
});
|
|
|
|
it("keeps pre-acknowledgement completion when turn notifications arrive before turn/start resolves", async () => {
|
|
const { clientFactory } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => {
|
|
fake.emit(completedItemNotification("thread", "early-turn", agentMessage("answer", '{"ok":true}')));
|
|
fake.emit(turnCompletedNotification("thread", turn([], { id: "early-turn", status: "completed" })));
|
|
return { turn: turn([], { id: "early-turn", status: "inProgress" }) };
|
|
};
|
|
});
|
|
|
|
const result = await runEphemeralStructuredTurn(runOptions(), { clientFactory });
|
|
|
|
expect(result.items).toEqual([agentMessage("answer", '{"ok":true}')]);
|
|
});
|
|
|
|
it("cleans up abort listeners after an operation settles", async () => {
|
|
const controller = new AbortController();
|
|
const add = vi.spyOn(controller.signal, "addEventListener");
|
|
const remove = vi.spyOn(controller.signal, "removeEventListener");
|
|
const { clientFactory } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn({ ...runOptions(), signal: controller.signal }, { clientFactory });
|
|
|
|
expect(add).toHaveBeenCalledWith("abort", expect.any(Function), { once: true });
|
|
expect(remove).toHaveBeenCalledTimes(add.mock.calls.length);
|
|
});
|
|
|
|
it("uses injected timers for structured turn timeout cleanup", async () => {
|
|
const timers = {
|
|
setTimeout: vi.fn((_callback: () => void, _delayMs: number) => 123),
|
|
clearTimeout: vi.fn(),
|
|
};
|
|
const { clientFactory } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn(runOptions(), { clientFactory, timers });
|
|
|
|
expect(timers.setTimeout).toHaveBeenCalledWith(expect.any(Function), 10_000);
|
|
expect(timers.clearTimeout).toHaveBeenCalledWith(123);
|
|
});
|
|
|
|
it("deletes the ephemeral thread before disconnecting after a completed turn", async () => {
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn(runOptions(), { clientFactory });
|
|
|
|
const fake = expectPresent(client.current);
|
|
expect(fake.deleteThreadRequest).toHaveBeenCalledWith({ threadId: "thread" }, { timeoutMs: 5_000 });
|
|
expect(expectPresent(fake.disconnect.mock.invocationCallOrder[0])).toBeGreaterThan(
|
|
expectPresent(fake.deleteThreadRequest.mock.invocationCallOrder[0]),
|
|
);
|
|
});
|
|
|
|
it("reports its client lifetime to the owning execution runtime", async () => {
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
const clientLifecycle = { created: vi.fn(), disposed: vi.fn() };
|
|
|
|
await runEphemeralStructuredTurn(runOptions(), { clientFactory, clientLifecycle });
|
|
|
|
expect(clientLifecycle.created).toHaveBeenCalledWith(client.current);
|
|
expect(clientLifecycle.disposed).toHaveBeenCalledWith(client.current);
|
|
});
|
|
|
|
it("does not try to delete when no ephemeral thread was started", async () => {
|
|
const timers = timerHarness();
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.connectImpl = () => new Promise<InitializeResponse>(() => undefined);
|
|
});
|
|
|
|
const running = runEphemeralStructuredTurn(runOptions(), { clientFactory, timers });
|
|
await Promise.resolve();
|
|
|
|
timers.fireTimeout();
|
|
|
|
await expect(running).rejects.toThrow("Structured test timed out.");
|
|
expect(expectPresent(client.current).deleteThreadRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the completed turn result when deleting the ephemeral thread fails", async () => {
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.deleteThreadRequest.mockRejectedValueOnce(new Error("delete failed"));
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await expect(runEphemeralStructuredTurn(runOptions(), { clientFactory })).resolves.toMatchObject({
|
|
items: [agentMessage("answer", '{"ok":true}')],
|
|
});
|
|
expect(expectPresent(client.current).disconnect).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects server requests with the configured message", async () => {
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.connectImpl = async () => {
|
|
fake.emitServerRequest(serverRequest(123));
|
|
return { codexHome: "/tmp/codex" } as InitializeResponse;
|
|
};
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn(runOptions(), { clientFactory });
|
|
|
|
expect(expectPresent(client.current).rejectServerRequest).toHaveBeenCalledWith(
|
|
123,
|
|
-32601,
|
|
"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.modelListImpl = async () => {
|
|
callOrder.push("model-list");
|
|
return { data: [], nextCursor: null };
|
|
};
|
|
fake.startEphemeralThreadImpl = async () => {
|
|
callOrder.push("start-thread");
|
|
return threadStartResponse("thread");
|
|
};
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn(
|
|
{
|
|
...runOptions(),
|
|
runtimeSettings: { model: "gpt-5.1", effort: "low" },
|
|
},
|
|
{ clientFactory },
|
|
);
|
|
|
|
expect(callOrder).toEqual(["model-list", "start-thread"]);
|
|
expect(expectPresent(client.current).modelListRequests).toEqual([{ includeHidden: false, cursor: null, limit: 100 }]);
|
|
expect(expectPresent(client.current).startStructuredTurnOptions).toEqual({
|
|
threadId: "thread",
|
|
cwd: "/vault",
|
|
text: "Run.",
|
|
outputSchema: { type: "object" },
|
|
runtime: { model: "gpt-5.1", effort: "low" },
|
|
});
|
|
});
|
|
|
|
it("starts the ephemeral thread with named service options", async () => {
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = async () => ({ turn: turn([agentMessage("answer", '{"ok":true}')]) });
|
|
});
|
|
|
|
await runEphemeralStructuredTurn(runOptions(), { clientFactory });
|
|
|
|
expect(expectPresent(client.current).startEphemeralThreadOptions).toEqual({
|
|
cwd: "/vault",
|
|
serviceName: "structured-test",
|
|
developerInstructions: "Return JSON.",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
stage: "connect",
|
|
configure: (fake: FakeStructuredTurnClient): void => {
|
|
fake.connectImpl = () => new Promise<InitializeResponse>(() => undefined);
|
|
},
|
|
},
|
|
{
|
|
stage: "runtime resolution",
|
|
configure: (fake: FakeStructuredTurnClient): void => {
|
|
fake.modelListImpl = () => new Promise<ClientResponseByMethod["model/list"]>(() => undefined);
|
|
},
|
|
runtimeSettings: { model: "gpt-5.1", effort: "low" } as const,
|
|
},
|
|
{
|
|
stage: "ephemeral thread start",
|
|
configure: (fake: FakeStructuredTurnClient): void => {
|
|
fake.startEphemeralThreadImpl = () => new Promise<ThreadStartResponse>(() => undefined);
|
|
},
|
|
},
|
|
{
|
|
stage: "structured turn start",
|
|
configure: (fake: FakeStructuredTurnClient): void => {
|
|
fake.startStructuredTurnImpl = () => new Promise<TurnStartResponse>(() => undefined);
|
|
},
|
|
},
|
|
{ stage: "completion wait", configure: (_fake: FakeStructuredTurnClient): void => undefined },
|
|
])("times out during $stage and disconnects the client", async ({ configure, runtimeSettings }) => {
|
|
const timers = timerHarness();
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory(configure);
|
|
|
|
const running = runEphemeralStructuredTurn(
|
|
{
|
|
...runOptions(),
|
|
...(runtimeSettings ? { runtimeSettings } : {}),
|
|
},
|
|
{ clientFactory, timers },
|
|
);
|
|
await Promise.resolve();
|
|
|
|
timers.fireTimeout();
|
|
|
|
await expect(running).rejects.toThrow("Structured test timed out.");
|
|
expect(expectPresent(client.current).disconnect).toHaveBeenCalledOnce();
|
|
expect(timers.clearTimeout).toHaveBeenCalledWith(123);
|
|
});
|
|
|
|
it("deletes a started ephemeral thread when structured turn startup times out", async () => {
|
|
const timers = timerHarness();
|
|
const { clientFactory, client } = fakeStructuredTurnClientFactory((fake) => {
|
|
fake.startStructuredTurnImpl = () => new Promise<TurnStartResponse>(() => undefined);
|
|
});
|
|
|
|
const running = runEphemeralStructuredTurn(runOptions(), { clientFactory, timers });
|
|
await expectPresent(client.current).structuredTurnStarted;
|
|
|
|
timers.fireTimeout();
|
|
|
|
await expect(running).rejects.toThrow("Structured test timed out.");
|
|
expect(expectPresent(client.current).deleteThreadRequest).toHaveBeenCalledWith({ threadId: "thread" }, { timeoutMs: 5_000 });
|
|
});
|
|
});
|
|
|
|
function runOptions(): Parameters<typeof runEphemeralStructuredTurn>[0] {
|
|
return {
|
|
codexPath: "/bin/codex",
|
|
cwd: "/vault",
|
|
serviceName: "structured-test",
|
|
developerInstructions: "Return JSON.",
|
|
prompt: "Run.",
|
|
outputSchema: { type: "object" },
|
|
timeoutMs: 10_000,
|
|
serverRequests: { kind: "reject", message: "Structured test does not handle server requests." },
|
|
exitedMessage: "Structured test app-server exited.",
|
|
timedOutMessage: "Structured test timed out.",
|
|
};
|
|
}
|
|
|
|
function fakeStructuredTurnClientFactory(configure?: (client: FakeStructuredTurnClient) => void): {
|
|
clientFactory: EphemeralStructuredTurnClientFactory;
|
|
client: { current: FakeStructuredTurnClient | null };
|
|
} {
|
|
const client: { current: FakeStructuredTurnClient | null } = { current: null };
|
|
return {
|
|
client,
|
|
clientFactory: (_codexPath, _cwd, handlers) => {
|
|
client.current = new FakeStructuredTurnClient(handlers);
|
|
configure?.(client.current);
|
|
return client.current;
|
|
},
|
|
};
|
|
}
|
|
|
|
class FakeStructuredTurnClient implements EphemeralStructuredTurnClient {
|
|
connectImpl: (() => Promise<InitializeResponse>) | null = null;
|
|
modelListImpl: (() => Promise<ClientResponseByMethod["model/list"]>) | null = null;
|
|
startEphemeralThreadImpl: (() => Promise<ThreadStartResponse>) | null = null;
|
|
startStructuredTurnImpl: (() => Promise<TurnStartResponse>) | null = null;
|
|
startEphemeralThreadOptions: AppServerStartEphemeralThreadOptions | null = null;
|
|
startStructuredTurnOptions: AppServerStartStructuredTurnOptions | null = null;
|
|
readonly modelListRequests: ClientRequestParams<"model/list">[] = [];
|
|
readonly rejectServerRequest = vi.fn();
|
|
readonly deleteThreadRequest = vi.fn(
|
|
async (_params: ClientRequestParams<"thread/delete">, _options?: { timeoutMs?: number }) => undefined,
|
|
);
|
|
readonly disconnect = vi.fn();
|
|
readonly structuredTurnStarted: Promise<void>;
|
|
private resolveStructuredTurnStarted!: () => void;
|
|
|
|
constructor(private readonly handlers: AppServerClientHandlers) {
|
|
this.structuredTurnStarted = new Promise((resolve) => {
|
|
this.resolveStructuredTurnStarted = resolve;
|
|
});
|
|
}
|
|
|
|
async connect(): Promise<InitializeResponse> {
|
|
return this.connectImpl ? this.connectImpl() : ({ codexHome: "/tmp/codex" } as InitializeResponse);
|
|
}
|
|
|
|
async request<M extends TypedClientRequestMethod>(
|
|
method: M,
|
|
params: ClientRequestParams<M>,
|
|
options: { timeoutMs?: number } = {},
|
|
): Promise<ClientResponseByMethod[M]> {
|
|
switch (method) {
|
|
case "model/list":
|
|
this.modelListRequests.push(params as ClientRequestParams<"model/list">);
|
|
return (this.modelListImpl ? await this.modelListImpl() : { data: [], nextCursor: null }) as ClientResponseByMethod[M];
|
|
case "thread/start":
|
|
this.startEphemeralThreadOptions = ephemeralThreadOptionsFromParams(params as ClientRequestParams<"thread/start">);
|
|
return (this.startEphemeralThreadImpl
|
|
? await this.startEphemeralThreadImpl()
|
|
: threadStartResponse("thread")) as unknown as ClientResponseByMethod[M];
|
|
case "turn/start":
|
|
this.startStructuredTurnOptions = structuredTurnOptionsFromParams(params as ClientRequestParams<"turn/start">);
|
|
this.resolveStructuredTurnStarted();
|
|
return (this.startStructuredTurnImpl
|
|
? await this.startStructuredTurnImpl()
|
|
: { turn: turn([], { id: "turn", status: "inProgress" }) }) as unknown as ClientResponseByMethod[M];
|
|
case "thread/delete":
|
|
await this.deleteThreadRequest(params as ClientRequestParams<"thread/delete">, options);
|
|
return {} as unknown as ClientResponseByMethod[M];
|
|
default:
|
|
throw new Error(`Unexpected app-server request: ${method}`);
|
|
}
|
|
}
|
|
|
|
emit(notification: ServerNotification): void {
|
|
this.handlers.onNotification(notification);
|
|
}
|
|
|
|
emitServerRequest(request: ServerRequest): void {
|
|
this.handlers.onServerRequest(request, {
|
|
respond: () => undefined,
|
|
reject: (code, message) => {
|
|
this.rejectServerRequest(request.id, code, message);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function ephemeralThreadOptionsFromParams(params: ClientRequestParams<"thread/start">): AppServerStartEphemeralThreadOptions {
|
|
if (!params.cwd || !params.serviceName || !params.developerInstructions) throw new Error("Expected ephemeral thread params.");
|
|
return {
|
|
cwd: params.cwd,
|
|
serviceName: params.serviceName,
|
|
developerInstructions: params.developerInstructions,
|
|
};
|
|
}
|
|
|
|
function structuredTurnOptionsFromParams(params: ClientRequestParams<"turn/start">): AppServerStartStructuredTurnOptions {
|
|
const textItem = params.input[0];
|
|
if (!params.cwd || !textItem || textItem.type !== "text" || !params.outputSchema) throw new Error("Expected structured turn params.");
|
|
const runtime = structuredTurnRuntimeFromParams(params);
|
|
return {
|
|
threadId: params.threadId,
|
|
cwd: params.cwd,
|
|
text: textItem.text,
|
|
outputSchema: params.outputSchema,
|
|
...(runtime !== undefined ? { runtime } : {}),
|
|
};
|
|
}
|
|
|
|
function structuredTurnRuntimeFromParams(params: ClientRequestParams<"turn/start">): AppServerStartStructuredTurnOptions["runtime"] {
|
|
const runtime = {
|
|
...(params.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}),
|
|
...(params.collaborationMode !== undefined && params.collaborationMode !== null ? { collaborationMode: params.collaborationMode } : {}),
|
|
...(params.model !== undefined ? { model: params.model } : {}),
|
|
...(params.effort !== undefined ? { effort: params.effort } : {}),
|
|
...(params.approvalsReviewer !== undefined ? { approvalsReviewer: params.approvalsReviewer } : {}),
|
|
};
|
|
return Object.keys(runtime).length > 0 ? runtime : undefined;
|
|
}
|
|
|
|
function timerHarness(): {
|
|
setTimeout: ReturnType<typeof vi.fn<(callback: () => void, delayMs: number) => number>>;
|
|
clearTimeout: ReturnType<typeof vi.fn<(timer: number) => void>>;
|
|
fireTimeout(): void;
|
|
} {
|
|
let timeoutCallback: (() => void) | null = null;
|
|
return {
|
|
setTimeout: vi.fn((callback: () => void, _delayMs: number) => {
|
|
timeoutCallback = callback;
|
|
return 123;
|
|
}),
|
|
clearTimeout: vi.fn(),
|
|
fireTimeout: () => {
|
|
if (!timeoutCallback) throw new Error("Expected timeout to be scheduled.");
|
|
timeoutCallback();
|
|
},
|
|
};
|
|
}
|
|
|
|
function threadStartResponse(threadId: string): ThreadStartResponse {
|
|
return {
|
|
thread: thread(threadId),
|
|
model: "gpt-5.1",
|
|
modelProvider: "openai",
|
|
serviceTier: null,
|
|
approvalPolicy: "never",
|
|
cwd: "/vault",
|
|
runtimeWorkspaceRoots: [],
|
|
instructionSources: [],
|
|
approvalsReviewer: "auto_review",
|
|
activePermissionProfile: null,
|
|
sandbox: { type: "readOnly", networkAccess: false },
|
|
reasoningEffort: null,
|
|
multiAgentMode: "explicitRequestOnly",
|
|
};
|
|
}
|
|
|
|
function thread(id: string): AppServerThread {
|
|
return {
|
|
id,
|
|
extra: null,
|
|
sessionId: "session",
|
|
forkedFromId: null,
|
|
parentThreadId: null,
|
|
preview: "",
|
|
ephemeral: true,
|
|
historyMode: "paginated",
|
|
modelProvider: "openai",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
recencyAt: null,
|
|
status: { type: "idle" },
|
|
path: null,
|
|
cwd: "/vault",
|
|
cliVersion: "0.0.0",
|
|
source: "unknown",
|
|
threadSource: null,
|
|
agentNickname: null,
|
|
agentRole: null,
|
|
gitInfo: null,
|
|
name: null,
|
|
turns: [],
|
|
};
|
|
}
|
|
|
|
function turn(items: TurnRecord["items"], overrides: Partial<TurnRecord> = {}): TurnRecord {
|
|
return {
|
|
id: "turn",
|
|
items,
|
|
itemsView: "full",
|
|
status: "completed",
|
|
error: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function agentMessage(id: string, text: string): TurnItem {
|
|
return { type: "agentMessage", id, text, phase: "final_answer", memoryCitation: null };
|
|
}
|
|
|
|
function completedItemNotification(threadId: string, turnId: string, item: TurnItem): ServerNotification {
|
|
type ItemCompletedNotification = Extract<ServerNotification, { method: "item/completed" }>;
|
|
return {
|
|
method: "item/completed",
|
|
params: { threadId, turnId, item, completedAtMs: 1 } as unknown as ItemCompletedNotification["params"],
|
|
};
|
|
}
|
|
|
|
function turnCompletedNotification(threadId: string, completedTurn: TurnRecord): ServerNotification {
|
|
type TurnCompletedNotification = Extract<ServerNotification, { method: "turn/completed" }>;
|
|
return {
|
|
method: "turn/completed",
|
|
params: { threadId, turn: completedTurn } as unknown as TurnCompletedNotification["params"],
|
|
};
|
|
}
|
|
|
|
function agentDeltaNotification(threadId: string, turnId: string, delta: string): ServerNotification {
|
|
return {
|
|
method: "item/agentMessage/delta",
|
|
params: { threadId, turnId, itemId: "agent", delta },
|
|
};
|
|
}
|
|
|
|
function reasoningNotification(threadId: string, turnId: string): ServerNotification {
|
|
return {
|
|
method: "item/reasoning/textDelta",
|
|
params: { threadId, turnId, itemId: "reasoning", contentIndex: 0, delta: "thinking" },
|
|
};
|
|
}
|
|
|
|
function serverRequest(id: RequestId): ServerRequest {
|
|
return {
|
|
id,
|
|
method: "item/tool/requestUserInput",
|
|
params: {},
|
|
} as ServerRequest;
|
|
}
|
|
|
|
function expectPresent<T>(value: T | null | undefined): T {
|
|
if (value == null) throw new Error("Expected value to be present.");
|
|
return value;
|
|
}
|