Dynamic tool calls - allow dynamic registration

Add experimental app-server support for connection-owned dynamic tool providers over both `stdio` and websocket.

Clients can now:
- register a tool with `toolProvider/register`
- unregister it with `toolProvider/unregister`

Registered tools are stored in an app-server registry, mapped back to the owning connection, and routed through the existing dynamic tool call flow. Tools with `injectIntoContext: true` are exposed to the model; tools with `injectIntoContext: false` remain callable only from `js_repl` via `codex.tool(...)`.

This also extends dynamic tool call events with `thread_id` so provider-owned tools can be invoked from newly created or resumed threads.

```mermaid
sequenceDiagram
    participant Client as Provider Client
    participant AppServer as App Server
    participant ThreadClient as Thread Client
    participant Core as Core Thread

    Client->>AppServer: toolProvider/register(tool)
    AppServer->>AppServer: store tool -> owning connection

    ThreadClient->>AppServer: thread/start
    AppServer->>Core: load thread + sync registered tools

    ThreadClient->>AppServer: turn/start
    AppServer->>Core: submit user input
    Core-->>AppServer: DynamicToolCallRequest(thread_id, tool, args, provider_owned=true)

    AppServer->>Client: item/tool/call
    Client-->>AppServer: DynamicToolCallResponse
    AppServer->>Core: submit dynamic tool response
    Core-->>AppServer: continue turn
This commit is contained in:
Channing Conger
2026-03-11 13:09:17 -07:00
parent f5bb338fdb
commit ee6257ae1c
35 changed files with 1157 additions and 30 deletions
@@ -501,6 +501,10 @@
"description": {
"type": "string"
},
"injectIntoContext": {
"default": true,
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
@@ -2795,6 +2799,33 @@
],
"type": "object"
},
"ToolProviderToolSpec": {
"properties": {
"defaultTimeoutMs": {
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"description": {
"type": "string"
},
"injectIntoContext": {
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"defaultTimeoutMs",
"description",
"injectIntoContext",
"inputSchema",
"name"
],
"type": "object"
},
"TurnInterruptParams": {
"properties": {
"threadId": {
@@ -1975,6 +1975,13 @@
"callId": {
"type": "string"
},
"providerOwned": {
"default": false,
"type": "boolean"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
@@ -1992,6 +1999,7 @@
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId",
"type"
@@ -8112,6 +8120,13 @@
"callId": {
"type": "string"
},
"providerOwned": {
"default": false,
"type": "boolean"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
@@ -8129,6 +8144,7 @@
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId",
"type"
@@ -3338,6 +3338,13 @@
"callId": {
"type": "string"
},
"providerOwned": {
"default": false,
"type": "boolean"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
@@ -3355,6 +3362,7 @@
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId",
"type"
@@ -11032,6 +11040,10 @@
"description": {
"type": "string"
},
"injectIntoContext": {
"default": true,
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
@@ -16997,6 +17009,33 @@
],
"type": "object"
},
"ToolProviderToolSpec": {
"properties": {
"defaultTimeoutMs": {
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"description": {
"type": "string"
},
"injectIntoContext": {
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"defaultTimeoutMs",
"description",
"injectIntoContext",
"inputSchema",
"name"
],
"type": "object"
},
"ToolsV2": {
"properties": {
"view_image": {
@@ -3671,6 +3671,10 @@
"description": {
"type": "string"
},
"injectIntoContext": {
"default": true,
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
@@ -5140,6 +5144,13 @@
"callId": {
"type": "string"
},
"providerOwned": {
"default": false,
"type": "boolean"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
@@ -5157,6 +5168,7 @@
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId",
"type"
@@ -14818,6 +14830,33 @@
],
"type": "object"
},
"ToolProviderToolSpec": {
"properties": {
"defaultTimeoutMs": {
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"description": {
"type": "string"
},
"injectIntoContext": {
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"defaultTimeoutMs",
"description",
"injectIntoContext",
"inputSchema",
"name"
],
"type": "object"
},
"ToolsV2": {
"properties": {
"view_image": {
@@ -56,6 +56,10 @@
"description": {
"type": "string"
},
"injectIntoContext": {
"default": true,
"type": "boolean"
},
"inputSchema": true,
"name": {
"type": "string"
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "./serde_json/JsonValue";
export type DynamicToolCallRequest = { callId: string, turnId: string, tool: string, arguments: JsonValue, };
export type DynamicToolCallRequest = { threadId: string, callId: string, turnId: string, tool: string, arguments: JsonValue, providerOwned: boolean, };
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "../serde_json/JsonValue";
export type DynamicToolSpec = { name: string, description: string, inputSchema: JsonValue, };
export type DynamicToolSpec = { name: string, description: string, inputSchema: JsonValue, injectIntoContext: boolean, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "../serde_json/JsonValue";
export type ToolProviderToolSpec = { name: string, description: string, inputSchema: JsonValue, defaultTimeoutMs: bigint, injectIntoContext: boolean, };
@@ -278,6 +278,7 @@ export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams";
export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse";
export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus";
export type { TokenUsageBreakdown } from "./TokenUsageBreakdown";
export type { ToolProviderToolSpec } from "./ToolProviderToolSpec";
export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer";
export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption";
export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
@@ -320,6 +320,16 @@ client_request_definitions! {
params: v2::PluginUninstallParams,
response: v2::PluginUninstallResponse,
},
#[experimental("toolProvider/register")]
ToolProviderRegister => "toolProvider/register" {
params: v2::ToolProviderRegisterParams,
response: v2::ToolProviderRegisterResponse,
},
#[experimental("toolProvider/unregister")]
ToolProviderUnregister => "toolProvider/unregister" {
params: v2::ToolProviderUnregisterParams,
response: v2::ToolProviderUnregisterResponse,
},
TurnStart => "turn/start" {
params: v2::TurnStartParams,
inspect_params: true,
@@ -1739,10 +1739,12 @@ mod tests {
}),
EventMsg::DynamicToolCallRequest(
codex_protocol::dynamic_tools::DynamicToolCallRequest {
thread_id: "thread-1".into(),
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
provider_owned: false,
},
),
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
@@ -509,6 +509,47 @@ pub struct DynamicToolSpec {
pub name: String,
pub description: String,
pub input_schema: JsonValue,
#[serde(default = "default_dynamic_tool_inject_into_context")]
pub inject_into_context: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolProviderToolSpec {
pub name: String,
pub description: String,
pub input_schema: JsonValue,
pub default_timeout_ms: u64,
pub inject_into_context: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolProviderRegisterParams {
pub tool: ToolProviderToolSpec,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolProviderRegisterResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolProviderUnregisterParams {
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolProviderUnregisterResponse {}
fn default_dynamic_tool_inject_into_context() -> bool {
true
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]