@howaboua/pi-codex-conversion 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Codex-oriented adapter for [Pi](https://github.com/badlogic/pi-mono).
4
4
 
5
5
  This package replaces Pi's default Codex/GPT experience with a narrower Codex-like surface while staying close to Pi's own runtime and prompt construction:
6
6
 
7
- - swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, and `view_image`
7
+ - swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, `view_image`, and native OpenAI Codex Responses `web_search` on `openai-codex`
8
8
  - preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
9
9
  - renders exec activity with Codex-style command and background-terminal labels
10
10
 
@@ -18,6 +18,7 @@ When the adapter is active, the LLM sees these tools:
18
18
  - `write_stdin` — continue or poll a running exec session
19
19
  - `apply_patch` — patch tool
20
20
  - `view_image` — image-only wrapper around Pi's native image reading, enabled only for image-capable models
21
+ - `web_search` — native OpenAI Codex Responses web search, enabled only on the `openai-codex` provider
21
22
 
22
23
  Notably:
23
24
 
@@ -53,6 +54,7 @@ npm run check
53
54
  - `write_stdin({ session_id, chars: "" })` renders like `Waited for background terminal`
54
55
  - `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
55
56
  - `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
57
+ - `web_search` is surfaced only on `openai-codex`, and the adapter rewrites it into the native OpenAI Responses `type: "web_search"` payload instead of executing a local function tool
56
58
 
57
59
  Raw command output is still available by expanding the tool result.
58
60
 
@@ -90,6 +92,7 @@ That keeps the prompt much closer to `pi-mono` while still steering the model to
90
92
  - Adapter mode activates automatically for OpenAI `gpt*` and `codex*` models.
91
93
  - When you switch away from those models, Pi restores the previous active tool set.
92
94
  - `view_image` resolves paths against the active session cwd and only exposes `detail: "original"` for Codex-family image-capable models.
95
+ - `web_search` is exposed only for the `openai-codex` provider and is forwarded as the native OpenAI Codex Responses web search tool.
93
96
  - `apply_patch` paths stay restricted to the current working directory.
94
97
  - `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
95
98
  - PTY output handling applies basic terminal rewrite semantics (`\r`, `\b`, erase-in-line, and common escape cleanup) so interactive redraws replay sensibly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -6,6 +6,11 @@ export interface CodexLikeModelDescriptor {
6
6
  id: string;
7
7
  }
8
8
 
9
+ export function isOpenAICodexModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
10
+ if (!model) return false;
11
+ return (model.provider ?? "").toLowerCase() === "openai-codex";
12
+ }
13
+
9
14
  // Keep model detection intentionally conservative. The adapter replaces the
10
15
  // system prompt and tool surface, so false positives are worse than misses.
11
16
  export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
@@ -21,3 +26,7 @@ export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null
21
26
  export function isCodexLikeContext(ctx: ExtensionContext): boolean {
22
27
  return isCodexLikeModel(ctx.model);
23
28
  }
29
+
30
+ export function isOpenAICodexContext(ctx: ExtensionContext): boolean {
31
+ return isOpenAICodexModel(ctx.model);
32
+ }
@@ -5,3 +5,4 @@ export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
5
5
 
6
6
  export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
7
7
  export const VIEW_IMAGE_TOOL_NAME = "view_image";
8
+ export const WEB_SEARCH_TOOL_NAME = "web_search";
package/src/index.ts CHANGED
@@ -1,21 +1,30 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME } from "./adapter/tool-set.ts";
2
+ import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
3
3
  import { registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
4
- import { isCodexLikeContext } from "./adapter/codex-model.ts";
4
+ import { isCodexLikeContext, isOpenAICodexContext } from "./adapter/codex-model.ts";
5
5
  import { createExecCommandTracker } from "./tools/exec-command-state.ts";
6
6
  import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
7
7
  import { createExecSessionManager } from "./tools/exec-session-manager.ts";
8
8
  import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from "./prompt/build-system-prompt.ts";
9
9
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
10
+ import {
11
+ WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
12
+ payloadContainsWebSearchTool,
13
+ registerWebSearchMessageRenderer,
14
+ registerWebSearchTool,
15
+ rewriteNativeWebSearchTool,
16
+ supportsNativeWebSearch,
17
+ } from "./tools/web-search-tool.ts";
10
18
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
11
19
 
12
20
  interface AdapterState {
13
21
  enabled: boolean;
14
22
  previousToolNames?: string[];
15
23
  promptSkills: PromptSkill[];
24
+ pendingWebSearchCount: number;
16
25
  }
17
26
 
18
- const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
27
+ const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME];
19
28
 
20
29
  function getCommandArg(args: unknown): string | undefined {
21
30
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -26,12 +35,14 @@ function getCommandArg(args: unknown): string | undefined {
26
35
 
27
36
  export default function codexConversion(pi: ExtensionAPI) {
28
37
  const tracker = createExecCommandTracker();
29
- const state: AdapterState = { enabled: false, promptSkills: [] };
38
+ const state: AdapterState = { enabled: false, promptSkills: [], pendingWebSearchCount: 0 };
30
39
  const sessions = createExecSessionManager();
31
40
 
32
41
  registerApplyPatchTool(pi);
33
42
  registerExecCommandTool(pi, tracker, sessions);
34
43
  registerWriteStdinTool(pi, sessions);
44
+ registerWebSearchTool(pi);
45
+ registerWebSearchMessageRenderer(pi);
35
46
 
36
47
  sessions.onSessionExit((_sessionId, command) => {
37
48
  tracker.recordCommandFinished(command);
@@ -72,6 +83,41 @@ export default function codexConversion(pi: ExtensionAPI) {
72
83
  }),
73
84
  };
74
85
  });
86
+
87
+ pi.on("context", async (event) => {
88
+ return {
89
+ messages: event.messages.filter(
90
+ (message) => !(message.role === "custom" && message.customType === WEB_SEARCH_ACTIVITY_MESSAGE_TYPE),
91
+ ),
92
+ };
93
+ });
94
+
95
+ pi.on("turn_start", async () => {
96
+ state.pendingWebSearchCount = 0;
97
+ });
98
+
99
+ pi.on("before_provider_request", async (event, ctx) => {
100
+ if (!isOpenAICodexContext(ctx)) {
101
+ return undefined;
102
+ }
103
+ if (payloadContainsWebSearchTool(event.payload)) {
104
+ state.pendingWebSearchCount += 1;
105
+ }
106
+ return rewriteNativeWebSearchTool(event.payload, ctx.model);
107
+ });
108
+
109
+ pi.on("agent_end", async () => {
110
+ if (state.pendingWebSearchCount <= 0) {
111
+ return;
112
+ }
113
+ pi.sendMessage({
114
+ customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
115
+ content: "",
116
+ display: true,
117
+ details: { count: state.pendingWebSearchCount },
118
+ });
119
+ state.pendingWebSearchCount = 0;
120
+ });
75
121
  }
76
122
 
77
123
  function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
@@ -116,10 +162,14 @@ function setStatus(ctx: ExtensionContext, enabled: boolean): void {
116
162
  }
117
163
 
118
164
  function getAdapterToolNames(ctx: ExtensionContext): string[] {
165
+ const toolNames = [...CORE_ADAPTER_TOOL_NAMES];
119
166
  if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
120
- return [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
167
+ toolNames.push(VIEW_IMAGE_TOOL_NAME);
168
+ }
169
+ if (supportsNativeWebSearch(ctx.model)) {
170
+ toolNames.push(WEB_SEARCH_TOOL_NAME);
121
171
  }
122
- return [...CORE_ADAPTER_TOOL_NAMES];
172
+ return toolNames;
123
173
  }
124
174
 
125
175
  export function mergeAdapterTools(activeTools: string[], adapterTools: string[]): string[] {
@@ -32,6 +32,14 @@ export function renderWriteStdinCall(
32
32
  return text;
33
33
  }
34
34
 
35
+ export function renderWebSearchActivity(count: number, theme: RenderTheme, expanded = false): string {
36
+ let text = `${theme.fg("dim", "•")} ${theme.bold("Searched the web")}`;
37
+ if (expanded && count > 1) {
38
+ text += `\n${theme.fg("dim", " └ ")}${theme.fg("muted", `${count} web searches in this turn`)}`;
39
+ }
40
+ return text;
41
+ }
42
+
35
43
  function renderExplorationText(actions: ShellAction[], state: ExecCommandStatus, theme: RenderTheme): string {
36
44
  const header = state === "running" ? "Exploring" : "Explored";
37
45
  let text = `${theme.fg("dim", "•")} ${theme.bold(header)}`;
@@ -0,0 +1,123 @@
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Box, Text } from "@mariozechner/pi-tui";
4
+ import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
+ import { renderWebSearchActivity } from "./codex-rendering.ts";
6
+
7
+ export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
8
+ const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
9
+ "web_search is a native openai-codex provider tool and should not execute locally";
10
+ export const WEB_SEARCH_ACTIVITY_MESSAGE_TYPE = "codex-web-search";
11
+
12
+ const WEB_SEARCH_PARAMETERS = Type.Object({}, { additionalProperties: false });
13
+
14
+ interface FunctionToolPayload {
15
+ type?: unknown;
16
+ name?: unknown;
17
+ }
18
+
19
+ interface ResponsesPayload {
20
+ tools?: unknown[];
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface WebSearchActivityDetails {
25
+ count: number;
26
+ }
27
+
28
+ export function supportsNativeWebSearch(model: ExtensionContext["model"]): boolean {
29
+ return isOpenAICodexModel(model);
30
+ }
31
+
32
+ function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
33
+ return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
34
+ }
35
+
36
+ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionContext["model"]): unknown {
37
+ if (!supportsNativeWebSearch(model) || !payload || typeof payload !== "object") {
38
+ return payload;
39
+ }
40
+
41
+ const tools = (payload as ResponsesPayload).tools;
42
+ if (!Array.isArray(tools)) {
43
+ return payload;
44
+ }
45
+
46
+ let rewritten = false;
47
+ const nextTools = tools.map((tool) => {
48
+ if (!isWebSearchFunctionTool(tool)) {
49
+ return tool;
50
+ }
51
+ rewritten = true;
52
+ // Match Codex's native tool shape rather than exposing a synthetic function tool.
53
+ return {
54
+ type: "web_search",
55
+ external_web_access: true,
56
+ };
57
+ });
58
+
59
+ if (!rewritten) {
60
+ return payload;
61
+ }
62
+
63
+ return {
64
+ ...(payload as ResponsesPayload),
65
+ tools: nextTools,
66
+ };
67
+ }
68
+
69
+ export function payloadContainsWebSearchTool(payload: unknown): boolean {
70
+ if (!payload || typeof payload !== "object") {
71
+ return false;
72
+ }
73
+ const tools = (payload as ResponsesPayload).tools;
74
+ if (!Array.isArray(tools)) {
75
+ return false;
76
+ }
77
+ return tools.some(
78
+ (tool) =>
79
+ !!tool &&
80
+ typeof tool === "object" &&
81
+ (("type" in tool && (tool as { type?: unknown }).type === "web_search") || isWebSearchFunctionTool(tool)),
82
+ );
83
+ }
84
+
85
+ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
86
+ return {
87
+ name: "web_search",
88
+ label: "web_search",
89
+ description: "Search the internet for sources related to the prompt.",
90
+ promptSnippet: "Search the internet for sources related to the prompt.",
91
+ parameters: WEB_SEARCH_PARAMETERS,
92
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
93
+ if (!supportsNativeWebSearch(ctx.model)) {
94
+ throw new Error(WEB_SEARCH_UNSUPPORTED_MESSAGE);
95
+ }
96
+ throw new Error(WEB_SEARCH_LOCAL_EXECUTION_MESSAGE);
97
+ },
98
+ renderCall(_args, theme) {
99
+ return new Text(`${theme.fg("toolTitle", theme.bold("web_search"))}`, 0, 0);
100
+ },
101
+ renderResult(result, { expanded }, theme) {
102
+ if (!expanded) {
103
+ return undefined;
104
+ }
105
+ const textBlock = result.content.find((item) => item.type === "text");
106
+ const text = textBlock?.type === "text" ? textBlock.text : "(no output)";
107
+ return new Text(theme.fg("dim", text), 0, 0);
108
+ },
109
+ };
110
+ }
111
+
112
+ export function registerWebSearchTool(pi: ExtensionAPI): void {
113
+ pi.registerTool(createWebSearchTool());
114
+ }
115
+
116
+ export function registerWebSearchMessageRenderer(pi: ExtensionAPI): void {
117
+ pi.registerMessageRenderer<WebSearchActivityDetails>(WEB_SEARCH_ACTIVITY_MESSAGE_TYPE, (message, { expanded }, theme) => {
118
+ const count = typeof message.details?.count === "number" ? message.details.count : 1;
119
+ const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
120
+ box.addChild(new Text(renderWebSearchActivity(count, theme, expanded), 0, 0));
121
+ return box;
122
+ });
123
+ }