Skip to content

Commit 0b4fc26

Browse files
codex: surface deferred dynamic tool names (#83813)
* codex: surface deferred dynamic tool names * codex: keep prompt snapshots source-backed * style: wrap mac voice settings help text * style: satisfy swiftformat for voice wake help text * style: apply swiftformat to voice wake help text * test: load codex prompt snapshots through plugin aliases * test: type codex source surface loader * test: avoid extra codex loader suppression --------- Co-authored-by: pashpashpash <[email protected]>
1 parent 48d9966 commit 0b4fc26

10 files changed

Lines changed: 139 additions & 26 deletions

File tree

apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,9 @@ private struct TriggerPhraseHelpRow: View {
818818
.frame(width: 18)
819819
.padding(.top, 1)
820820

821-
Text("OpenClaw reacts when any trigger appears in a transcription. Keep phrases short to avoid false positives.")
821+
Text(
822+
"OpenClaw reacts when any trigger appears in a transcription. " +
823+
"Keep phrases short to avoid false positives.")
822824
.font(.footnote)
823825
.foregroundStyle(.secondary)
824826
.fixedSize(horizontal: false, vertical: true)

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,9 @@ export async function runCodexAppServerAttempt(
980980
historyMessages =
981981
(await readMirroredSessionHistoryMessages(activeSessionFile)) ?? historyMessages;
982982
}
983-
const baseDeveloperInstructions = buildDeveloperInstructions(params);
983+
const baseDeveloperInstructions = buildDeveloperInstructions(params, {
984+
dynamicTools: toolBridge.specs,
985+
});
984986
// Keep OpenClaw user-editable context in the turn input so native Codex
985987
// system/developer instructions remain the higher-priority policy layer.
986988
const workspaceBootstrapContext = await buildCodexWorkspaceBootstrapContext({

extensions/codex/src/app-server/thread-lifecycle.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,52 @@ describe("Codex app-server native code mode config", () => {
6868
);
6969
});
7070

71+
it("summarizes deferred dynamic tool names in developer instructions", () => {
72+
const instructions = buildDeveloperInstructions(createAttemptParams({ provider: "openai" }), {
73+
dynamicTools: [
74+
{
75+
name: "message",
76+
description: "Send a message",
77+
inputSchema: { type: "object" },
78+
},
79+
{
80+
name: "music_generate",
81+
description: "Create music",
82+
inputSchema: { type: "object" },
83+
namespace: "openclaw",
84+
deferLoading: true,
85+
},
86+
{
87+
name: "image_generate",
88+
description: "Create images",
89+
inputSchema: { type: "object" },
90+
namespace: "openclaw",
91+
deferLoading: true,
92+
},
93+
],
94+
});
95+
96+
expect(instructions).toContain(
97+
"Deferred searchable OpenClaw dynamic tools available: image_generate, music_generate.",
98+
);
99+
expect(instructions).toContain("Use `tool_search` to load exact callable specs before use.");
100+
expect(instructions).not.toContain("message,");
101+
});
102+
103+
it("keeps developer instructions compact when no dynamic tools are deferred", () => {
104+
const instructions = buildDeveloperInstructions(createAttemptParams({ provider: "openai" }), {
105+
dynamicTools: [
106+
{
107+
name: "message",
108+
description: "Send a message",
109+
inputSchema: { type: "object" },
110+
},
111+
],
112+
});
113+
114+
expect(instructions).not.toContain("Deferred searchable OpenClaw dynamic tools available");
115+
});
116+
71117
it("keeps OpenClaw skill catalogs out of developer instructions", () => {
72118
const params = createAttemptParams({ provider: "openai" });
73119
params.skillsSnapshot = {

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export async function startOrResumeThread(params: {
261261
threadId: binding.threadId,
262262
authProfileId,
263263
appServer: params.appServer,
264+
dynamicTools: params.dynamicTools,
264265
developerInstructions: params.developerInstructions,
265266
config: resumeConfig,
266267
nativeCodeModeEnabled: params.nativeCodeModeEnabled,
@@ -585,7 +586,9 @@ export function buildThreadStartParams(
585586
nativeCodeModeOnlyEnabled: options.nativeCodeModeOnlyEnabled,
586587
}),
587588
...(options.nativeCodeModeEnabled === false ? { environments: [] } : {}),
588-
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
589+
developerInstructions:
590+
options.developerInstructions ??
591+
buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
589592
dynamicTools: options.dynamicTools,
590593
experimentalRawEvents: true,
591594
persistExtendedHistory: true,
@@ -598,6 +601,7 @@ export function buildThreadResumeParams(
598601
threadId: string;
599602
authProfileId?: string;
600603
appServer: CodexAppServerRuntimeOptions;
604+
dynamicTools?: CodexDynamicToolSpec[];
601605
developerInstructions?: string;
602606
config?: JsonObject;
603607
nativeCodeModeEnabled?: boolean;
@@ -623,7 +627,9 @@ export function buildThreadResumeParams(
623627
nativeCodeModeEnabled: options.nativeCodeModeEnabled,
624628
nativeCodeModeOnlyEnabled: options.nativeCodeModeOnlyEnabled,
625629
}),
626-
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
630+
developerInstructions:
631+
options.developerInstructions ??
632+
buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
627633
persistExtendedHistory: true,
628634
};
629635
}
@@ -820,13 +826,17 @@ function compareJsonFingerprint(left: JsonValue, right: JsonValue): number {
820826
return JSON.stringify(left).localeCompare(JSON.stringify(right));
821827
}
822828

823-
export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string {
829+
export function buildDeveloperInstructions(
830+
params: EmbeddedRunAttemptParams,
831+
options: { dynamicTools?: readonly CodexDynamicToolSpec[] } = {},
832+
): string {
824833
const nativeCommandGuidance = listRegisteredPluginAgentPromptGuidance({
825834
surface: "codex_app_server",
826835
includeLegacyGlobalGuidance: false,
827836
}).join("\n");
828837
const sections = [
829838
"Running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes capabilities when available.",
839+
buildDeferredDynamicToolManifest(options.dynamicTools),
830840
"Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation; if it is not already loaded, search for `sessions_spawn` in the `openclaw` dynamic tool namespace before calling it.",
831841
buildVisibleReplyInstruction(params),
832842
nativeCommandGuidance,
@@ -835,6 +845,23 @@ export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): st
835845
return sections.filter((section) => typeof section === "string" && section.trim()).join("\n\n");
836846
}
837847

848+
function buildDeferredDynamicToolManifest(
849+
dynamicTools: readonly CodexDynamicToolSpec[] | undefined,
850+
): string | undefined {
851+
const deferredToolNames = [
852+
...new Set(
853+
(dynamicTools ?? [])
854+
.filter((tool) => tool.deferLoading === true)
855+
.map((tool) => tool.name.trim())
856+
.filter(Boolean),
857+
),
858+
].toSorted((left, right) => left.localeCompare(right));
859+
if (deferredToolNames.length === 0) {
860+
return undefined;
861+
}
862+
return `Deferred searchable OpenClaw dynamic tools available: ${deferredToolNames.join(", ")}. Use \`tool_search\` to load exact callable specs before use.`;
863+
}
864+
838865
function buildVisibleReplyInstruction(params: EmbeddedRunAttemptParams): string {
839866
if (params.sourceReplyDeliveryMode === "message_tool_only") {
840867
return "Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply.";

extensions/codex/test-api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export function buildCodexHarnessPromptSnapshot(params: {
4343
config?: JsonObject;
4444
promptText?: string;
4545
}): CodexHarnessPromptSnapshot {
46-
const developerInstructions = buildDeveloperInstructions(params.attempt);
46+
const developerInstructions = buildDeveloperInstructions(params.attempt, {
47+
dynamicTools: params.dynamicTools,
48+
});
4749
return {
4850
developerInstructions,
4951
threadStartParams: buildThreadStartParams(params.attempt, {

src/test-utils/bundled-plugin-public-surface.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
findBundledPluginMetadataById,
1111
type BundledPluginMetadata,
1212
} from "../plugins/bundled-plugin-metadata.js";
13+
import {
14+
getCachedPluginSourceModuleLoader,
15+
type PluginModuleLoaderCache,
16+
} from "../plugins/plugin-module-loader-cache.js";
1317
import { normalizeBundledPluginArtifactSubpath } from "../plugins/public-surface-runtime.js";
1418
import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
1519

@@ -20,6 +24,7 @@ const OPENCLAW_PACKAGE_ROOT =
2024
}) ?? fileURLToPath(new URL("../..", import.meta.url));
2125

2226
type BundledPluginPublicSurfaceMetadata = Pick<BundledPluginMetadata, "dirName">;
27+
const sourceModuleLoaders: PluginModuleLoaderCache = new Map();
2328

2429
function isSafeBundledPluginDirName(pluginId: string): boolean {
2530
return /^[a-z0-9][a-z0-9._-]*$/u.test(pluginId);
@@ -127,6 +132,26 @@ export const loadBundledPluginPublicSurfaceSync: BundledPluginPublicSurfaceLoade
127132
});
128133
};
129134

135+
export function loadBundledPluginPublicSurfaceSourceSync(params: {
136+
pluginId: string;
137+
artifactBasename: string;
138+
}): object {
139+
const modulePath = resolveVitestSourceModulePath(
140+
resolveBundledPluginPublicModulePath({
141+
pluginId: params.pluginId,
142+
artifactBasename: params.artifactBasename,
143+
}),
144+
);
145+
const loader = getCachedPluginSourceModuleLoader({
146+
cache: sourceModuleLoaders,
147+
modulePath,
148+
importerUrl: import.meta.url,
149+
loaderFilename: import.meta.url,
150+
pluginSdkResolution: "src",
151+
});
152+
return loader(modulePath) as object;
153+
}
154+
130155
export const loadBundledPluginPublicSurface: AsyncBundledPluginPublicSurfaceLoader = (params) => {
131156
const metadata = findBundledPluginMetadata(params.pluginId);
132157
return loadBundledPluginPublicSurfaceModule({

test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,16 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
219219
"roughTokens": 10111
220220
},
221221
"openClawDeveloperInstructions": {
222-
"chars": 2506,
223-
"roughTokens": 627
222+
"chars": 2774,
223+
"roughTokens": 694
224224
},
225225
"totalTextOnly": {
226-
"chars": 25901,
227-
"roughTokens": 6476
226+
"chars": 26169,
227+
"roughTokens": 6543
228228
},
229229
"totalWithDynamicToolsJson": {
230-
"chars": 66344,
231-
"roughTokens": 16586
230+
"chars": 66612,
231+
"roughTokens": 16653
232232
},
233233
"userInputText": {
234234
"chars": 1747,
@@ -415,6 +415,8 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for
415415
````text
416416
Running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes capabilities when available.
417417
418+
Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use.
419+
418420
Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation; if it is not already loaded, search for `sessions_spawn` in the `openclaw` dynamic tool namespace before calling it.
419421
420422
Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply.

test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,16 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
219219
"roughTokens": 10054
220220
},
221221
"openClawDeveloperInstructions": {
222-
"chars": 1482,
223-
"roughTokens": 371
222+
"chars": 1750,
223+
"roughTokens": 438
224224
},
225225
"totalTextOnly": {
226-
"chars": 24377,
227-
"roughTokens": 6095
226+
"chars": 24645,
227+
"roughTokens": 6162
228228
},
229229
"totalWithDynamicToolsJson": {
230-
"chars": 64595,
231-
"roughTokens": 16149
230+
"chars": 64863,
231+
"roughTokens": 16216
232232
},
233233
"userInputText": {
234234
"chars": 1247,
@@ -415,6 +415,8 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for
415415
````text
416416
Running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes capabilities when available.
417417
418+
Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use.
419+
418420
Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation; if it is not already loaded, search for `sessions_spawn` in the `openclaw` dynamic tool namespace before calling it.
419421
420422
Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply.

test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,16 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
220220
"roughTokens": 10328
221221
},
222222
"openClawDeveloperInstructions": {
223-
"chars": 1482,
224-
"roughTokens": 371
223+
"chars": 1769,
224+
"roughTokens": 443
225225
},
226226
"totalTextOnly": {
227-
"chars": 26004,
228-
"roughTokens": 6501
227+
"chars": 26291,
228+
"roughTokens": 6573
229229
},
230230
"totalWithDynamicToolsJson": {
231-
"chars": 67317,
232-
"roughTokens": 16830
231+
"chars": 67604,
232+
"roughTokens": 16901
233233
},
234234
"userInputText": {
235235
"chars": 1485,
@@ -416,6 +416,8 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for
416416
````text
417417
Running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes capabilities when available.
418418
419+
Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, heartbeat_respond, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use.
420+
419421
Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation; if it is not already loaded, search for `sessions_spawn` in the `openclaw` dynamic tool namespace before calling it.
420422
421423
Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply.

test/helpers/agents/happy-path-prompt-snapshots.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
} from "../../../src/plugin-sdk/agent-harness-runtime.js";
2222
import { normalizeAgentRuntimeTools } from "../../../src/plugin-sdk/agent-harness-runtime.js";
2323
import { createOpenClawCodingTools } from "../../../src/plugin-sdk/agent-harness.js";
24-
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
24+
import { loadBundledPluginPublicSurfaceSourceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
2525
import {
2626
CODEX_MODEL_PROMPT_FIXTURE_DIR,
2727
CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR,
@@ -114,7 +114,10 @@ type PromptScenario = {
114114
toolSnapshotFile: string;
115115
};
116116

117-
const codexApi = loadBundledPluginTestApiSync("codex") as CodexPromptSnapshotApi;
117+
const codexApi = loadBundledPluginPublicSurfaceSourceSync({
118+
pluginId: "codex",
119+
artifactBasename: "test-api.js",
120+
}) as CodexPromptSnapshotApi;
118121

119122
const CODEX_WORKSPACE_BOOTSTRAP_CONTEXT_FILES = [
120123
{

0 commit comments

Comments
 (0)