Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/update-codex-protocol-0-144-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pwrdrvr/agent-client": patch
---

Update @pwrdrvr/codex-app-server-protocol to 0.144.0.
2 changes: 1 addition & 1 deletion packages/agent-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"dependencies": {
"@pwrdrvr/agent-core": "workspace:^",
"@pwrdrvr/agent-transport": "workspace:^",
"@pwrdrvr/codex-app-server-protocol": "^0.135.0",
"@pwrdrvr/codex-app-server-protocol": "^0.144.0",
"@pwrdrvr/codex-discovery": "workspace:^",
"zod": "^4.0.0"
},
Expand Down
28 changes: 22 additions & 6 deletions packages/agent-client/src/chat/define-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { z } from "zod";
import type {
DynamicToolCallOutputContentItem,
DynamicToolNamespaceTool,
DynamicToolSpec
} from "@pwrdrvr/codex-app-server-protocol/v2";

Expand Down Expand Up @@ -83,15 +84,30 @@ export function defineTool<TArgs>(spec: ToolSpec<TArgs>): ToolSpec<TArgs> {
export type AnyToolSpec = ToolSpec<any>;

/**
* Convert a `ToolSpec` into the protocol `DynamicToolSpec` registered with Codex
* at `thread/start`. The `inputSchema` is derived from the tool's zod
* `argsSchema` via zod v4's `z.toJSONSchema()` (JSON Schema draft 2020-12).
* Convert a `ToolSpec` into the protocol function-tool shape nested inside a
* Codex dynamic-tool namespace. The `inputSchema` is derived from the tool's
* zod `argsSchema` via zod v4's `z.toJSONSchema()` (JSON Schema draft 2020-12).
*/
export function toDynamicToolSpec(spec: AnyToolSpec): DynamicToolSpec {
export function toDynamicToolNamespaceTool(spec: AnyToolSpec): DynamicToolNamespaceTool {
return {
namespace: spec.namespace,
type: "function",
name: spec.name,
description: spec.description,
inputSchema: z.toJSONSchema(spec.argsSchema) as DynamicToolSpec["inputSchema"]
inputSchema: z.toJSONSchema(spec.argsSchema) as DynamicToolNamespaceTool["inputSchema"]
};
}

/**
* Convert a `ToolSpec` into a standalone protocol `DynamicToolSpec`. The current
* protocol carries namespaces as top-level specs containing function tools, so a
* single tool becomes a one-tool namespace. `buildToolCatalog` groups tools that
* share a namespace before registration.
*/
export function toDynamicToolSpec(spec: AnyToolSpec): DynamicToolSpec {
return {
type: "namespace",
name: spec.namespace,
description: `Tools in the ${spec.namespace} namespace.`,
tools: [toDynamicToolNamespaceTool(spec)]
};
}
19 changes: 17 additions & 2 deletions packages/agent-client/src/chat/tool-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,32 @@ import { z } from "zod";
import type {
DynamicToolCallParams,
DynamicToolCallResponse,
DynamicToolNamespaceTool,
DynamicToolSpec
} from "@pwrdrvr/codex-app-server-protocol/v2";
import type { AnyToolSpec } from "./define-tool";
import { toDynamicToolSpec } from "./define-tool";
import { toDynamicToolNamespaceTool } from "./define-tool";

/**
* Build the `DynamicToolSpec[]` registered with Codex at `thread/start`. Pure
* projection of the catalog — an empty catalog yields an empty spec list.
*/
export function buildToolCatalog(catalog: ReadonlyArray<AnyToolSpec>): DynamicToolSpec[] {
return catalog.map(toDynamicToolSpec);
const namespaces = new Map<string, DynamicToolNamespaceTool[]>();
for (const tool of catalog) {
const group = namespaces.get(tool.namespace);
if (group === undefined) {
namespaces.set(tool.namespace, [toDynamicToolNamespaceTool(tool)]);
} else {
group.push(toDynamicToolNamespaceTool(tool));
}
}
return Array.from(namespaces, ([name, tools]) => ({
type: "namespace",
name,
description: `Tools in the ${name} namespace.`,
tools
}));
}

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/agent-client/src/codex-oneshot-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,7 @@ export class CodexOneShotClient {
// dedicated cwd keeps the worker out of any host repo/worktree.
...(this.options.threadConfig !== undefined ? { config: this.options.threadConfig } : {}),
environments: [],
experimentalRawEvents: false,
persistExtendedHistory: false
experimentalRawEvents: false
},
this.requestTimeoutMs
)) as ThreadStartResponse;
Expand Down
9 changes: 3 additions & 6 deletions packages/agent-client/src/codex-thread-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,7 @@ export class CodexThreadClient implements AgentBackend {
await this.initialize();

const params: ThreadForkParams = {
threadId: opts.sourceThreadId,
persistExtendedHistory: false
threadId: opts.sourceThreadId
};
if (opts.cwd !== undefined) params.cwd = opts.cwd;
if (opts.workspaceRoots !== undefined) params.runtimeWorkspaceRoots = [...opts.workspaceRoots];
Expand Down Expand Up @@ -283,8 +282,7 @@ export class CodexThreadClient implements AgentBackend {

// exactOptionalPropertyTypes: only attach a key when the caller supplied it.
const params: ThreadStartParams = {
experimentalRawEvents: false,
persistExtendedHistory: false
experimentalRawEvents: false
};
if (opts.cwd !== undefined) params.cwd = opts.cwd;
if (opts.model !== undefined) params.model = opts.model;
Expand Down Expand Up @@ -331,8 +329,7 @@ export class CodexThreadClient implements AgentBackend {
await this.initialize();

const params: ThreadResumeParams = {
threadId,
persistExtendedHistory: false
threadId
};
const response = (await connection.request(
"thread/resume",
Expand Down
40 changes: 33 additions & 7 deletions packages/agent-client/test/define-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,47 @@ const listTool = defineTool({
describe("defineTool / toDynamicToolSpec", () => {
it("serializes a tool spec to a valid DynamicToolSpec via z.toJSONSchema", () => {
const spec = toDynamicToolSpec(listTool as ToolSpec<unknown>);
expect(spec.namespace).toBe("host_tools");
expect(spec.name).toBe("library_list");
expect(spec.description).toBe("List captures in the library.");
expect(spec.inputSchema).toMatchObject({
expect(spec.type).toBe("namespace");
if (spec.type !== "namespace") {
throw new Error("expected namespace dynamic tool spec");
}
expect(spec.name).toBe("host_tools");
expect(spec.description).toBe("Tools in the host_tools namespace.");
expect(spec.tools).toHaveLength(1);
expect(spec.tools[0]).toMatchObject({
type: "function",
name: "library_list",
description: "List captures in the library."
});
expect(spec.tools[0]?.inputSchema).toMatchObject({
type: "object",
properties: {
limit: { type: "integer", exclusiveMinimum: 0, maximum: 200 }
}
});
});

it("builds the catalog as a pure projection of the tool list", () => {
const catalog = buildToolCatalog([listTool as ToolSpec<unknown>]);
it("builds the catalog grouped by namespace", () => {
const renderTool = defineTool({
namespace: "host_tools",
name: "render",
description: "Render a capture.",
argsSchema: z.object({ id: z.string() }),
dispatch: async () => ({ ok: true, data: {} })
});
const catalog = buildToolCatalog([
listTool as ToolSpec<unknown>,
renderTool as ToolSpec<unknown>
]);
expect(catalog).toHaveLength(1);
expect(catalog[0]?.name).toBe("library_list");
expect(catalog[0]?.name).toBe("host_tools");
expect(catalog[0]).toMatchObject({
type: "namespace",
tools: [
{ type: "function", name: "library_list" },
{ type: "function", name: "render" }
]
});
expect(buildToolCatalog([])).toEqual([]);
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/agent-client/test/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ function syntheticSequence(): Array<{ method: string; params: unknown }> {
effort: "medium" as never,
summary: null,
collaborationMode: "off" as never,
multiAgentMode: "explicitRequestOnly",
personality: null
}
};
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading