@oai404iao/pi-codex-runtime 0.1.0 → 0.3.0
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 +9 -2
- package/config.schema.json +2 -101
- package/package.json +6 -6
- package/src/broker.ts +3 -0
- package/src/code-mode-contributions.ts +63 -0
- package/src/code-mode-owner.ts +45 -0
- package/src/model-catalog/runtime.ts +6 -2
- package/src/providers/responses/grammar.ts +52 -0
- package/src/providers/responses/messages.ts +10 -3
- package/src/providers/responses/sampling.ts +41 -0
- package/src/providers/responses/stream.ts +14 -11
- package/src/providers/responses/text.ts +4 -2
- package/src/providers/responses/tools.ts +12 -7
- package/src/providers/responses/types.ts +5 -0
- package/src/settings.ts +26 -16
- package/src/tool-activation.ts +33 -4
package/README.md
CHANGED
|
@@ -4,18 +4,25 @@ Alpha bootstrap candidate; initial npm publication is still pending.
|
|
|
4
4
|
It has no Pi extension entry
|
|
5
5
|
and does not automatically register tools or providers.
|
|
6
6
|
|
|
7
|
-
Peer floor: Pi 0.
|
|
7
|
+
Peer floor: Pi 0.86.1; tested against 0.86.1.
|
|
8
8
|
|
|
9
9
|
Owns shared Codex authentication/headers, wire identity, settings/catalog,
|
|
10
10
|
Responses replay contracts and the session-scoped composition broker.
|
|
11
11
|
Capability-specific HTTP clients, storage and transports live in other packages.
|
|
12
12
|
|
|
13
|
+
Responses helpers encode/decode generic grammar tools and replay calls/results
|
|
14
|
+
according to the current declaration, preserving legacy custom patch behavior.
|
|
15
|
+
An internal structural event adapter lets owners offer optional Code Mode
|
|
16
|
+
contributions without importing or depending on the private Code Mode package.
|
|
17
|
+
|
|
13
18
|
Configuration paths remain
|
|
14
19
|
`<agentDir>/extensions/pi-codex-minimal-tools/{config,models}.json`.
|
|
15
20
|
This package ships the canonical schemas and default catalog, including the
|
|
16
21
|
`openai-codex/gpt-6-astra` Responses Lite profile. The global
|
|
17
22
|
`config.json.imageGeneration:false` setting gates image capability derivation
|
|
18
|
-
without replacing the selected model profile.
|
|
23
|
+
without replacing the selected model profile. Likewise,
|
|
24
|
+
`config.json.webSocketEnabled:false` forces SSE and disables WebSocket prewarm
|
|
25
|
+
without changing that profile.
|
|
19
26
|
`./subagent-inline` supplies the existing SDK identity integration.
|
|
20
27
|
`./internal/*` is implementation wiring, not a stable public API.
|
|
21
28
|
|
package/config.schema.json
CHANGED
|
@@ -27,23 +27,9 @@
|
|
|
27
27
|
"description": "Automatically activate enabled package tools on supported models.",
|
|
28
28
|
"default": true
|
|
29
29
|
},
|
|
30
|
-
"
|
|
30
|
+
"webSocketEnabled": {
|
|
31
31
|
"type": "boolean",
|
|
32
|
-
"description": "
|
|
33
|
-
"deprecated": true,
|
|
34
|
-
"default": true
|
|
35
|
-
},
|
|
36
|
-
"openaiTransport": {
|
|
37
|
-
"type": "string",
|
|
38
|
-
"description": "Deprecated compatibility override. Move per-model Responses transport to extension models.json.",
|
|
39
|
-
"deprecated": true,
|
|
40
|
-
"enum": ["sse", "websocket", "websocket-cached", "auto"],
|
|
41
|
-
"default": "sse"
|
|
42
|
-
},
|
|
43
|
-
"openaiWebSocketPrewarm": {
|
|
44
|
-
"type": "boolean",
|
|
45
|
-
"description": "Deprecated compatibility override. Move per-model one-shot session-startup WebSocket prewarm behavior to extension models.json.",
|
|
46
|
-
"deprecated": true,
|
|
32
|
+
"description": "Global Responses WebSocket master switch. false forces SSE and disables WebSocket prewarm for every model profile.",
|
|
47
33
|
"default": true
|
|
48
34
|
},
|
|
49
35
|
"fastMode": {
|
|
@@ -51,73 +37,11 @@
|
|
|
51
37
|
"description": "Global user toggle for the Fast service tier declared by the active model profile.",
|
|
52
38
|
"default": false
|
|
53
39
|
},
|
|
54
|
-
"compactionMode": {
|
|
55
|
-
"type": "string",
|
|
56
|
-
"description": "Deprecated compatibility override. Move per-model compaction behavior to extension models.json.",
|
|
57
|
-
"deprecated": true,
|
|
58
|
-
"enum": ["pi", "responses", "responses-compact"],
|
|
59
|
-
"default": "pi"
|
|
60
|
-
},
|
|
61
|
-
"requestProfile": {
|
|
62
|
-
"type": "object",
|
|
63
|
-
"description": "Deprecated compatibility override. Move Responses mode, reasoning summary, prompt placement, patch transport, and tool capabilities to extension models.json.",
|
|
64
|
-
"deprecated": true,
|
|
65
|
-
"default": {},
|
|
66
|
-
"additionalProperties": false,
|
|
67
|
-
"properties": {
|
|
68
|
-
"responsesMode": {
|
|
69
|
-
"type": "string",
|
|
70
|
-
"description": "Responses protocol mode. Lite requires an endpoint that implements the Codex Responses Lite contract.",
|
|
71
|
-
"enum": ["standard", "lite"],
|
|
72
|
-
"default": "standard"
|
|
73
|
-
},
|
|
74
|
-
"reasoningSummary": {
|
|
75
|
-
"type": "string",
|
|
76
|
-
"description": "Deprecated compatibility override for the reasoning summary mode. Move this to responses.reasoningSummary in the per-model catalog.",
|
|
77
|
-
"enum": ["auto", "concise", "detailed", "none"],
|
|
78
|
-
"default": "auto"
|
|
79
|
-
},
|
|
80
|
-
"systemPromptPlacement": {
|
|
81
|
-
"type": "string",
|
|
82
|
-
"description": "Where Standard Responses places Pi's system prompt. Responses Lite always uses a developer message.",
|
|
83
|
-
"enum": ["instructions", "developer"],
|
|
84
|
-
"default": "instructions"
|
|
85
|
-
},
|
|
86
|
-
"patchTransport": {
|
|
87
|
-
"type": "string",
|
|
88
|
-
"description": "Transport for apply_patch: a JSON function tool or the Codex freeform custom tool.",
|
|
89
|
-
"enum": ["function", "custom"],
|
|
90
|
-
"default": "function"
|
|
91
|
-
},
|
|
92
|
-
"supportsHostedTools": {
|
|
93
|
-
"type": "boolean",
|
|
94
|
-
"description": "Enable hosted-tool activation and request rewriting. Responses Lite always disables hosted tools.",
|
|
95
|
-
"default": true
|
|
96
|
-
},
|
|
97
|
-
"supportsParallelTools": {
|
|
98
|
-
"type": "boolean",
|
|
99
|
-
"description": "Enable parallel tool calls in provider requests. Responses Lite always disables parallel tools.",
|
|
100
|
-
"default": true
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
"apiKeyMode": {
|
|
105
|
-
"type": "boolean",
|
|
106
|
-
"description": "Deprecated compatibility override. Move the endpoint/auth shape to responses.endpoint in extension models.json.",
|
|
107
|
-
"deprecated": true,
|
|
108
|
-
"default": false
|
|
109
|
-
},
|
|
110
40
|
"imageGeneration": {
|
|
111
41
|
"type": "boolean",
|
|
112
42
|
"description": "Global image-generation master switch. false disables image tools, commands, presentation, hosted injection, standalone endpoints, and direct fallback without changing other model-profile behavior.",
|
|
113
43
|
"default": true
|
|
114
44
|
},
|
|
115
|
-
"webSearchEnabled": {
|
|
116
|
-
"type": "boolean",
|
|
117
|
-
"description": "Deprecated compatibility override. Move per-model web-search support to extension models.json.",
|
|
118
|
-
"deprecated": true,
|
|
119
|
-
"default": false
|
|
120
|
-
},
|
|
121
45
|
"imageOutputDir": {
|
|
122
46
|
"type": "string",
|
|
123
47
|
"description": "Directory for generated images. Relative paths resolve against the workspace root.",
|
|
@@ -136,34 +60,11 @@
|
|
|
136
60
|
"description": "Allow direct OpenAI Images API generation when native Codex generation is unavailable.",
|
|
137
61
|
"default": false
|
|
138
62
|
},
|
|
139
|
-
"viewImage": {
|
|
140
|
-
"type": "boolean",
|
|
141
|
-
"description": "Deprecated compatibility override. Move per-model view_image support to extension models.json.",
|
|
142
|
-
"deprecated": true,
|
|
143
|
-
"default": false
|
|
144
|
-
},
|
|
145
63
|
"viewImageWorkspaceOnly": {
|
|
146
64
|
"type": "boolean",
|
|
147
65
|
"description": "Reject view_image paths outside the active workspace.",
|
|
148
66
|
"default": false
|
|
149
67
|
},
|
|
150
|
-
"applyPatchEnabled": {
|
|
151
|
-
"type": "boolean",
|
|
152
|
-
"description": "Deprecated compatibility override. Move per-model apply_patch support to extension models.json.",
|
|
153
|
-
"deprecated": true,
|
|
154
|
-
"default": true
|
|
155
|
-
},
|
|
156
|
-
"additionalModelIds": {
|
|
157
|
-
"type": "array",
|
|
158
|
-
"description": "Deprecated compatibility allowlist. Add exact per-model entries to extension models.json instead.",
|
|
159
|
-
"deprecated": true,
|
|
160
|
-
"items": {
|
|
161
|
-
"type": "string",
|
|
162
|
-
"pattern": "^[^\\s/]+/\\S+$"
|
|
163
|
-
},
|
|
164
|
-
"uniqueItems": true,
|
|
165
|
-
"default": []
|
|
166
|
-
},
|
|
167
68
|
"deferApplyPatchRendering": {
|
|
168
69
|
"type": "boolean",
|
|
169
70
|
"description": "Use Pi's fallback tool renderer instead of the built-in streaming filesystem preview.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oai404iao/pi-codex-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Unpublished pi-codex-runtime workspace for the staged Codex split",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"./subagent-inline": "./src/subagent-inline.ts"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@earendil-works/pi-ai": ">=0.
|
|
27
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
26
|
+
"@earendil-works/pi-ai": ">=0.86.1",
|
|
27
|
+
"@earendil-works/pi-coding-agent": ">=0.86.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"@earendil-works/pi-ai": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@earendil-works/pi-ai": "0.
|
|
40
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
39
|
+
"@earendil-works/pi-ai": "0.86.1",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
41
41
|
"@types/node": "^26.2.0",
|
|
42
42
|
"tsx": "^4.20.6",
|
|
43
43
|
"typescript": "^7.0.2"
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22.19.0"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "b02484cecad9081886797bf9b4438135aaae1e07"
|
|
68
68
|
}
|
package/src/broker.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import type { PackageToolName } from "./capabilities.js";
|
|
4
4
|
import type { ProviderPresentation } from "./extension/provider-presentation.js";
|
|
5
|
+
import type { OwnerState } from "./code-mode-owner.js";
|
|
5
6
|
|
|
6
7
|
export const CODEX_BROKER_CHANNEL = "@oai404iao/pi-codex:broker";
|
|
7
8
|
export const CODEX_RUNTIME_VERSION: string = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -10,6 +11,7 @@ const CACHE = Symbol.for("@oai404iao/pi-codex/broker/v1");
|
|
|
10
11
|
export interface InstalledTool {
|
|
11
12
|
register(): void;
|
|
12
13
|
registered: boolean;
|
|
14
|
+
codeModeOwner?: OwnerState;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface CodexBroker {
|
|
@@ -18,6 +20,7 @@ export interface CodexBroker {
|
|
|
18
20
|
readonly closed: boolean;
|
|
19
21
|
readonly tools: Map<PackageToolName, InstalledTool>;
|
|
20
22
|
coreEnabled: boolean;
|
|
23
|
+
codeModeDefinitions?: Map<string, { parameters: unknown; description: string }>;
|
|
21
24
|
claim(name: string): boolean;
|
|
22
25
|
addPresentation(name: string, presentation: ProviderPresentation): void;
|
|
23
26
|
presentation: ProviderPresentation;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getCodexBroker } from "./broker.js";
|
|
3
|
+
import { codeModeOwner, type DirectBinding } from "./code-mode-owner.js";
|
|
4
|
+
import type { PackageToolName } from "./capabilities.js";
|
|
5
|
+
|
|
6
|
+
/** Unique public schema reference proves which registration won Pi's registry.
|
|
7
|
+
* If a future Pi clones metadata, cooperation fails closed instead of claiming
|
|
8
|
+
* a name/source belonging to another extension. */
|
|
9
|
+
export function registerCodeModeOwnedTool(pi: ExtensionAPI, definition: Record<string, unknown>): void {
|
|
10
|
+
if (typeof definition.name !== "string" || typeof definition.description !== "string"
|
|
11
|
+
|| !definition.parameters || typeof definition.parameters !== "object") throw new Error("Invalid owned Code Mode tool definition");
|
|
12
|
+
const broker = getCodexBroker(pi);
|
|
13
|
+
const owned = { ...definition, name: definition.name, description: definition.description, parameters: { ...definition.parameters } };
|
|
14
|
+
(broker.codeModeDefinitions ??= new Map()).set(owned.name, owned);
|
|
15
|
+
pi.registerTool(owned as never);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NestedCodeModeContext {
|
|
19
|
+
readonly cellId: string;
|
|
20
|
+
readonly toolCallId: string;
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly signal: AbortSignal;
|
|
23
|
+
readonly pi?: ExtensionContext;
|
|
24
|
+
}
|
|
25
|
+
export interface OwnedCodeModeTool {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
parameters: unknown;
|
|
29
|
+
effect: "read" | "write";
|
|
30
|
+
parallel?: boolean;
|
|
31
|
+
direct?: DirectBinding;
|
|
32
|
+
invoke(input: unknown, context: NestedCodeModeContext): Promise<{ value: unknown }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Structural client for the optional v1 bus contract. Neither side installs
|
|
36
|
+
* the other package; all authority still comes from Code Mode's exact grants. */
|
|
37
|
+
export function registerCodeModeContribution(pi: ExtensionAPI, id: string,
|
|
38
|
+
tools: (context: ExtensionContext) => readonly OwnedCodeModeTool[]): void {
|
|
39
|
+
let context: ExtensionContext | undefined;
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const changed = () => pi.events.emit("@oai404iao/pi-code-mode:changed/v1", { version: 1 });
|
|
42
|
+
const off = pi.events.on("@oai404iao/pi-code-mode:discover/v1", (value) => {
|
|
43
|
+
const discovery = value as { version?: number; provider?: (provider: unknown) => void } | undefined;
|
|
44
|
+
if (!disposed && context && discovery?.version === 1 && typeof discovery.provider === "function") {
|
|
45
|
+
const broker = getCodexBroker(pi);
|
|
46
|
+
discovery.provider({ id, tools: tools(context).map((tool) => {
|
|
47
|
+
const owned = broker.tools.get(tool.name as PackageToolName);
|
|
48
|
+
return { ...tool, direct: owned ? codeModeOwner(pi, tool.name, owned, broker.codeModeDefinitions?.get(tool.name))?.binding : undefined };
|
|
49
|
+
}) });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const update = (_event: unknown, ctx: ExtensionContext) => {
|
|
53
|
+
if (disposed) return;
|
|
54
|
+
context = ctx;
|
|
55
|
+
changed();
|
|
56
|
+
};
|
|
57
|
+
pi.on("session_start", update);
|
|
58
|
+
pi.on("model_select", update);
|
|
59
|
+
pi.on("session_shutdown", () => {
|
|
60
|
+
if (disposed) return;
|
|
61
|
+
disposed = true; context = undefined; off(); changed();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface DirectBinding {
|
|
4
|
+
readonly version: 1; readonly name: string;
|
|
5
|
+
acquire(): { reconcile(): boolean; release(): void } | undefined;
|
|
6
|
+
}
|
|
7
|
+
interface Control {
|
|
8
|
+
binding: DirectBinding; readonly activeIntent: boolean | undefined;
|
|
9
|
+
projectActive(active: boolean): boolean | undefined;
|
|
10
|
+
reconcile(): boolean; dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
interface Factory {
|
|
13
|
+
version: 1; create(pi: ExtensionAPI, options: { name: string; sourcePath: string }): Control;
|
|
14
|
+
}
|
|
15
|
+
export interface OwnerState { identity: string; replaced?: boolean; factory?: Factory; control?: Control }
|
|
16
|
+
export const OWNER_CHANGED = "@oai404iao/pi-code-mode:direct-owner-changed/v1";
|
|
17
|
+
/** Optional v1 bus client. No private-package dependency or global registry. */
|
|
18
|
+
export function codeModeOwner(pi: ExtensionAPI, name: string, tool: { registered: boolean; codeModeOwner?: OwnerState },
|
|
19
|
+
expected?: { parameters: unknown; description: string }): Control | undefined {
|
|
20
|
+
if (!["apply_patch", "web_search"].includes(name) || !tool.registered) return;
|
|
21
|
+
const info = pi.getAllTools?.().find((item) => item.name === name);
|
|
22
|
+
if (!info?.sourceInfo || ["builtin", "sdk"].includes(info.sourceInfo.source)) return tool.codeModeOwner?.control;
|
|
23
|
+
if (!expected || info.parameters !== expected.parameters || info.description !== expected.description) {
|
|
24
|
+
if (expected) (tool.codeModeOwner ??= { identity: "" }).replaced = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const identity = JSON.stringify([info.sourceInfo, info.description, info.parameters, info.promptGuidelines]);
|
|
28
|
+
const state = tool.codeModeOwner ??= { identity };
|
|
29
|
+
// Never rebind a replaced registration, even if the optional factory reloads.
|
|
30
|
+
if (state.identity !== identity) { state.replaced = true; return state.control; }
|
|
31
|
+
if (state.replaced) return state.control;
|
|
32
|
+
const factories: Factory[] = [];
|
|
33
|
+
pi.events.emit("@oai404iao/pi-code-mode:direct-owner/v1", { version: 1, accept(value: Factory) {
|
|
34
|
+
if (factories.length < 2) factories.push(value);
|
|
35
|
+
} });
|
|
36
|
+
const factory = factories.length === 1 && factories[0]?.version === 1
|
|
37
|
+
&& typeof factories[0].create === "function" ? factories[0] : undefined;
|
|
38
|
+
if (factory !== state.factory) {
|
|
39
|
+
const old = state.control;
|
|
40
|
+
state.control = undefined; state.factory = factory;
|
|
41
|
+
old?.dispose();
|
|
42
|
+
if (factory) state.control = factory.create(pi, { name, sourcePath: info.sourceInfo.path });
|
|
43
|
+
}
|
|
44
|
+
return state.control;
|
|
45
|
+
}
|
|
@@ -96,8 +96,12 @@ export function loadModelSettings(
|
|
|
96
96
|
...baseSettings,
|
|
97
97
|
providerShimActive,
|
|
98
98
|
nativeProviderTools: providerShimActive && usesProviderToolRewrite,
|
|
99
|
-
openaiTransport:
|
|
100
|
-
|
|
99
|
+
openaiTransport: baseSettings.webSocketEnabled
|
|
100
|
+
? effective.responses.transport
|
|
101
|
+
: "sse",
|
|
102
|
+
openaiWebSocketPrewarm:
|
|
103
|
+
baseSettings.webSocketEnabled
|
|
104
|
+
&& effective.responses.websocketPrewarm,
|
|
101
105
|
compactionMode: providerShimActive ? effective.compaction : "pi",
|
|
102
106
|
requestProfile,
|
|
103
107
|
apiKeyMode: endpointUsesApiKey(effective.responses.endpoint, model),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Api, AssistantMessage, AssistantMessageEventStream, Model, Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
import { grammarInputDelta, resolveGrammarSampling } from "./sampling.js";
|
|
3
|
+
import type { CustomToolCallState, OpenAIResponsesStreamOptions } from "./types.js";
|
|
4
|
+
import { localToolName } from "./text.js";
|
|
5
|
+
|
|
6
|
+
export function supportsGrammar(model: Model<Api>): boolean {
|
|
7
|
+
return (model.compat as { supportsOpenAIGrammarTools?: boolean } | undefined)?.supportsOpenAIGrammarTools === true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Capture the actual request's custom declarations, including Lite namespaces.
|
|
11
|
+
* Legacy custom tools without sampling metadata keep their established {input}. */
|
|
12
|
+
export function responseGrammarProperties(body: { tools?: unknown; input?: unknown }, tools?: Tool[], includeLegacy = false): ReadonlyMap<string, string> {
|
|
13
|
+
const properties = new Map<string, string>();
|
|
14
|
+
const visit = (items: unknown, namespace?: string): void => {
|
|
15
|
+
if (!Array.isArray(items)) return;
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (!item || typeof item !== "object") continue;
|
|
18
|
+
if (item.type === "namespace" && typeof item.name === "string") { visit(item.tools, item.name); continue; }
|
|
19
|
+
if (item.type !== "custom" || typeof item.name !== "string") continue;
|
|
20
|
+
const name = localToolName(namespace, item.name);
|
|
21
|
+
const tool = tools?.find((candidate) => candidate.name === name);
|
|
22
|
+
const grammar = tool && resolveGrammarSampling(tool, true);
|
|
23
|
+
if (grammar) properties.set(name, grammar.inputProperty);
|
|
24
|
+
else if (includeLegacy) properties.set(name, "input");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
visit(body.tools);
|
|
28
|
+
if (Array.isArray(body.input)) for (const item of body.input) {
|
|
29
|
+
if (item?.type === "additional_tools") visit(item.tools);
|
|
30
|
+
}
|
|
31
|
+
return properties;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function customArguments(name: string, input: string, options?: OpenAIResponsesStreamOptions): Record<string, string> {
|
|
35
|
+
return { [options?.grammarToolInputProperties?.get(name) ?? "input"]: input };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updateCustomInput(state: CustomToolCallState, input: string, close: boolean,
|
|
39
|
+
output: AssistantMessage, stream: AssistantMessageEventStream, options?: OpenAIResponsesStreamOptions): void {
|
|
40
|
+
const property = options?.grammarToolInputProperties?.get(state.block.name);
|
|
41
|
+
const previous = state.input;
|
|
42
|
+
state.input = input;
|
|
43
|
+
state.block.partialInput = input;
|
|
44
|
+
state.block.arguments = customArguments(state.block.name, input, options);
|
|
45
|
+
if (property !== undefined) {
|
|
46
|
+
state.inputJson ??= { input: "", started: false, closed: false };
|
|
47
|
+
const delta = grammarInputDelta(state.inputJson, property, input, close);
|
|
48
|
+
if (delta) stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta, partial: output });
|
|
49
|
+
} else if (!close && input.startsWith(previous) && input.length > previous.length) {
|
|
50
|
+
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta: input.slice(previous.length), partial: output });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -6,6 +6,7 @@ import { decodeWebSearchActivityTextSignature, isWebSearchActivityTextSignature,
|
|
|
6
6
|
import { sanitizeSurrogates, shortHash } from "./text.js";
|
|
7
7
|
import { wireToolIdentity } from "./tool-identity.js";
|
|
8
8
|
import { type ConvertResponsesMessagesOptions, type InternalAssistantContent, type Message } from "./types.js";
|
|
9
|
+
import { grammarToolInput } from "./sampling.js";
|
|
9
10
|
|
|
10
11
|
export function convertResponsesMessages<TApi extends Api>(
|
|
11
12
|
model: Model<TApi>,
|
|
@@ -14,6 +15,7 @@ export function convertResponsesMessages<TApi extends Api>(
|
|
|
14
15
|
options?: ConvertResponsesMessagesOptions,
|
|
15
16
|
): ResponseInput {
|
|
16
17
|
const messages: ResponseInput = [];
|
|
18
|
+
const customCalls = new Map<string, boolean>();
|
|
17
19
|
const normalizeIdPart = (part: string) => {
|
|
18
20
|
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
19
21
|
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
@@ -92,9 +94,14 @@ export function convertResponsesMessages<TApi extends Api>(
|
|
|
92
94
|
assistantBlockIndex++;
|
|
93
95
|
} else if (block.type === "toolCall") {
|
|
94
96
|
const [callId, itemIdRaw] = block.id.split("|");
|
|
95
|
-
const
|
|
97
|
+
const property = options?.grammarToolInputProperties?.get(block.name);
|
|
98
|
+
const declared = options?.grammarToolInputProperties !== undefined && context.tools?.some((tool) => tool.name === block.name);
|
|
99
|
+
const custom = property !== undefined || (!declared && itemIdRaw?.startsWith("ctc_") === true && typeof block.arguments.input === "string");
|
|
100
|
+
customCalls.set(callId, custom);
|
|
96
101
|
let itemId: string | undefined = itemIdRaw;
|
|
97
102
|
if (isDifferentModel && (itemId?.startsWith("fc_") || itemId?.startsWith("ctc_"))) itemId = undefined;
|
|
103
|
+
if (!custom && itemId?.startsWith("ctc_")) itemId = undefined;
|
|
104
|
+
if (custom && itemId?.startsWith("fc_")) itemId = undefined;
|
|
98
105
|
const wireIdentity = wireToolIdentity(block.name, block.thoughtSignature);
|
|
99
106
|
if (custom) {
|
|
100
107
|
output.push({
|
|
@@ -103,7 +110,7 @@ export function convertResponsesMessages<TApi extends Api>(
|
|
|
103
110
|
call_id: callId,
|
|
104
111
|
name: wireIdentity.name,
|
|
105
112
|
...(wireIdentity.namespace ? { namespace: wireIdentity.namespace } : {}),
|
|
106
|
-
input:
|
|
113
|
+
input: sanitizeSurrogates(grammarToolInput(block.name, block.arguments, property ?? "input")),
|
|
107
114
|
} as ResponseInput[number]);
|
|
108
115
|
} else {
|
|
109
116
|
output.push({
|
|
@@ -157,7 +164,7 @@ export function convertResponsesMessages<TApi extends Api>(
|
|
|
157
164
|
]
|
|
158
165
|
: sanitizeSurrogates(hasText ? textResult : "(see attached image)");
|
|
159
166
|
messages.push({
|
|
160
|
-
type: itemId?.startsWith("ctc_") ? "custom_tool_call_output" : "function_call_output",
|
|
167
|
+
type: (customCalls.get(callId) ?? (options?.grammarToolInputProperties?.has(msg.toolName) || itemId?.startsWith("ctc_"))) ? "custom_tool_call_output" : "function_call_output",
|
|
161
168
|
call_id: callId,
|
|
162
169
|
output,
|
|
163
170
|
} as ResponseInput[number]);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export interface GrammarInputBuffer { input: string; started: boolean; closed: boolean }
|
|
4
|
+
export interface GrammarSampling { format: "lark" | "regex"; definition: string; inputProperty: string }
|
|
5
|
+
|
|
6
|
+
// Pi's extension loader aliases SDK roots to files, so api/* subpath imports
|
|
7
|
+
// do not resolve in production extensions. Implement the small wire contract
|
|
8
|
+
// locally rather than resolving private SDK dist paths.
|
|
9
|
+
export function resolveGrammarSampling(tool: Tool, supported: boolean): GrammarSampling | undefined {
|
|
10
|
+
const sampling = tool.constrainedSampling;
|
|
11
|
+
if (!supported || !sampling || sampling.type !== "grammar") return undefined;
|
|
12
|
+
const schema = tool.parameters as { type?: unknown; required?: unknown; properties?: Record<string, { type?: unknown }> };
|
|
13
|
+
const required = schema.required;
|
|
14
|
+
if (schema.type !== "object" || !Array.isArray(required) || required.length !== 1
|
|
15
|
+
|| typeof required[0] !== "string" || schema.properties?.[required[0]]?.type !== "string") {
|
|
16
|
+
throw new Error(`Tool ${tool.name} grammar requires exactly one required string property`);
|
|
17
|
+
}
|
|
18
|
+
for (const [variant, format] of [["openai_lark", "lark"], ["openai_regex", "regex"]] as const) {
|
|
19
|
+
const definition = sampling.variants[variant];
|
|
20
|
+
if (typeof definition === "string" && definition.trim()) return { format, definition, inputProperty: required[0] };
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Tool ${tool.name} has no supported grammar variant`);
|
|
23
|
+
}
|
|
24
|
+
export function grammarToolInput(name: string, args: Record<string, unknown>, property: string): string {
|
|
25
|
+
const value = args[property];
|
|
26
|
+
if (typeof value !== "string") throw new Error(`Grammar tool ${name} requires string argument ${property}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export function grammarInputDelta(buffer: GrammarInputBuffer, property: string, next: string, close: boolean): string | undefined {
|
|
30
|
+
if (buffer.closed) {
|
|
31
|
+
if (next !== buffer.input) throw new Error("Grammar input changed after it was closed");
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!next.startsWith(buffer.input)) throw new Error("Grammar input is not monotonic");
|
|
35
|
+
const suffix = JSON.stringify(next.slice(buffer.input.length)).slice(1, -1);
|
|
36
|
+
const delta = (buffer.started ? "" : `{${JSON.stringify(property)}:"`) + suffix + (close ? '"}' : "");
|
|
37
|
+
buffer.input = next;
|
|
38
|
+
buffer.started = true;
|
|
39
|
+
buffer.closed = close;
|
|
40
|
+
return delta || undefined;
|
|
41
|
+
}
|
|
@@ -5,7 +5,8 @@ import { createResponsesStreamState } from "./stream-state.js";
|
|
|
5
5
|
import { createResponseTextRenderer } from "./text-renderer.js";
|
|
6
6
|
import { localToolName, parseStreamingJson } from "./text.js";
|
|
7
7
|
import { encodeToolNamespaceSignature } from "./tool-identity.js";
|
|
8
|
-
import type { TextBlock, ThinkingBlock, ToolCallBlock } from "./types.js";
|
|
8
|
+
import type { CustomToolCallState, TextBlock, ThinkingBlock, ToolCallBlock } from "./types.js";
|
|
9
|
+
import { customArguments, updateCustomInput } from "./grammar.js";
|
|
9
10
|
import { type OpenAIResponsesStreamOptions } from "./types.js";
|
|
10
11
|
import { finalizeResponseUsage } from "./usage.js";
|
|
11
12
|
|
|
@@ -97,12 +98,12 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
97
98
|
type: "toolCall",
|
|
98
99
|
id: `${customItem.call_id}|${itemId}`,
|
|
99
100
|
name: localToolName(customItem.namespace, customItem.name),
|
|
100
|
-
arguments:
|
|
101
|
+
arguments: customArguments(localToolName(customItem.namespace, customItem.name), input, options),
|
|
101
102
|
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
102
103
|
partialInput: input,
|
|
103
104
|
};
|
|
104
105
|
pushResponseBlock(currentBlock, event.output_index);
|
|
105
|
-
|
|
106
|
+
const state: CustomToolCallState = {
|
|
106
107
|
kind: "custom_tool_call",
|
|
107
108
|
blockIndex: blockIndex(),
|
|
108
109
|
block: currentBlock,
|
|
@@ -110,8 +111,10 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
110
111
|
sourceItemId: customItem.id,
|
|
111
112
|
callId: customItem.call_id,
|
|
112
113
|
input,
|
|
113
|
-
}
|
|
114
|
+
};
|
|
115
|
+
outputStates.set(event.output_index, state);
|
|
114
116
|
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
117
|
+
if (options?.grammarToolInputProperties?.has(currentBlock.name)) updateCustomInput(state, input, false, output, stream, options);
|
|
115
118
|
}
|
|
116
119
|
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
117
120
|
const state = outputStates.get(event.output_index);
|
|
@@ -193,11 +196,12 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
193
196
|
const customEvent = event as unknown as { output_index?: number; item_id?: string; call_id?: string; delta?: string };
|
|
194
197
|
const state = findCustomToolCallState(customEvent);
|
|
195
198
|
if (state && typeof customEvent.delta === "string") {
|
|
196
|
-
state.input
|
|
197
|
-
state.block.partialInput = state.input;
|
|
198
|
-
state.block.arguments = { input: state.input };
|
|
199
|
-
stream.push({ type: "toolcall_delta", contentIndex: state.blockIndex, delta: customEvent.delta, partial: output });
|
|
199
|
+
updateCustomInput(state, state.input + customEvent.delta, false, output, stream, options);
|
|
200
200
|
}
|
|
201
|
+
} else if ((event as { type?: string }).type === "response.custom_tool_call_input.done") {
|
|
202
|
+
const customEvent = event as unknown as { output_index?: number; item_id?: string; call_id?: string; input?: string };
|
|
203
|
+
const state = findCustomToolCallState(customEvent);
|
|
204
|
+
if (state && typeof customEvent.input === "string") updateCustomInput(state, customEvent.input, true, output, stream, options);
|
|
201
205
|
} else if (event.type === "response.function_call_arguments.delta") {
|
|
202
206
|
const state = outputStates.get(event.output_index);
|
|
203
207
|
if (state?.kind === "function_call") {
|
|
@@ -309,9 +313,8 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
309
313
|
const thoughtSignature = encodeToolNamespaceSignature(customItem.namespace, customItem.name);
|
|
310
314
|
const toolCall = state
|
|
311
315
|
? (() => {
|
|
312
|
-
state.input = input;
|
|
313
316
|
state.block.name = localToolName(customItem.namespace, customItem.name);
|
|
314
|
-
state
|
|
317
|
+
updateCustomInput(state, input, true, output, stream, options);
|
|
315
318
|
if (thoughtSignature) state.block.thoughtSignature = thoughtSignature;
|
|
316
319
|
delete state.block.partialInput;
|
|
317
320
|
return state.block;
|
|
@@ -321,7 +324,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
321
324
|
type: "toolCall",
|
|
322
325
|
id: `${customItem.call_id}|${customItemId(customItem.id, customItem.call_id)}`,
|
|
323
326
|
name: localToolName(customItem.namespace, customItem.name),
|
|
324
|
-
arguments:
|
|
327
|
+
arguments: customArguments(localToolName(customItem.namespace, customItem.name), input, options),
|
|
325
328
|
...(thoughtSignature ? { thoughtSignature } : {}),
|
|
326
329
|
};
|
|
327
330
|
pushResponseBlock(fallbackToolCall, event.output_index);
|
|
@@ -11,10 +11,11 @@ export function shortHash(str: string): string {
|
|
|
11
11
|
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function parseStreamingJson(partialJson: string):
|
|
14
|
+
export function parseStreamingJson(partialJson: string): JsonObject {
|
|
15
15
|
if (!partialJson || partialJson.trim() === "") return {};
|
|
16
16
|
try {
|
|
17
|
-
|
|
17
|
+
const value: unknown = JSON.parse(partialJson);
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {};
|
|
18
19
|
} catch {
|
|
19
20
|
return {};
|
|
20
21
|
}
|
|
@@ -30,3 +31,4 @@ export function localToolName(namespace: unknown, name: string): string {
|
|
|
30
31
|
if (namespace === "image_gen" && name === "imagegen") return "image_generation";
|
|
31
32
|
return name;
|
|
32
33
|
}
|
|
34
|
+
import type { JsonObject } from "@earendil-works/pi-ai";
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { type Tool } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Tool as OpenAITool } from "openai/resources/responses/responses.js";
|
|
3
3
|
import { type ConvertResponsesToolsOptions } from "./types.js";
|
|
4
|
+
import { resolveGrammarSampling } from "./sampling.js";
|
|
4
5
|
|
|
5
6
|
export function convertResponsesTools(tools: Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] {
|
|
6
7
|
const strict = options?.strict === undefined ? false : options.strict;
|
|
7
|
-
return tools.map((tool) =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
return tools.map((tool) => {
|
|
9
|
+
const grammar = resolveGrammarSampling(tool, options?.supportsOpenAIGrammarTools === true);
|
|
10
|
+
if (grammar) return {
|
|
11
|
+
type: "custom", name: tool.name, description: tool.description,
|
|
12
|
+
format: { type: "grammar", syntax: grammar.format, definition: grammar.definition },
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
type: "function", name: tool.name, description: tool.description,
|
|
16
|
+
parameters: tool.parameters as unknown as Record<string, unknown>, strict,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
14
19
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Context, type Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import { type ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
|
|
4
|
+
import type { GrammarInputBuffer } from "./sampling.js";
|
|
4
5
|
|
|
5
6
|
type MessageRole = Context["messages"][number]["role"];
|
|
6
7
|
|
|
@@ -52,6 +53,7 @@ export interface ReplayableResponseMessageItem {
|
|
|
52
53
|
export type InternalAssistantContent = Extract<Message, { role: "assistant" }>["content"][number] | ImageGenerationCallBlock;
|
|
53
54
|
|
|
54
55
|
export interface OpenAIResponsesStreamOptions {
|
|
56
|
+
grammarToolInputProperties?: ReadonlyMap<string, string>;
|
|
55
57
|
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
|
|
56
58
|
resolveServiceTier?: (
|
|
57
59
|
responseServiceTier: ResponseCreateParamsStreaming["service_tier"] | undefined,
|
|
@@ -66,10 +68,12 @@ export type TextSignaturePhase = "commentary" | "final_answer";
|
|
|
66
68
|
|
|
67
69
|
export interface ConvertResponsesMessagesOptions {
|
|
68
70
|
includeSystemPrompt?: boolean;
|
|
71
|
+
grammarToolInputProperties?: ReadonlyMap<string, string>;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface ConvertResponsesToolsOptions {
|
|
72
75
|
strict?: boolean | null;
|
|
76
|
+
supportsOpenAIGrammarTools?: boolean;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
export type ThinkingBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
|
|
@@ -108,6 +112,7 @@ export type CustomToolCallState = {
|
|
|
108
112
|
sourceItemId?: string;
|
|
109
113
|
callId: string;
|
|
110
114
|
input: string;
|
|
115
|
+
inputJson?: GrammarInputBuffer;
|
|
111
116
|
};
|
|
112
117
|
|
|
113
118
|
export type OutputState = ReasoningState | MessageState | FunctionCallState | CustomToolCallState;
|
package/src/settings.ts
CHANGED
|
@@ -6,50 +6,59 @@ import type { CodexRequestProfileOverride } from "./codex-request-profile.js";
|
|
|
6
6
|
export const PACKAGE_NAME = "pi-codex-minimal-tools";
|
|
7
7
|
export const CONFIG_FILE_NAME = "config.json";
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface CodexGlobalSettings {
|
|
10
10
|
enabled: boolean;
|
|
11
11
|
glyphStyle: "unicode" | "ascii";
|
|
12
12
|
autoEnable: boolean;
|
|
13
|
+
webSocketEnabled: boolean;
|
|
14
|
+
fastMode: boolean;
|
|
15
|
+
imageGeneration: boolean;
|
|
16
|
+
imageOutputDir: string;
|
|
17
|
+
imageModel: "gpt-image-2" | "gpt-image-1.5" | "gpt-image-1";
|
|
18
|
+
directImageApiFallback: boolean;
|
|
19
|
+
viewImageWorkspaceOnly: boolean;
|
|
20
|
+
deferApplyPatchRendering: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CodexMinimalToolsSettings extends CodexGlobalSettings {
|
|
13
24
|
nativeProviderTools: boolean;
|
|
14
25
|
openaiTransport: "sse" | "websocket" | "websocket-cached" | "auto";
|
|
15
26
|
openaiWebSocketPrewarm: boolean;
|
|
16
|
-
fastMode: boolean;
|
|
17
27
|
compactionMode: "pi" | "responses" | "responses-compact";
|
|
18
28
|
requestProfile: CodexRequestProfileOverride;
|
|
19
29
|
apiKeyMode: boolean;
|
|
20
|
-
imageGeneration: boolean;
|
|
21
30
|
webSearchEnabled: boolean;
|
|
22
|
-
imageOutputDir: string;
|
|
23
|
-
imageModel: "gpt-image-2" | "gpt-image-1.5" | "gpt-image-1";
|
|
24
|
-
directImageApiFallback: boolean;
|
|
25
31
|
viewImage: boolean;
|
|
26
|
-
viewImageWorkspaceOnly: boolean;
|
|
27
32
|
applyPatchEnabled: boolean;
|
|
28
33
|
additionalModelIds: string[];
|
|
29
|
-
deferApplyPatchRendering: boolean;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
export const
|
|
36
|
+
export const DEFAULT_GLOBAL_SETTINGS: Readonly<CodexGlobalSettings> = {
|
|
33
37
|
enabled: true,
|
|
34
38
|
glyphStyle: "unicode",
|
|
35
39
|
autoEnable: true,
|
|
40
|
+
webSocketEnabled: true,
|
|
41
|
+
fastMode: false,
|
|
42
|
+
imageGeneration: true,
|
|
43
|
+
imageOutputDir: ".pi/openai-codex-images",
|
|
44
|
+
imageModel: "gpt-image-2",
|
|
45
|
+
directImageApiFallback: false,
|
|
46
|
+
viewImageWorkspaceOnly: false,
|
|
47
|
+
deferApplyPatchRendering: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_SETTINGS: CodexMinimalToolsSettings = {
|
|
51
|
+
...DEFAULT_GLOBAL_SETTINGS,
|
|
36
52
|
nativeProviderTools: true,
|
|
37
53
|
openaiTransport: "sse",
|
|
38
54
|
openaiWebSocketPrewarm: true,
|
|
39
|
-
fastMode: false,
|
|
40
55
|
compactionMode: "pi",
|
|
41
56
|
requestProfile: {},
|
|
42
57
|
apiKeyMode: false,
|
|
43
|
-
imageGeneration: true,
|
|
44
58
|
webSearchEnabled: false,
|
|
45
|
-
imageOutputDir: ".pi/openai-codex-images",
|
|
46
|
-
imageModel: "gpt-image-2",
|
|
47
|
-
directImageApiFallback: false,
|
|
48
59
|
viewImage: false,
|
|
49
|
-
viewImageWorkspaceOnly: false,
|
|
50
60
|
applyPatchEnabled: true,
|
|
51
61
|
additionalModelIds: [],
|
|
52
|
-
deferApplyPatchRendering: false,
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
type SettingsRecord = Record<string, unknown>;
|
|
@@ -188,6 +197,7 @@ export function loadSettings(_cwd?: string): CodexMinimalToolsSettings {
|
|
|
188
197
|
enabled: boolSetting(raw, "enabled"),
|
|
189
198
|
glyphStyle: glyphStyleSetting(raw),
|
|
190
199
|
autoEnable: boolSetting(raw, "autoEnable"),
|
|
200
|
+
webSocketEnabled: boolSetting(raw, "webSocketEnabled"),
|
|
191
201
|
nativeProviderTools: boolSetting(raw, "nativeProviderTools"),
|
|
192
202
|
openaiTransport: openaiTransportSetting(raw),
|
|
193
203
|
openaiWebSocketPrewarm: boolSetting(raw, "openaiWebSocketPrewarm"),
|
package/src/tool-activation.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { installCodexIdentityLifecycle } from "./codex-identity-extension.js";
|
|
9
9
|
import { loadModelSettings } from "./model-catalog/runtime.js";
|
|
10
10
|
import { loadSettings } from "./settings.js";
|
|
11
|
+
import { codeModeOwner, OWNER_CHANGED } from "./code-mode-owner.js";
|
|
11
12
|
|
|
12
13
|
function enableDefinitions(broker: CodexBroker) {
|
|
13
14
|
for (const tool of broker.tools.values()) {
|
|
@@ -40,7 +41,13 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
|
|
|
40
41
|
suppressed.clear();
|
|
41
42
|
return active;
|
|
42
43
|
};
|
|
44
|
+
let latest: ExtensionContext | undefined;
|
|
45
|
+
let syncing = false;
|
|
43
46
|
const sync = (ctx: ExtensionContext) => {
|
|
47
|
+
latest = ctx;
|
|
48
|
+
if (syncing) return;
|
|
49
|
+
syncing = true;
|
|
50
|
+
try {
|
|
44
51
|
const settings = loadSettings(ctx.cwd);
|
|
45
52
|
const available = settings.enabled && hasConfiguredModelsLoaded(ctx, settings);
|
|
46
53
|
if (available) enableDefinitions(broker);
|
|
@@ -48,9 +55,17 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
|
|
|
48
55
|
const active = new Set(current);
|
|
49
56
|
const capabilities = computeToolCapabilities(ctx.model as ModelLike | undefined, settings);
|
|
50
57
|
const model = loadModelSettings(ctx.model as ModelLike | undefined, ctx.cwd, settings);
|
|
58
|
+
const controls = new Map<string, NonNullable<ReturnType<typeof codeModeOwner>>>();
|
|
51
59
|
for (const name of PACKAGE_TOOL_NAMES) {
|
|
52
60
|
const owned = broker.tools.get(name);
|
|
53
61
|
if (!owned) continue; // Other extensions retain ownership of uninstalled names.
|
|
62
|
+
const control = codeModeOwner(pi, name, owned, broker.codeModeDefinitions?.get(name));
|
|
63
|
+
if (owned.codeModeOwner?.replaced) continue;
|
|
64
|
+
if (control) {
|
|
65
|
+
controls.set(name, control);
|
|
66
|
+
if (control.activeIntent === undefined) continue; // foreign replacement
|
|
67
|
+
if (control.activeIntent) active.add(name); else active.delete(name);
|
|
68
|
+
}
|
|
54
69
|
const hostedWithoutCore = !broker.coreEnabled && (
|
|
55
70
|
(name === "web_search" && model.webSearchImplementation === "hosted")
|
|
56
71
|
|| (name === "image_generation" && model.imageGenerationImplementation === "hosted" && !settings.directImageApiFallback)
|
|
@@ -59,16 +74,28 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
|
|
|
59
74
|
if (!desired) active.delete(name);
|
|
60
75
|
else if (settings.autoEnable) active.add(name);
|
|
61
76
|
}
|
|
62
|
-
|
|
77
|
+
const ownsPatch = broker.tools.has("apply_patch") && !broker.tools.get("apply_patch")?.codeModeOwner?.replaced;
|
|
78
|
+
if (ownsPatch && active.has("apply_patch")) {
|
|
63
79
|
for (const name of NATIVE_MUTATION_TOOL_NAMES) {
|
|
64
80
|
if (active.delete(name) && !suppressed.has(name)) suppressed.set(name, current.indexOf(name));
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
|
-
const
|
|
68
|
-
for (const name of
|
|
69
|
-
|
|
83
|
+
const physical = new Set(active);
|
|
84
|
+
for (const [name, control] of controls) {
|
|
85
|
+
const projected = control.projectActive(active.has(name));
|
|
86
|
+
if (projected === undefined) { if (current.includes(name)) physical.add(name); else physical.delete(name); }
|
|
87
|
+
else if (projected) physical.add(name); else physical.delete(name);
|
|
88
|
+
}
|
|
89
|
+
const next = current.filter(name => physical.has(name));
|
|
90
|
+
for (const name of physical) if (!next.includes(name)) next.push(name);
|
|
91
|
+
if (!ownsPatch || !active.has("apply_patch")) restore(next);
|
|
70
92
|
if (next.join("\0") !== current.join("\0")) pi.setActiveTools(next);
|
|
93
|
+
for (const control of controls.values()) control.reconcile();
|
|
94
|
+
} finally { syncing = false; }
|
|
71
95
|
};
|
|
96
|
+
const offOwner = pi.events.on(OWNER_CHANGED, (message) => {
|
|
97
|
+
if ((message as { version?: number })?.version === 1 && latest && !broker.closed) sync(latest);
|
|
98
|
+
});
|
|
72
99
|
pi.on("session_start", (_event, ctx) => {
|
|
73
100
|
broker.presentation.clear();
|
|
74
101
|
sync(ctx);
|
|
@@ -77,6 +104,8 @@ export function ensureCodexServices(pi: ExtensionAPI): CodexBroker {
|
|
|
77
104
|
pi.on("thinking_level_select", (_event, ctx) => sync(ctx));
|
|
78
105
|
pi.on("agent_end", () => broker.presentation.scheduleFlush());
|
|
79
106
|
pi.on("session_shutdown", () => {
|
|
107
|
+
offOwner();
|
|
108
|
+
for (const tool of broker.tools.values()) tool.codeModeOwner?.control?.dispose();
|
|
80
109
|
try { broker.presentation.flush(); }
|
|
81
110
|
finally {
|
|
82
111
|
broker.presentation.clear();
|