From aa15534e71c8d1470113f54f34a2b53388dc45c6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 08:35:26 -0700 Subject: [PATCH] refactor: deduplicate Codex prompt snapshot fixtures --- scripts/generate-prompt-snapshots.ts | 115 +- .../codex-runtime-happy-path/README.md | 10 + .../codex-dynamic-tools.discord-group.json | 1705 +---------------- .../codex-dynamic-tools.heartbeat-turn.json | 1699 +--------------- .../discord-group-codex-message-tool.md | 2 +- .../telegram-heartbeat-codex-tool.md | 2 +- .../agents/happy-path-prompt-snapshots.ts | 15 +- test/scripts/prompt-snapshots.test.ts | 46 +- 8 files changed, 336 insertions(+), 3258 deletions(-) diff --git a/scripts/generate-prompt-snapshots.ts b/scripts/generate-prompt-snapshots.ts index 33fd3908a579..8f9d135036d4 100644 --- a/scripts/generate-prompt-snapshots.ts +++ b/scripts/generate-prompt-snapshots.ts @@ -24,6 +24,14 @@ const oxfmtPath = path.resolve( const execFileAsync = promisify(execFile); type PromptSnapshotFile = Awaited>[number]; +type CodexDynamicToolSnapshotSpec = { name: string }; +type CodexDynamicToolSnapshotOverrides = { + base: string; + replace: Record; +}; + +const CODEX_DYNAMIC_TOOL_SNAPSHOT_PREFIX = "codex-dynamic-tools."; +const CODEX_DYNAMIC_TOOL_BASE_SNAPSHOT = `${CODEX_DYNAMIC_TOOL_SNAPSHOT_PREFIX}telegram-direct.json`; function describeError(error: unknown): string { return error instanceof Error ? error.message : String(error); @@ -60,8 +68,49 @@ async function readSnapshotFiles(root: string, files: PromptSnapshotFile[]) { ); } -export async function createFormattedPromptSnapshotFiles(): Promise { - const files = await createHappyPathPromptSnapshotFiles(); +/** Keep complete Codex tool specs on the wire; deduplicate only their committed review fixtures. */ +function factorCodexDynamicToolSnapshotFiles(files: PromptSnapshotFile[]): PromptSnapshotFile[] { + const base = files.find((file) => path.basename(file.path) === CODEX_DYNAMIC_TOOL_BASE_SNAPSHOT); + if (!base) { + return files; + } + const baseTools = JSON.parse(base.content) as CodexDynamicToolSnapshotSpec[]; + if (new Set(baseTools.map((tool) => tool.name)).size !== baseTools.length) { + return files; + } + + return files.map((file) => { + const fileName = path.basename(file.path); + if ( + fileName === CODEX_DYNAMIC_TOOL_BASE_SNAPSHOT || + !fileName.startsWith(CODEX_DYNAMIC_TOOL_SNAPSHOT_PREFIX) || + !fileName.endsWith(".json") + ) { + return file; + } + const tools = JSON.parse(file.content) as CodexDynamicToolSnapshotSpec[]; + if ( + tools.length !== baseTools.length || + tools.some((tool, index) => tool.name !== baseTools[index]?.name) + ) { + return file; + } + const replace = Object.fromEntries( + tools.flatMap((tool, index) => + JSON.stringify(tool) === JSON.stringify(baseTools[index]) ? [] : [[tool.name, tool]], + ), + ); + const overrides: CodexDynamicToolSnapshotOverrides = { + base: CODEX_DYNAMIC_TOOL_BASE_SNAPSHOT, + replace, + }; + return { ...file, content: `${JSON.stringify(overrides, null, 2)}\n` }; + }); +} + +async function formatPromptSnapshotFiles( + files: PromptSnapshotFile[], +): Promise { const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-snapshots-")); try { await writeSnapshotFiles(tmpRoot, files); @@ -72,6 +121,56 @@ export async function createFormattedPromptSnapshotFiles(): Promise { + const files = await createHappyPathPromptSnapshotFiles(); + return await formatPromptSnapshotFiles(factorCodexDynamicToolSnapshotFiles(files)); +} + +/** Materialize one complete, formatted Codex dynamic-tool catalog for human review. */ +export async function materializeCodexDynamicToolSnapshot(scenario: string): Promise { + if (!/^[a-z][a-z0-9-]*$/u.test(scenario)) { + throw new Error(`Invalid Codex dynamic-tool snapshot scenario: ${scenario}`); + } + const snapshotPath = path.join( + CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, + `${CODEX_DYNAMIC_TOOL_SNAPSHOT_PREFIX}${scenario}.json`, + ); + const snapshot = JSON.parse(await fs.readFile(path.resolve(repoRoot, snapshotPath), "utf8")) as + | CodexDynamicToolSnapshotSpec[] + | CodexDynamicToolSnapshotOverrides; + let tools: CodexDynamicToolSnapshotSpec[]; + if (Array.isArray(snapshot)) { + tools = snapshot; + } else { + if (snapshot.base !== CODEX_DYNAMIC_TOOL_BASE_SNAPSHOT) { + throw new Error(`Unsupported Codex dynamic-tool snapshot base: ${snapshot.base}`); + } + const basePath = path.join(CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR, snapshot.base); + const base = JSON.parse(await fs.readFile(path.resolve(repoRoot, basePath), "utf8")) as + | CodexDynamicToolSnapshotSpec[] + | CodexDynamicToolSnapshotOverrides; + if (!Array.isArray(base)) { + throw new Error("The canonical Codex dynamic-tool snapshot must contain complete tools"); + } + const baseNames = new Set(base.map((tool) => tool.name)); + for (const [name, replacement] of Object.entries(snapshot.replace)) { + if (!baseNames.has(name) || replacement.name !== name) { + throw new Error(`Invalid Codex dynamic-tool snapshot replacement: ${name}`); + } + } + tools = base.map((tool) => + Object.hasOwn(snapshot.replace, tool.name) ? snapshot.replace[tool.name]! : tool, + ); + } + const [formatted] = await formatPromptSnapshotFiles([ + { path: snapshotPath, content: `${JSON.stringify(tools, null, 2)}\n` }, + ]); + if (!formatted) { + throw new Error(`Failed to materialize Codex dynamic-tool snapshot: ${scenario}`); + } + return formatted.content; +} + async function writeSnapshots() { const files = await createFormattedPromptSnapshotFiles(); await fs.mkdir(path.resolve(repoRoot, CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR), { @@ -117,6 +216,18 @@ async function checkSnapshots() { } async function runPromptSnapshotGenerator(argv = process.argv.slice(2)) { + if (argv[0] === "--materialize") { + const scenario = argv[1]; + if (!scenario || argv.length !== 2) { + console.error( + "Usage: node --import tsx scripts/generate-prompt-snapshots.ts --materialize ", + ); + process.exitCode = 2; + return; + } + process.stdout.write(await materializeCodexDynamicToolSnapshot(scenario)); + return; + } const mode = argv.includes("--write") ? "write" : argv.includes("--check") ? "check" : undefined; if (!mode) { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md index 42da5eeef382..73516bb7f178 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/README.md @@ -14,6 +14,16 @@ The workspace bootstrap simulation includes dummy workspace contents so prompt r The tool catalog is pinned to the canonical happy-path OpenClaw tools so optional locally installed plugin tools do not create fixture churn. +The Telegram JSON is the complete shared tool catalog. Discord and heartbeat JSON fixtures contain readable, complete replacements for their changed top-level tools or namespaces; their `base` field points to the Telegram catalog. + +Materialize the complete, formatted tool catalog for a scenario with: + +```sh +node --import tsx scripts/generate-prompt-snapshots.ts --materialize discord-group +``` + +Replace `discord-group` with `heartbeat-turn` to inspect the complete heartbeat catalog. + The Codex model prompt fixture is generated from the same Codex model catalog/cache shape that the Codex runtime uses for remote model metadata. Regenerate it from Codex's runtime cache or, when present, a local Codex checkout with: ```sh diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index bbd1da944c33..6939021c267a 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -1,1628 +1,127 @@ -[ - { - "description": "List configured agent ids with name/model/runtime metadata, allowed as `sessions_spawn(runtime:\"subagent\")` targets.", - "inputSchema": { - "additionalProperties": false, - "properties": {}, - "required": [], - "type": "object" - }, - "name": "agents_list", - "type": "function" - }, - { - "description": "Send/manage channel messages. Supports actions: send.", - "inputSchema": { - "properties": { - "accountId": { - "type": "string" - }, - "action": { - "description": "Select one action. For action=\"send\", provide message or another send payload; fields for other actions do not count as send content.", - "enum": ["send"], - "type": "string" - }, - "asDocument": { - "description": "Alias for forceDocument.", - "type": "boolean" - }, - "asVoice": { - "type": "boolean" - }, - "attachments": { - "description": "Attachments; each uses media.", - "items": { - "properties": { - "media": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "enum": ["image", "audio", "video", "file"], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "buffer": { - "description": "Base64/data-URL attachment.", - "type": "string" - }, - "caption": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "dryRun": { - "type": "boolean" - }, - "effect": { - "description": "Alias for effectId.", - "type": "string" - }, - "effectId": { - "description": "sendWithEffect id/name.", - "type": "string" - }, - "filename": { - "type": "string" - }, - "forceDocument": { - "description": "Send media as document; no compression.", - "type": "boolean" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "gifPlayback": { - "type": "boolean" - }, - "media": { - "description": "Media URL/path. data: use buffer.", - "type": "string" - }, - "message": { - "description": "Text for action=\"send\". A send needs message or another send payload such as media, attachments, or presentation.", - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "quoteText": { - "description": "Telegram reply quote text.", - "type": "string" - }, - "replyTo": { - "type": "string" - }, - "silent": { - "type": "boolean" - }, - "target": { - "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", - "type": "string" - }, - "targets": { - "items": { - "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", +{ + "base": "codex-dynamic-tools.telegram-direct.json", + "replace": { + "sessions_spawn": { + "description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work. Run result returns; session output stays thread.", + "inputSchema": { + "properties": { + "agentId": { "type": "string" }, - "type": "array" - }, - "threadId": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "message", - "type": "function" - }, - { - "description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work. Run result returns; session output stays thread.", - "inputSchema": { - "properties": { - "agentId": { - "type": "string" - }, - "attachAs": { - "description": "Attachment mount hint; unavailable with visible=true.", - "properties": { - "mountPath": { - "type": "string" - } - }, - "type": "object" - }, - "attachments": { - "description": "Inline snapshots; unavailable with visible=true.", - "items": { + "attachAs": { + "description": "Attachment mount hint; unavailable with visible=true.", "properties": { - "content": { - "type": "string" - }, - "encoding": { - "enum": ["utf8", "base64"], - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { + "mountPath": { "type": "string" } }, - "required": ["name", "content"], "type": "object" }, - "maxItems": 50, - "type": "array" - }, - "cleanup": { - "description": "Hidden session cleanup; visible=true always keeps the session.", - "enum": ["delete", "keep"], - "type": "string" - }, - "context": { - "description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.", - "enum": ["isolated", "fork"], - "type": "string" - }, - "cwd": { - "type": "string" - }, - "label": { - "description": "Short task title shown in UI lists; name the work, not the agent.", - "type": "string" - }, - "lightContext": { - "description": "Light bootstrap; subagent only; unavailable with visible=true.", - "type": "boolean" - }, - "mode": { - "description": "\"run\" one-shot; \"session\" persistent/thread-bound. Omit with visible=true.", - "enum": ["run", "session"], - "type": "string" - }, - "model": { - "type": "string" - }, - "runtime": { - "description": "Runtime; visible=true requires \"subagent\".", - "enum": ["subagent"], - "type": "string" - }, - "runTimeoutSeconds": { - "description": "Per-run timeout in seconds; overrides the configured subagent default. Zero disables the timeout.", - "minimum": 0, - "type": "integer" - }, - "sandbox": { - "description": "\"inherit\" parent sandbox policy; \"require\" fails unless child is sandboxed.", - "enum": ["inherit", "require"], - "type": "string" - }, - "task": { - "type": "string" - }, - "taskName": { - "description": "Stable later-target alias; starts lowercase letter; then lowercase/digit/_/-.", - "type": "string" - }, - "thinking": { - "description": "Thinking override; unavailable with visible=true.", - "type": "string" - }, - "thread": { - "description": "Bind new chat thread when supported; true defaults mode=\"session\"; unavailable with visible=true.", - "type": "boolean" - }, - "visible": { - "description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.", - "type": "boolean" - }, - "worktree": { - "description": "Visible session worktree", - "type": "boolean" - }, - "worktreeBaseRef": { - "description": "Worktree base ref", - "type": "string" - }, - "worktreeName": { - "description": "Worktree name", - "type": "string" - } - }, - "required": ["task"], - "type": "object" - }, - "name": "sessions_spawn", - "type": "function" - }, - { - "description": "", - "name": "openclaw", - "tools": [ - { - "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", - "inputSchema": { - "additionalProperties": true, - "properties": { - "action": { - "enum": [ - "status", - "list", - "get", - "add", - "update", - "remove", - "run", - "runs", - "next_check", - "wake" - ], - "type": "string" - }, - "agentId": { - "description": "List filter for `action: \"list\"`; wake target override for `action: \"wake\"` (defaults to the calling agent when omitted on wake)", - "type": "string" - }, - "contextMessages": { - "maximum": 10, - "minimum": 0, - "type": "integer" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "id": { - "type": "string" - }, - "in": { - "description": "Relative duration for action=\"next_check\" (for example, \"15m\")", - "type": "string" - }, - "includeDisabled": { - "type": "boolean" - }, - "job": { - "additionalProperties": true, + "attachments": { + "description": "Inline snapshots; unavailable with visible=true.", + "items": { "properties": { - "agentId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Agent id, or null to keep it unset" - }, - "declarationKey": { - "description": "Idempotent declaration key.", - "maxLength": 200, - "minLength": 1, + "content": { "type": "string" }, - "deleteAfterRun": { - "description": "Delete after first run", - "type": "boolean" - }, - "delivery": { - "additionalProperties": true, - "properties": { - "accountId": { - "description": "Delivery account", - "type": "string" - }, - "bestEffort": { - "type": "boolean" - }, - "channel": { - "description": "Delivery channel", - "type": "string" - }, - "failureDestination": { - "additionalProperties": true, - "properties": { - "accountId": { - "description": "Failure delivery account", - "type": "string" - }, - "channel": { - "description": "Failure delivery channel", - "type": "string" - }, - "mode": { - "anyOf": [ - { - "const": "announce", - "type": "string" - }, - { - "const": "webhook", - "type": "string" - } - ] - }, - "to": { - "description": "Failure delivery target", - "type": "string" - } - }, - "type": "object" - }, - "mode": { - "description": "Delivery mode", - "enum": ["none", "announce", "webhook"], - "type": "string" - }, - "threadId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - } - ], - "description": "Thread/topic id" - }, - "to": { - "description": "Delivery target", - "type": "string" - } - }, - "type": "object" - }, - "description": { - "description": "Human description", + "encoding": { + "enum": ["utf8", "base64"], "type": "string" }, - "displayName": { - "description": "Human-readable declarative job label", - "maxLength": 200, + "mimeType": { "type": "string" }, - "enabled": { - "type": "boolean" - }, - "failureAlert": { - "additionalProperties": true, - "description": "Failure alert; false disables.", - "properties": { - "accountId": { - "type": "string" - }, - "after": { - "description": "Failures before alert", - "minimum": 1, - "type": "integer" - }, - "channel": { - "description": "Alert channel", - "type": "string" - }, - "cooldownMs": { - "description": "Alert cooldown ms", - "minimum": 0, - "type": "integer" - }, - "includeSkipped": { - "description": "Count skipped runs.", - "type": "boolean" - }, - "mode": { - "enum": ["announce", "webhook"], - "type": "string" - }, - "to": { - "description": "Alert target", - "type": "string" - } - }, - "type": "object" - }, "name": { - "description": "Job name", - "type": "string" - }, - "owner": { - "additionalProperties": false, - "properties": { - "agentId": { - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "type": "object" - }, - "pacing": { - "additionalProperties": false, - "description": "Dynamic-cadence bounds; at least one of min or max is required", - "properties": { - "max": { - "description": "Maximum dynamic delay", - "type": "string" - }, - "min": { - "description": "Minimum dynamic delay", - "type": "string" - } - }, - "type": "object" - }, - "payload": { - "additionalProperties": true, - "properties": { - "allowUnsafeExternalContent": { - "description": "Allow untrusted external content in prompt", - "type": "boolean" - }, - "fallbacks": { - "description": "Fallback models", - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "description": "Payload kind", - "enum": ["systemEvent", "agentTurn", "script"], - "type": "string" - }, - "lightContext": { - "description": "Lightweight bootstrap context (skip full workspace context)", - "type": "boolean" - }, - "message": { - "description": "agentTurn prompt", - "type": "string" - }, - "model": { - "description": "Model override", - "type": "string" - }, - "script": { - "description": "Headless code-mode script", - "type": "string" - }, - "text": { - "description": "systemEvent text", - "type": "string" - }, - "thinking": { - "description": "Thinking override", - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "number" - }, - "toolBudget": { - "description": "Maximum script tool calls", - "minimum": 1, - "type": "integer" - }, - "toolsAllow": { - "description": "Allowed tools", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": true, - "properties": { - "anchorMs": { - "description": "Start anchor ms (kind=every)", - "minimum": 0, - "type": "integer" - }, - "at": { - "description": "ISO-8601 time (kind=at)", - "type": "string" - }, - "batchMs": { - "minimum": 0, - "type": "integer" - }, - "command": { - "description": "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "cwd": { - "description": "Working directory (kind=stream)", - "type": "string" - }, - "everyMs": { - "description": "Interval ms (kind=every)", - "minimum": 1, - "type": "integer" - }, - "expr": { - "description": "Cron wall-time expr; never UTC-convert. Missing tz=Gateway local. Example \"0 18 * * *\", \"Asia/Shanghai\".", - "type": "string" - }, - "kind": { - "description": "Schedule kind", - "enum": ["at", "every", "cron", "stream"], - "type": "string" - }, - "match": { - "description": "Regex source (stream match mode)", - "type": "string" - }, - "maxBatchBytes": { - "minimum": 0, - "type": "integer" - }, - "mode": { - "enum": ["line", "match"], - "type": "string" - }, - "staggerMs": { - "description": "Jitter ms (kind=cron)", - "minimum": 0, - "type": "integer" - }, - "tz": { - "description": "IANA timezone for wall-clock fields; missing=Gateway host local timezone. Example \"Asia/Shanghai\".", - "type": "string" - } - }, - "type": "object" - }, - "sessionKey": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Explicit session key, or null to clear it" - }, - "sessionTarget": { - "description": "main | isolated | current (agentTurn default) | session:", - "type": "string" - }, - "trigger": { - "additionalProperties": false, - "properties": { - "once": { - "type": "boolean" - }, - "script": { - "maxLength": 65536, - "minLength": 1, - "type": "string" - } - }, - "required": ["script"], - "type": "object" - }, - "wakeMode": { - "description": "Wake timing", - "enum": ["now", "next-heartbeat"], "type": "string" } }, + "required": ["name", "content"], "type": "object" }, - "jobId": { - "type": "string" - }, - "limit": { - "description": "Maximum jobs returned by action=\"list\"", - "maximum": 200, - "minimum": 1, - "type": "integer" - }, - "mode": { - "description": "Wake mode for action=\"wake\" (default next-heartbeat)", - "enum": ["now", "next-heartbeat"], - "type": "string" - }, - "offset": { - "description": "Job offset for action=\"list\"; use nextOffset to load the next page", - "minimum": 0, - "type": "integer" - }, - "patch": { - "additionalProperties": true, - "properties": { - "agentId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Agent id, or null to clear it" - }, - "deleteAfterRun": { - "type": "boolean" - }, - "delivery": { - "additionalProperties": true, - "properties": { - "accountId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery account, or null to clear" - }, - "bestEffort": { - "type": "boolean" - }, - "channel": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery channel, or null to clear" - }, - "failureDestination": { - "anyOf": [ - { - "additionalProperties": true, - "properties": { - "accountId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery account, or null to clear" - }, - "channel": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery channel, or null to clear" - }, - "mode": { - "anyOf": [ - { - "const": "announce", - "type": "string" - }, - { - "const": "webhook", - "type": "string" - }, - { - "type": "null" - } - ] - }, - "to": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery target, or null to clear" - } - }, - "type": "object" - }, - { - "type": "null" - } - ], - "description": "Failure destination; null clears." - }, - "mode": { - "description": "Delivery mode", - "enum": ["none", "announce", "webhook"], - "type": "string" - }, - "threadId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Thread/topic id" - }, - "to": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery target, or null to clear" - } - }, - "type": "object" - }, - "description": { - "type": "string" - }, - "displayName": { - "anyOf": [ - { - "maxLength": 200, - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Human-readable label; null clears it" - }, - "enabled": { - "type": "boolean" - }, - "failureAlert": { - "additionalProperties": true, - "description": "Failure alert; false disables.", - "properties": { - "accountId": { - "type": "string" - }, - "after": { - "description": "Failures before alert", - "minimum": 1, - "type": "integer" - }, - "channel": { - "description": "Alert channel", - "type": "string" - }, - "cooldownMs": { - "description": "Alert cooldown ms", - "minimum": 0, - "type": "integer" - }, - "includeSkipped": { - "description": "Count skipped runs.", - "type": "boolean" - }, - "mode": { - "enum": ["announce", "webhook"], - "type": "string" - }, - "to": { - "description": "Alert target", - "type": "string" - } - }, - "type": "object" - }, - "name": { - "description": "Job name", - "type": "string" - }, - "pacing": { - "anyOf": [ - { - "additionalProperties": false, - "description": "Dynamic-cadence bounds; at least one of min or max is required", - "properties": { - "max": { - "description": "Maximum dynamic delay", - "type": "string" - }, - "min": { - "description": "Minimum dynamic delay", - "type": "string" - } - }, - "type": "object" - }, - { - "type": "null" - } - ] - }, - "payload": { - "additionalProperties": true, - "properties": { - "allowUnsafeExternalContent": { - "description": "Allow untrusted external content in prompt", - "type": "boolean" - }, - "fallbacks": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "description": "Fallback models, or null to clear" - }, - "kind": { - "description": "Payload kind", - "enum": ["systemEvent", "agentTurn", "script"], - "type": "string" - }, - "lightContext": { - "description": "Lightweight bootstrap context (skip full workspace context)", - "type": "boolean" - }, - "message": { - "description": "agentTurn prompt", - "type": "string" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Model override, or null to clear" - }, - "script": { - "description": "Headless code-mode script", - "type": "string" - }, - "text": { - "description": "systemEvent text", - "type": "string" - }, - "thinking": { - "description": "Thinking override", - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "number" - }, - "toolBudget": { - "description": "Maximum script tool calls", - "minimum": 1, - "type": "integer" - }, - "toolsAllow": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "description": "Allowed tool ids, or null to clear" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": true, - "properties": { - "anchorMs": { - "description": "Start anchor ms (kind=every)", - "minimum": 0, - "type": "integer" - }, - "at": { - "description": "ISO-8601 time (kind=at)", - "type": "string" - }, - "batchMs": { - "minimum": 0, - "type": "integer" - }, - "command": { - "description": "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "cwd": { - "description": "Working directory (kind=stream)", - "type": "string" - }, - "everyMs": { - "description": "Interval ms (kind=every)", - "minimum": 1, - "type": "integer" - }, - "expr": { - "description": "Cron wall-time expr; never UTC-convert. Missing tz=Gateway local. Example \"0 18 * * *\", \"Asia/Shanghai\".", - "type": "string" - }, - "kind": { - "description": "Schedule kind", - "enum": ["at", "every", "cron", "stream"], - "type": "string" - }, - "match": { - "description": "Regex source (stream match mode)", - "type": "string" - }, - "maxBatchBytes": { - "minimum": 0, - "type": "integer" - }, - "mode": { - "enum": ["line", "match"], - "type": "string" - }, - "staggerMs": { - "description": "Jitter ms (kind=cron)", - "minimum": 0, - "type": "integer" - }, - "tz": { - "description": "IANA timezone for wall-clock fields; missing=Gateway host local timezone. Example \"Asia/Shanghai\".", - "type": "string" - } - }, - "type": "object" - }, - "sessionKey": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Explicit session key, or null to clear it" - }, - "sessionTarget": { - "description": "main | isolated | current (agentTurn default) | session:", - "type": "string" - }, - "trigger": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "once": { - "type": "boolean" - }, - "script": { - "maxLength": 65536, - "minLength": 1, - "type": "string" - } - }, - "required": ["script"], - "type": "object" - }, - { - "type": "null" - } - ] - }, - "wakeMode": { - "enum": ["now", "next-heartbeat"], - "type": "string" - } - }, - "type": "object" - }, - "runMode": { - "description": "Run mode for action=\"run\": omitted defaults to \"due\"; use \"force\" to trigger now.", - "enum": ["due", "force"], - "type": "string" - }, - "sessionKey": { - "description": "Wake target override for `action: \"wake\"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.", - "type": "string" - }, - "text": { - "description": "systemEvent text for action=\"wake\"", - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - } + "maxItems": 50, + "type": "array" }, - "required": ["action"], - "type": "object" + "cleanup": { + "description": "Hidden session cleanup; visible=true always keeps the session.", + "enum": ["delete", "keep"], + "type": "string" + }, + "context": { + "description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.", + "enum": ["isolated", "fork"], + "type": "string" + }, + "cwd": { + "type": "string" + }, + "label": { + "description": "Short task title shown in UI lists; name the work, not the agent.", + "type": "string" + }, + "lightContext": { + "description": "Light bootstrap; subagent only; unavailable with visible=true.", + "type": "boolean" + }, + "mode": { + "description": "\"run\" one-shot; \"session\" persistent/thread-bound. Omit with visible=true.", + "enum": ["run", "session"], + "type": "string" + }, + "model": { + "type": "string" + }, + "runtime": { + "description": "Runtime; visible=true requires \"subagent\".", + "enum": ["subagent"], + "type": "string" + }, + "runTimeoutSeconds": { + "description": "Per-run timeout in seconds; overrides the configured subagent default. Zero disables the timeout.", + "minimum": 0, + "type": "integer" + }, + "sandbox": { + "description": "\"inherit\" parent sandbox policy; \"require\" fails unless child is sandboxed.", + "enum": ["inherit", "require"], + "type": "string" + }, + "task": { + "type": "string" + }, + "taskName": { + "description": "Stable later-target alias; starts lowercase letter; then lowercase/digit/_/-.", + "type": "string" + }, + "thinking": { + "description": "Thinking override; unavailable with visible=true.", + "type": "string" + }, + "thread": { + "description": "Bind new chat thread when supported; true defaults mode=\"session\"; unavailable with visible=true.", + "type": "boolean" + }, + "visible": { + "description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.", + "type": "boolean" + }, + "worktree": { + "description": "Visible session worktree", + "type": "boolean" + }, + "worktreeBaseRef": { + "description": "Worktree base ref", + "type": "string" + }, + "worktreeName": { + "description": "Worktree name", + "type": "string" + } }, - "name": "automations", - "type": "function" + "required": ["task"], + "type": "object" }, - { - "deferLoading": true, - "description": "Read gateway config + schema. Writes/restart: use openclaw tool.", - "inputSchema": { - "properties": { - "action": { - "enum": ["config.get", "config.schema.lookup"], - "type": "string" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "path": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "gateway", - "type": "function" - }, - { - "deferLoading": true, - "description": "Paired nodes: status/list with active-computer presence; pass node to describe/control. Pairing lifecycle (pending/approve/reject), notify, camera_snap/camera_list/camera_clip (with audio), photos_latest, screen_snapshot, screen_record video, location_get, notifications_list + notifications_action (open/dismiss/reply), device_status/device_info/device_permissions/device_health, executable lookup (which + bins), generic invoke. Files: file_fetch.", - "inputSchema": { - "properties": { - "action": { - "enum": [ - "status", - "describe", - "pending", - "approve", - "reject", - "notify", - "camera_snap", - "camera_list", - "camera_clip", - "photos_latest", - "screen_record", - "screen_snapshot", - "location_get", - "notifications_list", - "notifications_action", - "device_status", - "device_info", - "device_permissions", - "device_health", - "which", - "invoke" - ], - "type": "string" - }, - "bins": { - "description": "which: executable names to resolve on the selected node.", - "items": { - "minLength": 1, - "type": "string" - }, - "maxItems": 64, - "minItems": 1, - "type": "array" - }, - "body": { - "type": "string" - }, - "delayMs": { - "minimum": 0, - "type": "integer" - }, - "delivery": { - "enum": ["system", "overlay", "auto"], - "type": "string" - }, - "desiredAccuracy": { - "enum": ["coarse", "balanced", "precise"], - "type": "string" - }, - "deviceId": { - "type": "string" - }, - "duration": { - "type": "string" - }, - "durationMs": { - "maximum": 300000, - "minimum": 1, - "type": "integer" - }, - "facing": { - "description": "camera_snap: front/back/both; camera_clip: front/back only.", - "enum": ["front", "back", "both"], - "type": "string" - }, - "fps": { - "exclusiveMinimum": 0, - "type": "number" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "includeAudio": { - "type": "boolean" - }, - "invokeCommand": { - "type": "string" - }, - "invokeParamsJson": { - "type": "string" - }, - "invokeTimeoutMs": { - "minimum": 1, - "type": "integer" - }, - "limit": { - "maximum": 20, - "minimum": 1, - "type": "integer" - }, - "locationTimeoutMs": { - "minimum": 1, - "type": "integer" - }, - "maxAgeMs": { - "minimum": 0, - "type": "integer" - }, - "maxWidth": { - "minimum": 1, - "type": "integer" - }, - "node": { - "description": "Node ID, name, or IP. Required for describe and node-targeted actions; use status to discover nodes.", - "type": "string" - }, - "notificationAction": { - "enum": ["open", "dismiss", "reply"], - "type": "string" - }, - "notificationKey": { - "type": "string" - }, - "notificationReplyText": { - "type": "string" - }, - "outPath": { - "type": "string" - }, - "priority": { - "enum": ["passive", "active", "timeSensitive"], - "type": "string" - }, - "quality": { - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "requestId": { - "type": "string" - }, - "screenIndex": { - "minimum": 0, - "type": "integer" - }, - "sound": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - }, - "title": { - "type": "string" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "nodes", - "type": "function" - }, - { - "deferLoading": true, - "description": "Show visible-session model/usage/time/cost/tasks. `sessionKey=\"current\"` for current; UI labels are not keys. `model` overrides; `model=default` resets. Use for active model/session questions.", - "inputSchema": { - "properties": { - "changesSince": { - "minimum": 0, - "type": "integer" - }, - "model": { - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "type": "object" - }, - "name": "session_status", - "type": "function" - }, - { - "deferLoading": true, - "description": "Read sanitized visible-session history. Before reply/debug/resume. Supports limit, offset, search-result sessionId/messageId anchors, and tool messages.", - "inputSchema": { - "properties": { - "includeTools": { - "type": "boolean" - }, - "limit": { - "minimum": 1, - "type": "integer" - }, - "messageId": { - "minLength": 1, - "type": "string" - }, - "offset": { - "minimum": 0, - "type": "integer" - }, - "sessionId": { - "minLength": 1, - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "required": ["sessionKey"], - "type": "object" - }, - "name": "sessions_history", - "type": "function" - }, - { - "deferLoading": true, - "description": "List visible sessions; filter kind/label/agentId/search/activity/archive. Preview recent messages inline via includeLastMessage/messageLimit; includeDerivedTitles adds derived titles. Use before history/send target selection.", - "inputSchema": { - "properties": { - "activeMinutes": { - "minimum": 1, - "type": "integer" - }, - "agentId": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "archived": { - "type": "boolean" - }, - "includeDerivedTitles": { - "type": "boolean" - }, - "includeLastMessage": { - "type": "boolean" - }, - "kinds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "label": { - "minLength": 1, - "type": "string" - }, - "limit": { - "minimum": 1, - "type": "integer" - }, - "messageLimit": { - "minimum": 0, - "type": "integer" - }, - "search": { - "minLength": 1, - "type": "string" - } - }, - "type": "object" - }, - "name": "sessions_list", - "type": "function" - }, - { - "deferLoading": true, - "description": "Search your own past sessions for matching user and assistant text. Follow up with sessions_history using a returned sessionKey, sessionId, and messageId for neighboring context.", - "inputSchema": { - "properties": { - "limit": { - "maximum": 25, - "minimum": 1, - "type": "integer" - }, - "query": { - "maxLength": 4096, - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "sessions_search", - "type": "function" - }, - { - "deferLoading": true, - "description": "Run a visible session on this Gateway by sessionKey/label, or a configured local agent by agentId; sessionKey wins redundant label. A session identifies model context, not an external address; its reply may still announce through established delivery context. For an exact external destination, use `conversations_list` plus `conversations_send`/`conversations_turn`, or `message` with an explicit channel and target. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available. watch:true: notice arrives when others later change target session.", - "inputSchema": { - "properties": { - "agentId": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "label": { - "maxLength": 512, - "minLength": 1, - "type": "string" - }, - "message": { - "type": "string" - }, - "sessionKey": { - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "integer" - }, - "watch": { - "type": "boolean" - } - }, - "required": ["message"], - "type": "object" - }, - "name": "sessions_send", - "type": "function" - }, - { - "deferLoading": true, - "description": "Background work: subagents, media gen, automation runs. list/cancel.", - "inputSchema": { - "properties": { - "action": { - "enum": ["list", "cancel"], - "type": "string" - }, - "recentMinutes": { - "minimum": 1, - "type": "integer" - }, - "taskId": { - "description": "Task id", - "type": "string" - } - }, - "type": "object" - }, - "name": "subagents", - "type": "function" - }, - { - "deferLoading": true, - "description": "Convert text to spoken audio (TTS) with the configured voice provider. Only explicit voice/speech/TTS intent or active TTS config; never ordinary text reply. Audio auto-delivered. After success follow reply instructions; no duplicate text/audio.", - "inputSchema": { - "properties": { - "channel": { - "description": "Channel id; output-format hint.", - "type": "string" - }, - "text": { - "description": "Text to speak.", - "type": "string" - }, - "timeoutMs": { - "description": "Provider timeout ms.", - "minimum": 1, - "type": "integer" - } - }, - "required": ["text"], - "type": "object" - }, - "name": "tts", - "type": "function" - }, - { - "deferLoading": true, - "description": "Fetch URL; extract readable markdown/text. Lightweight; no browser automation.", - "inputSchema": { - "properties": { - "extractMode": { - "default": "markdown", - "description": "Extract as markdown/text.", - "enum": ["markdown", "text"], - "type": "string" - }, - "maxChars": { - "description": "Max chars returned; truncates.", - "minimum": 100, - "type": "integer" - }, - "url": { - "description": "HTTP(S) URL.", - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "web_fetch", - "type": "function" - }, - { - "deferLoading": true, - "description": "Search current web; normalized provider results. Supports freshness and date-range filters (freshness, date_after/date_before) and domain filtering (domain_filter).", - "inputSchema": { - "properties": { - "count": { - "description": "Result count.", - "maximum": 10, - "minimum": 1, - "type": "number" - }, - "country": { - "description": "2-letter country code.", - "type": "string" - }, - "date_after": { - "description": "Published after YYYY-MM-DD.", - "type": "string" - }, - "date_before": { - "description": "Published before YYYY-MM-DD.", - "type": "string" - }, - "domain_filter": { - "description": "Perplexity domain filter.", - "items": { - "type": "string" - }, - "type": "array" - }, - "freshness": { - "description": "Time filter: day/week/month/year.", - "type": "string" - }, - "language": { - "description": "ISO 639-1 language.", - "type": "string" - }, - "max_tokens": { - "description": "Perplexity total token budget.", - "maximum": 1000000, - "minimum": 1, - "type": "number" - }, - "max_tokens_per_page": { - "description": "Perplexity tokens per page.", - "minimum": 1, - "type": "number" - }, - "query": { - "description": "Search query.", - "type": "string" - }, - "search_lang": { - "description": "Brave result language.", - "type": "string" - }, - "ui_lang": { - "description": "Brave UI locale.", - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "web_search", - "type": "function" - } - ], - "type": "namespace" - }, - { - "description": "", - "name": "openclaw_direct", - "tools": [ - { - "description": "End turn after subagent spawn; results arrive next message.", - "inputSchema": { - "properties": { - "message": { - "type": "string" - } - }, - "type": "object" - }, - "name": "sessions_yield", - "type": "function" - } - ], - "type": "namespace" + "name": "sessions_spawn", + "type": "function" + } } -] +} diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 5661f7885102..435782732f3a 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -1,1663 +1,64 @@ -[ - { - "description": "List configured agent ids with name/model/runtime metadata, allowed as `sessions_spawn(runtime:\"subagent\")` targets.", - "inputSchema": { - "additionalProperties": false, - "properties": {}, - "required": [], - "type": "object" - }, - "name": "agents_list", - "type": "function" - }, - { - "description": "Send/manage channel messages. Supports actions: send.", - "inputSchema": { - "properties": { - "accountId": { - "type": "string" - }, - "action": { - "description": "Select one action. For action=\"send\", provide message or another send payload; fields for other actions do not count as send content.", - "enum": ["send"], - "type": "string" - }, - "asDocument": { - "description": "Alias for forceDocument.", - "type": "boolean" - }, - "asVoice": { - "type": "boolean" - }, - "attachments": { - "description": "Attachments; each uses media.", - "items": { +{ + "base": "codex-dynamic-tools.telegram-direct.json", + "replace": { + "openclaw_direct": { + "description": "", + "name": "openclaw_direct", + "tools": [ + { + "description": "Record heartbeat result. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only; manage recurring tasks with cron.", + "inputSchema": { + "additionalProperties": false, "properties": { - "media": { + "nextCheck": { "type": "string" }, - "mimeType": { + "notificationText": { "type": "string" }, - "name": { + "notify": { + "type": "boolean" + }, + "outcome": { + "enum": ["no_change", "progress", "done", "blocked", "needs_attention"], "type": "string" }, - "type": { - "enum": ["image", "audio", "video", "file"], + "priority": { + "enum": ["low", "normal", "high"], + "type": "string" + }, + "reason": { + "type": "string" + }, + "scratch": { + "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": ["outcome", "notify", "summary"], + "type": "object" + }, + "name": "heartbeat_respond", + "type": "function" + }, + { + "description": "End turn after subagent spawn; results arrive next message.", + "inputSchema": { + "properties": { + "message": { "type": "string" } }, "type": "object" }, - "type": "array" - }, - "buffer": { - "description": "Base64/data-URL attachment.", - "type": "string" - }, - "caption": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "dryRun": { - "type": "boolean" - }, - "effect": { - "description": "Alias for effectId.", - "type": "string" - }, - "effectId": { - "description": "sendWithEffect id/name.", - "type": "string" - }, - "filename": { - "type": "string" - }, - "forceDocument": { - "description": "Send media as document; no compression.", - "type": "boolean" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "gifPlayback": { - "type": "boolean" - }, - "media": { - "description": "Media URL/path. data: use buffer.", - "type": "string" - }, - "message": { - "description": "Text for action=\"send\". A send needs message or another send payload such as media, attachments, or presentation.", - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "quoteText": { - "description": "Telegram reply quote text.", - "type": "string" - }, - "replyTo": { - "type": "string" - }, - "silent": { - "type": "boolean" - }, - "target": { - "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", - "type": "string" - }, - "targets": { - "items": { - "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", - "type": "string" - }, - "type": "array" - }, - "threadId": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" + "name": "sessions_yield", + "type": "function" } - }, - "required": ["action"], - "type": "object" - }, - "name": "message", - "type": "function" - }, - { - "description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work while run result returns.", - "inputSchema": { - "properties": { - "agentId": { - "type": "string" - }, - "attachAs": { - "description": "Attachment mount hint; unavailable with visible=true.", - "properties": { - "mountPath": { - "type": "string" - } - }, - "type": "object" - }, - "attachments": { - "description": "Inline snapshots; unavailable with visible=true.", - "items": { - "properties": { - "content": { - "type": "string" - }, - "encoding": { - "enum": ["utf8", "base64"], - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["name", "content"], - "type": "object" - }, - "maxItems": 50, - "type": "array" - }, - "cleanup": { - "description": "Hidden session cleanup; visible=true always keeps the session.", - "enum": ["delete", "keep"], - "type": "string" - }, - "context": { - "description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.", - "enum": ["isolated", "fork"], - "type": "string" - }, - "cwd": { - "type": "string" - }, - "label": { - "description": "Short task title shown in UI lists; name the work, not the agent.", - "type": "string" - }, - "lightContext": { - "description": "Light bootstrap; subagent only; unavailable with visible=true.", - "type": "boolean" - }, - "mode": { - "description": "\"run\" one-shot. Omit with visible=true; visible sessions are persistent.", - "enum": ["run"], - "type": "string" - }, - "model": { - "type": "string" - }, - "runtime": { - "description": "Runtime; visible=true requires \"subagent\".", - "enum": ["subagent"], - "type": "string" - }, - "runTimeoutSeconds": { - "description": "Per-run timeout in seconds; overrides the configured subagent default. Zero disables the timeout.", - "minimum": 0, - "type": "integer" - }, - "sandbox": { - "description": "\"inherit\" parent sandbox policy; \"require\" fails unless child is sandboxed.", - "enum": ["inherit", "require"], - "type": "string" - }, - "task": { - "type": "string" - }, - "taskName": { - "description": "Stable later-target alias; starts lowercase letter; then lowercase/digit/_/-.", - "type": "string" - }, - "thinking": { - "description": "Thinking override; unavailable with visible=true.", - "type": "string" - }, - "visible": { - "description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.", - "type": "boolean" - }, - "worktree": { - "description": "Visible session worktree", - "type": "boolean" - }, - "worktreeBaseRef": { - "description": "Worktree base ref", - "type": "string" - }, - "worktreeName": { - "description": "Worktree name", - "type": "string" - } - }, - "required": ["task"], - "type": "object" - }, - "name": "sessions_spawn", - "type": "function" - }, - { - "description": "", - "name": "openclaw", - "tools": [ - { - "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", - "inputSchema": { - "additionalProperties": true, - "properties": { - "action": { - "enum": [ - "status", - "list", - "get", - "add", - "update", - "remove", - "run", - "runs", - "next_check", - "wake" - ], - "type": "string" - }, - "agentId": { - "description": "List filter for `action: \"list\"`; wake target override for `action: \"wake\"` (defaults to the calling agent when omitted on wake)", - "type": "string" - }, - "contextMessages": { - "maximum": 10, - "minimum": 0, - "type": "integer" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "id": { - "type": "string" - }, - "in": { - "description": "Relative duration for action=\"next_check\" (for example, \"15m\")", - "type": "string" - }, - "includeDisabled": { - "type": "boolean" - }, - "job": { - "additionalProperties": true, - "properties": { - "agentId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Agent id, or null to keep it unset" - }, - "declarationKey": { - "description": "Idempotent declaration key.", - "maxLength": 200, - "minLength": 1, - "type": "string" - }, - "deleteAfterRun": { - "description": "Delete after first run", - "type": "boolean" - }, - "delivery": { - "additionalProperties": true, - "properties": { - "accountId": { - "description": "Delivery account", - "type": "string" - }, - "bestEffort": { - "type": "boolean" - }, - "channel": { - "description": "Delivery channel", - "type": "string" - }, - "failureDestination": { - "additionalProperties": true, - "properties": { - "accountId": { - "description": "Failure delivery account", - "type": "string" - }, - "channel": { - "description": "Failure delivery channel", - "type": "string" - }, - "mode": { - "anyOf": [ - { - "const": "announce", - "type": "string" - }, - { - "const": "webhook", - "type": "string" - } - ] - }, - "to": { - "description": "Failure delivery target", - "type": "string" - } - }, - "type": "object" - }, - "mode": { - "description": "Delivery mode", - "enum": ["none", "announce", "webhook"], - "type": "string" - }, - "threadId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - } - ], - "description": "Thread/topic id" - }, - "to": { - "description": "Delivery target", - "type": "string" - } - }, - "type": "object" - }, - "description": { - "description": "Human description", - "type": "string" - }, - "displayName": { - "description": "Human-readable declarative job label", - "maxLength": 200, - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "failureAlert": { - "additionalProperties": true, - "description": "Failure alert; false disables.", - "properties": { - "accountId": { - "type": "string" - }, - "after": { - "description": "Failures before alert", - "minimum": 1, - "type": "integer" - }, - "channel": { - "description": "Alert channel", - "type": "string" - }, - "cooldownMs": { - "description": "Alert cooldown ms", - "minimum": 0, - "type": "integer" - }, - "includeSkipped": { - "description": "Count skipped runs.", - "type": "boolean" - }, - "mode": { - "enum": ["announce", "webhook"], - "type": "string" - }, - "to": { - "description": "Alert target", - "type": "string" - } - }, - "type": "object" - }, - "name": { - "description": "Job name", - "type": "string" - }, - "owner": { - "additionalProperties": false, - "properties": { - "agentId": { - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "type": "object" - }, - "pacing": { - "additionalProperties": false, - "description": "Dynamic-cadence bounds; at least one of min or max is required", - "properties": { - "max": { - "description": "Maximum dynamic delay", - "type": "string" - }, - "min": { - "description": "Minimum dynamic delay", - "type": "string" - } - }, - "type": "object" - }, - "payload": { - "additionalProperties": true, - "properties": { - "allowUnsafeExternalContent": { - "description": "Allow untrusted external content in prompt", - "type": "boolean" - }, - "fallbacks": { - "description": "Fallback models", - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "description": "Payload kind", - "enum": ["systemEvent", "agentTurn", "script"], - "type": "string" - }, - "lightContext": { - "description": "Lightweight bootstrap context (skip full workspace context)", - "type": "boolean" - }, - "message": { - "description": "agentTurn prompt", - "type": "string" - }, - "model": { - "description": "Model override", - "type": "string" - }, - "script": { - "description": "Headless code-mode script", - "type": "string" - }, - "text": { - "description": "systemEvent text", - "type": "string" - }, - "thinking": { - "description": "Thinking override", - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "number" - }, - "toolBudget": { - "description": "Maximum script tool calls", - "minimum": 1, - "type": "integer" - }, - "toolsAllow": { - "description": "Allowed tools", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": true, - "properties": { - "anchorMs": { - "description": "Start anchor ms (kind=every)", - "minimum": 0, - "type": "integer" - }, - "at": { - "description": "ISO-8601 time (kind=at)", - "type": "string" - }, - "batchMs": { - "minimum": 0, - "type": "integer" - }, - "command": { - "description": "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "cwd": { - "description": "Working directory (kind=stream)", - "type": "string" - }, - "everyMs": { - "description": "Interval ms (kind=every)", - "minimum": 1, - "type": "integer" - }, - "expr": { - "description": "Cron wall-time expr; never UTC-convert. Missing tz=Gateway local. Example \"0 18 * * *\", \"Asia/Shanghai\".", - "type": "string" - }, - "kind": { - "description": "Schedule kind", - "enum": ["at", "every", "cron", "stream"], - "type": "string" - }, - "match": { - "description": "Regex source (stream match mode)", - "type": "string" - }, - "maxBatchBytes": { - "minimum": 0, - "type": "integer" - }, - "mode": { - "enum": ["line", "match"], - "type": "string" - }, - "staggerMs": { - "description": "Jitter ms (kind=cron)", - "minimum": 0, - "type": "integer" - }, - "tz": { - "description": "IANA timezone for wall-clock fields; missing=Gateway host local timezone. Example \"Asia/Shanghai\".", - "type": "string" - } - }, - "type": "object" - }, - "sessionKey": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Explicit session key, or null to clear it" - }, - "sessionTarget": { - "description": "main | isolated | current (agentTurn default) | session:", - "type": "string" - }, - "trigger": { - "additionalProperties": false, - "properties": { - "once": { - "type": "boolean" - }, - "script": { - "maxLength": 65536, - "minLength": 1, - "type": "string" - } - }, - "required": ["script"], - "type": "object" - }, - "wakeMode": { - "description": "Wake timing", - "enum": ["now", "next-heartbeat"], - "type": "string" - } - }, - "type": "object" - }, - "jobId": { - "type": "string" - }, - "limit": { - "description": "Maximum jobs returned by action=\"list\"", - "maximum": 200, - "minimum": 1, - "type": "integer" - }, - "mode": { - "description": "Wake mode for action=\"wake\" (default next-heartbeat)", - "enum": ["now", "next-heartbeat"], - "type": "string" - }, - "offset": { - "description": "Job offset for action=\"list\"; use nextOffset to load the next page", - "minimum": 0, - "type": "integer" - }, - "patch": { - "additionalProperties": true, - "properties": { - "agentId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Agent id, or null to clear it" - }, - "deleteAfterRun": { - "type": "boolean" - }, - "delivery": { - "additionalProperties": true, - "properties": { - "accountId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery account, or null to clear" - }, - "bestEffort": { - "type": "boolean" - }, - "channel": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery channel, or null to clear" - }, - "failureDestination": { - "anyOf": [ - { - "additionalProperties": true, - "properties": { - "accountId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery account, or null to clear" - }, - "channel": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery channel, or null to clear" - }, - "mode": { - "anyOf": [ - { - "const": "announce", - "type": "string" - }, - { - "const": "webhook", - "type": "string" - }, - { - "type": "null" - } - ] - }, - "to": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Failure delivery target, or null to clear" - } - }, - "type": "object" - }, - { - "type": "null" - } - ], - "description": "Failure destination; null clears." - }, - "mode": { - "description": "Delivery mode", - "enum": ["none", "announce", "webhook"], - "type": "string" - }, - "threadId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Thread/topic id" - }, - "to": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Delivery target, or null to clear" - } - }, - "type": "object" - }, - "description": { - "type": "string" - }, - "displayName": { - "anyOf": [ - { - "maxLength": 200, - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Human-readable label; null clears it" - }, - "enabled": { - "type": "boolean" - }, - "failureAlert": { - "additionalProperties": true, - "description": "Failure alert; false disables.", - "properties": { - "accountId": { - "type": "string" - }, - "after": { - "description": "Failures before alert", - "minimum": 1, - "type": "integer" - }, - "channel": { - "description": "Alert channel", - "type": "string" - }, - "cooldownMs": { - "description": "Alert cooldown ms", - "minimum": 0, - "type": "integer" - }, - "includeSkipped": { - "description": "Count skipped runs.", - "type": "boolean" - }, - "mode": { - "enum": ["announce", "webhook"], - "type": "string" - }, - "to": { - "description": "Alert target", - "type": "string" - } - }, - "type": "object" - }, - "name": { - "description": "Job name", - "type": "string" - }, - "pacing": { - "anyOf": [ - { - "additionalProperties": false, - "description": "Dynamic-cadence bounds; at least one of min or max is required", - "properties": { - "max": { - "description": "Maximum dynamic delay", - "type": "string" - }, - "min": { - "description": "Minimum dynamic delay", - "type": "string" - } - }, - "type": "object" - }, - { - "type": "null" - } - ] - }, - "payload": { - "additionalProperties": true, - "properties": { - "allowUnsafeExternalContent": { - "description": "Allow untrusted external content in prompt", - "type": "boolean" - }, - "fallbacks": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "description": "Fallback models, or null to clear" - }, - "kind": { - "description": "Payload kind", - "enum": ["systemEvent", "agentTurn", "script"], - "type": "string" - }, - "lightContext": { - "description": "Lightweight bootstrap context (skip full workspace context)", - "type": "boolean" - }, - "message": { - "description": "agentTurn prompt", - "type": "string" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Model override, or null to clear" - }, - "script": { - "description": "Headless code-mode script", - "type": "string" - }, - "text": { - "description": "systemEvent text", - "type": "string" - }, - "thinking": { - "description": "Thinking override", - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "number" - }, - "toolBudget": { - "description": "Maximum script tool calls", - "minimum": 1, - "type": "integer" - }, - "toolsAllow": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "description": "Allowed tool ids, or null to clear" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": true, - "properties": { - "anchorMs": { - "description": "Start anchor ms (kind=every)", - "minimum": 0, - "type": "integer" - }, - "at": { - "description": "ISO-8601 time (kind=at)", - "type": "string" - }, - "batchMs": { - "minimum": 0, - "type": "integer" - }, - "command": { - "description": "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "cwd": { - "description": "Working directory (kind=stream)", - "type": "string" - }, - "everyMs": { - "description": "Interval ms (kind=every)", - "minimum": 1, - "type": "integer" - }, - "expr": { - "description": "Cron wall-time expr; never UTC-convert. Missing tz=Gateway local. Example \"0 18 * * *\", \"Asia/Shanghai\".", - "type": "string" - }, - "kind": { - "description": "Schedule kind", - "enum": ["at", "every", "cron", "stream"], - "type": "string" - }, - "match": { - "description": "Regex source (stream match mode)", - "type": "string" - }, - "maxBatchBytes": { - "minimum": 0, - "type": "integer" - }, - "mode": { - "enum": ["line", "match"], - "type": "string" - }, - "staggerMs": { - "description": "Jitter ms (kind=cron)", - "minimum": 0, - "type": "integer" - }, - "tz": { - "description": "IANA timezone for wall-clock fields; missing=Gateway host local timezone. Example \"Asia/Shanghai\".", - "type": "string" - } - }, - "type": "object" - }, - "sessionKey": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Explicit session key, or null to clear it" - }, - "sessionTarget": { - "description": "main | isolated | current (agentTurn default) | session:", - "type": "string" - }, - "trigger": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "once": { - "type": "boolean" - }, - "script": { - "maxLength": 65536, - "minLength": 1, - "type": "string" - } - }, - "required": ["script"], - "type": "object" - }, - { - "type": "null" - } - ] - }, - "wakeMode": { - "enum": ["now", "next-heartbeat"], - "type": "string" - } - }, - "type": "object" - }, - "runMode": { - "description": "Run mode for action=\"run\": omitted defaults to \"due\"; use \"force\" to trigger now.", - "enum": ["due", "force"], - "type": "string" - }, - "sessionKey": { - "description": "Wake target override for `action: \"wake\"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.", - "type": "string" - }, - "text": { - "description": "systemEvent text for action=\"wake\"", - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "automations", - "type": "function" - }, - { - "deferLoading": true, - "description": "Read gateway config + schema. Writes/restart: use openclaw tool.", - "inputSchema": { - "properties": { - "action": { - "enum": ["config.get", "config.schema.lookup"], - "type": "string" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "path": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "gateway", - "type": "function" - }, - { - "deferLoading": true, - "description": "Paired nodes: status/list with active-computer presence; pass node to describe/control. Pairing lifecycle (pending/approve/reject), notify, camera_snap/camera_list/camera_clip (with audio), photos_latest, screen_snapshot, screen_record video, location_get, notifications_list + notifications_action (open/dismiss/reply), device_status/device_info/device_permissions/device_health, executable lookup (which + bins), generic invoke. Files: file_fetch.", - "inputSchema": { - "properties": { - "action": { - "enum": [ - "status", - "describe", - "pending", - "approve", - "reject", - "notify", - "camera_snap", - "camera_list", - "camera_clip", - "photos_latest", - "screen_record", - "screen_snapshot", - "location_get", - "notifications_list", - "notifications_action", - "device_status", - "device_info", - "device_permissions", - "device_health", - "which", - "invoke" - ], - "type": "string" - }, - "bins": { - "description": "which: executable names to resolve on the selected node.", - "items": { - "minLength": 1, - "type": "string" - }, - "maxItems": 64, - "minItems": 1, - "type": "array" - }, - "body": { - "type": "string" - }, - "delayMs": { - "minimum": 0, - "type": "integer" - }, - "delivery": { - "enum": ["system", "overlay", "auto"], - "type": "string" - }, - "desiredAccuracy": { - "enum": ["coarse", "balanced", "precise"], - "type": "string" - }, - "deviceId": { - "type": "string" - }, - "duration": { - "type": "string" - }, - "durationMs": { - "maximum": 300000, - "minimum": 1, - "type": "integer" - }, - "facing": { - "description": "camera_snap: front/back/both; camera_clip: front/back only.", - "enum": ["front", "back", "both"], - "type": "string" - }, - "fps": { - "exclusiveMinimum": 0, - "type": "number" - }, - "gatewayToken": { - "type": "string" - }, - "gatewayUrl": { - "type": "string" - }, - "includeAudio": { - "type": "boolean" - }, - "invokeCommand": { - "type": "string" - }, - "invokeParamsJson": { - "type": "string" - }, - "invokeTimeoutMs": { - "minimum": 1, - "type": "integer" - }, - "limit": { - "maximum": 20, - "minimum": 1, - "type": "integer" - }, - "locationTimeoutMs": { - "minimum": 1, - "type": "integer" - }, - "maxAgeMs": { - "minimum": 0, - "type": "integer" - }, - "maxWidth": { - "minimum": 1, - "type": "integer" - }, - "node": { - "description": "Node ID, name, or IP. Required for describe and node-targeted actions; use status to discover nodes.", - "type": "string" - }, - "notificationAction": { - "enum": ["open", "dismiss", "reply"], - "type": "string" - }, - "notificationKey": { - "type": "string" - }, - "notificationReplyText": { - "type": "string" - }, - "outPath": { - "type": "string" - }, - "priority": { - "enum": ["passive", "active", "timeSensitive"], - "type": "string" - }, - "quality": { - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "requestId": { - "type": "string" - }, - "screenIndex": { - "minimum": 0, - "type": "integer" - }, - "sound": { - "type": "string" - }, - "timeoutMs": { - "minimum": 1, - "type": "integer" - }, - "title": { - "type": "string" - } - }, - "required": ["action"], - "type": "object" - }, - "name": "nodes", - "type": "function" - }, - { - "deferLoading": true, - "description": "Show visible-session model/usage/time/cost/tasks. `sessionKey=\"current\"` for current; UI labels are not keys. `model` overrides; `model=default` resets. Use for active model/session questions.", - "inputSchema": { - "properties": { - "changesSince": { - "minimum": 0, - "type": "integer" - }, - "model": { - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "type": "object" - }, - "name": "session_status", - "type": "function" - }, - { - "deferLoading": true, - "description": "Read sanitized visible-session history. Before reply/debug/resume. Supports limit, offset, search-result sessionId/messageId anchors, and tool messages.", - "inputSchema": { - "properties": { - "includeTools": { - "type": "boolean" - }, - "limit": { - "minimum": 1, - "type": "integer" - }, - "messageId": { - "minLength": 1, - "type": "string" - }, - "offset": { - "minimum": 0, - "type": "integer" - }, - "sessionId": { - "minLength": 1, - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "required": ["sessionKey"], - "type": "object" - }, - "name": "sessions_history", - "type": "function" - }, - { - "deferLoading": true, - "description": "List visible sessions; filter kind/label/agentId/search/activity/archive. Preview recent messages inline via includeLastMessage/messageLimit; includeDerivedTitles adds derived titles. Use before history/send target selection.", - "inputSchema": { - "properties": { - "activeMinutes": { - "minimum": 1, - "type": "integer" - }, - "agentId": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "archived": { - "type": "boolean" - }, - "includeDerivedTitles": { - "type": "boolean" - }, - "includeLastMessage": { - "type": "boolean" - }, - "kinds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "label": { - "minLength": 1, - "type": "string" - }, - "limit": { - "minimum": 1, - "type": "integer" - }, - "messageLimit": { - "minimum": 0, - "type": "integer" - }, - "search": { - "minLength": 1, - "type": "string" - } - }, - "type": "object" - }, - "name": "sessions_list", - "type": "function" - }, - { - "deferLoading": true, - "description": "Search your own past sessions for matching user and assistant text. Follow up with sessions_history using a returned sessionKey, sessionId, and messageId for neighboring context.", - "inputSchema": { - "properties": { - "limit": { - "maximum": 25, - "minimum": 1, - "type": "integer" - }, - "query": { - "maxLength": 4096, - "type": "string" - }, - "sessionKey": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "sessions_search", - "type": "function" - }, - { - "deferLoading": true, - "description": "Run a visible session on this Gateway by sessionKey/label, or a configured local agent by agentId; sessionKey wins redundant label. A session identifies model context, not an external address; its reply may still announce through established delivery context. For an exact external destination, use `conversations_list` plus `conversations_send`/`conversations_turn`, or `message` with an explicit channel and target. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available. watch:true: notice arrives when others later change target session.", - "inputSchema": { - "properties": { - "agentId": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "label": { - "maxLength": 512, - "minLength": 1, - "type": "string" - }, - "message": { - "type": "string" - }, - "sessionKey": { - "type": "string" - }, - "timeoutSeconds": { - "minimum": 0, - "type": "integer" - }, - "watch": { - "type": "boolean" - } - }, - "required": ["message"], - "type": "object" - }, - "name": "sessions_send", - "type": "function" - }, - { - "deferLoading": true, - "description": "Background work: subagents, media gen, automation runs. list/cancel.", - "inputSchema": { - "properties": { - "action": { - "enum": ["list", "cancel"], - "type": "string" - }, - "recentMinutes": { - "minimum": 1, - "type": "integer" - }, - "taskId": { - "description": "Task id", - "type": "string" - } - }, - "type": "object" - }, - "name": "subagents", - "type": "function" - }, - { - "deferLoading": true, - "description": "Convert text to spoken audio (TTS) with the configured voice provider. Only explicit voice/speech/TTS intent or active TTS config; never ordinary text reply. Audio auto-delivered. After success follow reply instructions; no duplicate text/audio.", - "inputSchema": { - "properties": { - "channel": { - "description": "Channel id; output-format hint.", - "type": "string" - }, - "text": { - "description": "Text to speak.", - "type": "string" - }, - "timeoutMs": { - "description": "Provider timeout ms.", - "minimum": 1, - "type": "integer" - } - }, - "required": ["text"], - "type": "object" - }, - "name": "tts", - "type": "function" - }, - { - "deferLoading": true, - "description": "Fetch URL; extract readable markdown/text. Lightweight; no browser automation.", - "inputSchema": { - "properties": { - "extractMode": { - "default": "markdown", - "description": "Extract as markdown/text.", - "enum": ["markdown", "text"], - "type": "string" - }, - "maxChars": { - "description": "Max chars returned; truncates.", - "minimum": 100, - "type": "integer" - }, - "url": { - "description": "HTTP(S) URL.", - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "web_fetch", - "type": "function" - }, - { - "deferLoading": true, - "description": "Search current web; normalized provider results. Supports freshness and date-range filters (freshness, date_after/date_before) and domain filtering (domain_filter).", - "inputSchema": { - "properties": { - "count": { - "description": "Result count.", - "maximum": 10, - "minimum": 1, - "type": "number" - }, - "country": { - "description": "2-letter country code.", - "type": "string" - }, - "date_after": { - "description": "Published after YYYY-MM-DD.", - "type": "string" - }, - "date_before": { - "description": "Published before YYYY-MM-DD.", - "type": "string" - }, - "domain_filter": { - "description": "Perplexity domain filter.", - "items": { - "type": "string" - }, - "type": "array" - }, - "freshness": { - "description": "Time filter: day/week/month/year.", - "type": "string" - }, - "language": { - "description": "ISO 639-1 language.", - "type": "string" - }, - "max_tokens": { - "description": "Perplexity total token budget.", - "maximum": 1000000, - "minimum": 1, - "type": "number" - }, - "max_tokens_per_page": { - "description": "Perplexity tokens per page.", - "minimum": 1, - "type": "number" - }, - "query": { - "description": "Search query.", - "type": "string" - }, - "search_lang": { - "description": "Brave result language.", - "type": "string" - }, - "ui_lang": { - "description": "Brave UI locale.", - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "web_search", - "type": "function" - } - ], - "type": "namespace" - }, - { - "description": "", - "name": "openclaw_direct", - "tools": [ - { - "description": "Record heartbeat result. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only; manage recurring tasks with cron.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "nextCheck": { - "type": "string" - }, - "notificationText": { - "type": "string" - }, - "notify": { - "type": "boolean" - }, - "outcome": { - "enum": ["no_change", "progress", "done", "blocked", "needs_attention"], - "type": "string" - }, - "priority": { - "enum": ["low", "normal", "high"], - "type": "string" - }, - "reason": { - "type": "string" - }, - "scratch": { - "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", - "type": "string" - }, - "summary": { - "type": "string" - } - }, - "required": ["outcome", "notify", "summary"], - "type": "object" - }, - "name": "heartbeat_respond", - "type": "function" - }, - { - "description": "End turn after subagent spawn; results arrive next message.", - "inputSchema": { - "properties": { - "message": { - "type": "string" - } - }, - "type": "object" - }, - "name": "sessions_yield", - "type": "function" - } - ], - "type": "namespace" + ], + "type": "namespace" + } } -] +} diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 13b017c8a446..940eaab1cedc 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -517,7 +517,7 @@ can you audit whether this prompt path has conflicting silence instructions? ### Tools: Dynamic Tool Catalog -Full JSON: `codex-dynamic-tools.discord-group.json` +Full tool overrides: `codex-dynamic-tools.discord-group.json` (base: `codex-dynamic-tools.telegram-direct.json`) ## Dynamic Tool Names diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 2630efd9c5db..61a6c6539e1d 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -512,7 +512,7 @@ Follow the heartbeat monitor scratch context when provided. Recurring tasks are ### Tools: Dynamic Tool Catalog -Full JSON: `codex-dynamic-tools.heartbeat-turn.json` +Full tool overrides: `codex-dynamic-tools.heartbeat-turn.json` (base: `codex-dynamic-tools.telegram-direct.json`) ## Dynamic Tool Names diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 6689987bb1ea..29fd3fa892bc 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -829,7 +829,9 @@ function renderModelBoundPromptLayers(params: { "", "### Tools: Dynamic Tool Catalog", "", - `Full JSON: \`${params.scenario.toolSnapshotFile}\``, + params.scenario.toolSnapshotFile === "codex-dynamic-tools.telegram-direct.json" + ? `Full JSON: \`${params.scenario.toolSnapshotFile}\`` + : `Full tool overrides: \`${params.scenario.toolSnapshotFile}\` (base: \`codex-dynamic-tools.telegram-direct.json\`)`, "", ]; } @@ -971,6 +973,17 @@ function renderReadme(scenarios: PromptScenario[]): string { "", "The tool catalog is pinned to the canonical happy-path OpenClaw tools so optional locally installed plugin tools do not create fixture churn.", "", + "The Telegram JSON is the complete shared tool catalog. Discord and heartbeat JSON fixtures contain readable, complete replacements for their changed top-level tools or namespaces; their `base` field points to the Telegram catalog.", + "", + "Materialize the complete, formatted tool catalog for a scenario with:", + "", + markdownFence( + "sh", + "node --import tsx scripts/generate-prompt-snapshots.ts --materialize discord-group", + ), + "", + "Replace `discord-group` with `heartbeat-turn` to inspect the complete heartbeat catalog.", + "", "The Codex model prompt fixture is generated from the same Codex model catalog/cache shape that the Codex runtime uses for remote model metadata. Regenerate it from Codex's runtime cache or, when present, a local Codex checkout with:", "", markdownFence("sh", "pnpm prompt:snapshots:sync-codex-model"), diff --git a/test/scripts/prompt-snapshots.test.ts b/test/scripts/prompt-snapshots.test.ts index 646c18a552f2..f8f495236a20 100644 --- a/test/scripts/prompt-snapshots.test.ts +++ b/test/scripts/prompt-snapshots.test.ts @@ -4,7 +4,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { createFormattedPromptSnapshotFiles } from "../../scripts/generate-prompt-snapshots.js"; +import { + createFormattedPromptSnapshotFiles, + materializeCodexDynamicToolSnapshot, +} from "../../scripts/generate-prompt-snapshots.js"; import { deleteStalePromptSnapshotFiles } from "../../scripts/prompt-snapshot-files.js"; import { CODEX_MODEL_PROMPT_FIXTURE_DIR as SYNC_CODEX_MODEL_PROMPT_FIXTURE_DIR, @@ -133,6 +136,47 @@ describe("happy path prompt snapshots", () => { ]); }); + it("reconstructs complete Codex tool catalogs from readable full-tool overrides", async () => { + const generated = await createHappyPathPromptSnapshotFiles(); + const scenarios = [ + { name: "telegram-direct", replacements: [] }, + { name: "discord-group", replacements: ["sessions_spawn"] }, + { name: "heartbeat-turn", replacements: ["openclaw_direct"] }, + ]; + + for (const { name, replacements } of scenarios) { + const fileName = `codex-dynamic-tools.${name}.json`; + const expected = generated.find((file) => path.basename(file.path) === fileName); + expect(expected, `missing complete generated tool catalog for ${name}`).toBeDefined(); + + const committed = JSON.parse(readCommittedSnapshot(fileName)) as + | unknown[] + | { + base: string; + replace: Record; + }; + if (Array.isArray(committed)) { + expect(replacements).toEqual([]); + } else { + expect(committed.base).toBe("codex-dynamic-tools.telegram-direct.json"); + expect(Object.keys(committed.replace)).toEqual(replacements); + for (const [toolName, tool] of Object.entries(committed.replace)) { + expect(tool.name).toBe(toolName); + expect(tool.inputSchema !== undefined || Array.isArray(tool.tools)).toBe(true); + } + } + + const materialized = await materializeCodexDynamicToolSnapshot(name); + expect(JSON.parse(materialized)).toEqual(JSON.parse(expected!.content)); + } + }); + + it("rejects invalid Codex dynamic-tool materialization scenarios", async () => { + await expect(materializeCodexDynamicToolSnapshot("../outside")).rejects.toThrow( + "Invalid Codex dynamic-tool snapshot scenario", + ); + }); + it("generates snapshots without jiti plugin-loader fallbacks", async () => { // Perf contract for the check-prompt-snapshots CI lane: scenario channel // plugins are preloaded through the ambient module graph. A jiti