@howaboua/pi-codex-conversion 1.0.3 → 1.0.5
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 +8 -1
- package/package.json +1 -1
- package/src/adapter/codex-model.ts +9 -0
- package/src/adapter/tool-set.ts +1 -0
- package/src/index.ts +51 -6
- package/src/tools/web-search-tool.ts +112 -0
package/README.md
CHANGED
|
@@ -4,12 +4,15 @@ 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 `
|
|
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
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
+
> [!NOTE]
|
|
14
|
+
> Native OpenAI Codex Responses web search runs silently. Pi does not expose native web-search usage events to extensions, so the adapter shows a one-time session notice instead of per-search tool-call history.
|
|
15
|
+
|
|
13
16
|
## Active tools in adapter mode
|
|
14
17
|
|
|
15
18
|
When the adapter is active, the LLM sees these tools:
|
|
@@ -18,6 +21,7 @@ When the adapter is active, the LLM sees these tools:
|
|
|
18
21
|
- `write_stdin` — continue or poll a running exec session
|
|
19
22
|
- `apply_patch` — patch tool
|
|
20
23
|
- `view_image` — image-only wrapper around Pi's native image reading, enabled only for image-capable models
|
|
24
|
+
- `web_search` — native OpenAI Codex Responses web search, enabled only on the `openai-codex` provider
|
|
21
25
|
|
|
22
26
|
Notably:
|
|
23
27
|
|
|
@@ -53,6 +57,8 @@ npm run check
|
|
|
53
57
|
- `write_stdin({ session_id, chars: "" })` renders like `Waited for background terminal`
|
|
54
58
|
- `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
|
|
55
59
|
- `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
|
|
60
|
+
- `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
|
|
61
|
+
- when native web search is available, the adapter shows a one-time session notice; individual searches are not surfaced because Pi does not expose native web-search execution events to extensions
|
|
56
62
|
|
|
57
63
|
Raw command output is still available by expanding the tool result.
|
|
58
64
|
|
|
@@ -90,6 +96,7 @@ That keeps the prompt much closer to `pi-mono` while still steering the model to
|
|
|
90
96
|
- Adapter mode activates automatically for OpenAI `gpt*` and `codex*` models.
|
|
91
97
|
- When you switch away from those models, Pi restores the previous active tool set.
|
|
92
98
|
- `view_image` resolves paths against the active session cwd and only exposes `detail: "original"` for Codex-family image-capable models.
|
|
99
|
+
- `web_search` is exposed only for the `openai-codex` provider and is forwarded as the native OpenAI Codex Responses web search tool.
|
|
93
100
|
- `apply_patch` paths stay restricted to the current working directory.
|
|
94
101
|
- `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
|
|
95
102
|
- 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
|
@@ -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
|
+
}
|
package/src/adapter/tool-set.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
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
|
+
registerWebSearchTool,
|
|
12
|
+
registerWebSearchSessionNoteRenderer,
|
|
13
|
+
rewriteNativeWebSearchTool,
|
|
14
|
+
shouldShowWebSearchSessionNote,
|
|
15
|
+
supportsNativeWebSearch,
|
|
16
|
+
WEB_SEARCH_SESSION_NOTE_TEXT,
|
|
17
|
+
WEB_SEARCH_SESSION_NOTE_TYPE,
|
|
18
|
+
} from "./tools/web-search-tool.ts";
|
|
10
19
|
import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
|
|
11
20
|
|
|
12
21
|
interface AdapterState {
|
|
13
22
|
enabled: boolean;
|
|
14
23
|
previousToolNames?: string[];
|
|
15
24
|
promptSkills: PromptSkill[];
|
|
25
|
+
webSearchNoticeShown: boolean;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
|
|
28
|
+
const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME];
|
|
19
29
|
|
|
20
30
|
function getCommandArg(args: unknown): string | undefined {
|
|
21
31
|
if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
|
|
@@ -26,18 +36,21 @@ function getCommandArg(args: unknown): string | undefined {
|
|
|
26
36
|
|
|
27
37
|
export default function codexConversion(pi: ExtensionAPI) {
|
|
28
38
|
const tracker = createExecCommandTracker();
|
|
29
|
-
const state: AdapterState = { enabled: false, promptSkills: [] };
|
|
39
|
+
const state: AdapterState = { enabled: false, promptSkills: [], webSearchNoticeShown: false };
|
|
30
40
|
const sessions = createExecSessionManager();
|
|
31
41
|
|
|
32
42
|
registerApplyPatchTool(pi);
|
|
33
43
|
registerExecCommandTool(pi, tracker, sessions);
|
|
34
44
|
registerWriteStdinTool(pi, sessions);
|
|
45
|
+
registerWebSearchTool(pi);
|
|
46
|
+
registerWebSearchSessionNoteRenderer(pi);
|
|
35
47
|
|
|
36
48
|
sessions.onSessionExit((_sessionId, command) => {
|
|
37
49
|
tracker.recordCommandFinished(command);
|
|
38
50
|
});
|
|
39
51
|
|
|
40
52
|
pi.on("session_start", async (_event, ctx) => {
|
|
53
|
+
state.webSearchNoticeShown = false;
|
|
41
54
|
syncAdapter(pi, ctx, state);
|
|
42
55
|
});
|
|
43
56
|
|
|
@@ -72,12 +85,28 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
72
85
|
}),
|
|
73
86
|
};
|
|
74
87
|
});
|
|
88
|
+
|
|
89
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
90
|
+
if (!isOpenAICodexContext(ctx)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return rewriteNativeWebSearchTool(event.payload, ctx.model);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.on("context", async (event) => {
|
|
97
|
+
return {
|
|
98
|
+
messages: event.messages.filter(
|
|
99
|
+
(message) => !(message.role === "custom" && message.customType === WEB_SEARCH_SESSION_NOTE_TYPE),
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
78
106
|
state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
|
|
79
107
|
|
|
80
108
|
registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
|
|
109
|
+
maybeShowWebSearchSessionNote(pi, ctx, state);
|
|
81
110
|
|
|
82
111
|
if (isCodexLikeContext(ctx)) {
|
|
83
112
|
enableAdapter(pi, ctx, state);
|
|
@@ -116,10 +145,14 @@ function setStatus(ctx: ExtensionContext, enabled: boolean): void {
|
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
function getAdapterToolNames(ctx: ExtensionContext): string[] {
|
|
148
|
+
const toolNames = [...CORE_ADAPTER_TOOL_NAMES];
|
|
119
149
|
if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
|
|
120
|
-
|
|
150
|
+
toolNames.push(VIEW_IMAGE_TOOL_NAME);
|
|
151
|
+
}
|
|
152
|
+
if (supportsNativeWebSearch(ctx.model)) {
|
|
153
|
+
toolNames.push(WEB_SEARCH_TOOL_NAME);
|
|
121
154
|
}
|
|
122
|
-
return
|
|
155
|
+
return toolNames;
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
export function mergeAdapterTools(activeTools: string[], adapterTools: string[]): string[] {
|
|
@@ -140,3 +173,15 @@ export function restoreTools(previousTools: string[], activeTools: string[]): st
|
|
|
140
173
|
function hasAdapterTools(activeTools: string[]): boolean {
|
|
141
174
|
return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
|
|
142
175
|
}
|
|
176
|
+
|
|
177
|
+
function maybeShowWebSearchSessionNote(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
178
|
+
if (!shouldShowWebSearchSessionNote(ctx.model, ctx.hasUI, state.webSearchNoticeShown)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pi.sendMessage({
|
|
182
|
+
customType: WEB_SEARCH_SESSION_NOTE_TYPE,
|
|
183
|
+
content: WEB_SEARCH_SESSION_NOTE_TEXT,
|
|
184
|
+
display: true,
|
|
185
|
+
});
|
|
186
|
+
state.webSearchNoticeShown = true;
|
|
187
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
|
|
6
|
+
export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
|
|
7
|
+
const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
|
|
8
|
+
"web_search is a native openai-codex provider tool and should not execute locally";
|
|
9
|
+
export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
|
|
10
|
+
export const WEB_SEARCH_SESSION_NOTE_TEXT =
|
|
11
|
+
"Native OpenAI Codex web search is enabled for this session. Search runs silently and is not surfaced as a separate tool call.";
|
|
12
|
+
|
|
13
|
+
const WEB_SEARCH_PARAMETERS = Type.Object({}, { additionalProperties: false });
|
|
14
|
+
|
|
15
|
+
interface FunctionToolPayload {
|
|
16
|
+
type?: unknown;
|
|
17
|
+
name?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResponsesPayload {
|
|
21
|
+
tools?: unknown[];
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function supportsNativeWebSearch(model: ExtensionContext["model"]): boolean {
|
|
26
|
+
return isOpenAICodexModel(model);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldShowWebSearchSessionNote(
|
|
30
|
+
model: ExtensionContext["model"],
|
|
31
|
+
hasUI: boolean,
|
|
32
|
+
alreadyShown: boolean,
|
|
33
|
+
): boolean {
|
|
34
|
+
return hasUI && !alreadyShown && supportsNativeWebSearch(model);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
|
|
38
|
+
return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionContext["model"]): unknown {
|
|
42
|
+
if (!supportsNativeWebSearch(model) || !payload || typeof payload !== "object") {
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tools = (payload as ResponsesPayload).tools;
|
|
47
|
+
if (!Array.isArray(tools)) {
|
|
48
|
+
return payload;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let rewritten = false;
|
|
52
|
+
const nextTools = tools.map((tool) => {
|
|
53
|
+
if (!isWebSearchFunctionTool(tool)) {
|
|
54
|
+
return tool;
|
|
55
|
+
}
|
|
56
|
+
rewritten = true;
|
|
57
|
+
// Match Codex's native tool shape rather than exposing a synthetic function tool.
|
|
58
|
+
return {
|
|
59
|
+
type: "web_search",
|
|
60
|
+
external_web_access: true,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!rewritten) {
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...(payload as ResponsesPayload),
|
|
70
|
+
tools: nextTools,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
|
|
75
|
+
return {
|
|
76
|
+
name: "web_search",
|
|
77
|
+
label: "web_search",
|
|
78
|
+
description: "Search the internet for sources related to the prompt.",
|
|
79
|
+
promptSnippet: "Search the internet for sources related to the prompt.",
|
|
80
|
+
parameters: WEB_SEARCH_PARAMETERS,
|
|
81
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
82
|
+
if (!supportsNativeWebSearch(ctx.model)) {
|
|
83
|
+
throw new Error(WEB_SEARCH_UNSUPPORTED_MESSAGE);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(WEB_SEARCH_LOCAL_EXECUTION_MESSAGE);
|
|
86
|
+
},
|
|
87
|
+
renderCall(_args, theme) {
|
|
88
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("web_search"))}`, 0, 0);
|
|
89
|
+
},
|
|
90
|
+
renderResult(result, { expanded }, theme) {
|
|
91
|
+
if (!expanded) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const textBlock = result.content.find((item) => item.type === "text");
|
|
95
|
+
const text = textBlock?.type === "text" ? textBlock.text : "(no output)";
|
|
96
|
+
return new Text(theme.fg("dim", text), 0, 0);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function registerWebSearchTool(pi: ExtensionAPI): void {
|
|
102
|
+
pi.registerTool(createWebSearchTool());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function registerWebSearchSessionNoteRenderer(pi: ExtensionAPI): void {
|
|
106
|
+
pi.registerMessageRenderer(WEB_SEARCH_SESSION_NOTE_TYPE, (_message, _options, theme) => {
|
|
107
|
+
const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
|
|
108
|
+
box.addChild(new Text(theme.bold("Web search enabled"), 0, 0));
|
|
109
|
+
box.addChild(new Text(`\n${theme.fg("dim", WEB_SEARCH_SESSION_NOTE_TEXT)}`, 0, 0));
|
|
110
|
+
return box;
|
|
111
|
+
});
|
|
112
|
+
}
|