Skip to content

Feat: client executed tools#880

Closed
vinitkadam03 wants to merge 1 commit intoprism-php:mainfrom
vinitkadam03:feat/client-executed-tools
Closed

Feat: client executed tools#880
vinitkadam03 wants to merge 1 commit intoprism-php:mainfrom
vinitkadam03:feat/client-executed-tools

Conversation

@vinitkadam03
Copy link
Contributor

Description

Summary

This PR introduces support for client-executed tools that are intended to be executed by the client/caller rather than by Prism.

Motivation

Client-executed tools enable scenarios where tool execution must happen on the client side, such as:

  • Interactive user input - Rendering forms, confirmations, or option selectors based on tool call params passed by llm, then continuing the conversation with the user's selection (similar to how AI coding assistants ask clarifying questions during agentic workflows)
  • Browser automation - Controlling UI elements, clicking buttons, or navigating pages
  • Frontend-only operations - Accessing browser APIs, local storage, or device capabilities
  • Any tool where the server should not (or cannot) execute the logic

Changes

Core Implementation

src/Tool.php

  • Added clientExecuted() method to explicitly mark a tool as client-executed
  • Added isClientExecuted() method that returns true when no handler function is defined ($this->fn === null)
public function clientExecuted(): self
{
    $this->fn = null;

    return $this;
}

public function isClientExecuted(): bool
{
    return $this->fn === null;
}

src/Concerns/CallsTools.php

  • Modified callToolsAndYieldEvents() to filter out client-executed tools from execution

Behavior:

  • Client-executed tools are skipped during tool execution
  • Server-executed tools in the same request are still executed normally
  • When client-executed tools are detected, execution stops and control is returned to the caller
  • The LLM is not called for the next turn, allowing the client to execute the tool and continue the conversation
  • Response/stream ends with FinishReason::ToolCalls

Usage Example

use Prism\Prism\Facades\Tool;

// Explicit declaration (recommended)
$clientTool = Tool::as('browser_action')
    ->for('Perform an action in the user\'s browser')
    ->withStringParameter('action', 'The action to perform')
    ->clientExecuted();

// Implicit declaration (also works - omit using())
$clientTool = Tool::as('browser_action')
    ->for('Perform an action in the user\'s browser')
    ->withStringParameter('action', 'The action to perform');

Breaking Changes

None. This is a backward-compatible addition. Existing tools with handlers continue to work exactly as before.

@sixlive
Copy link
Contributor

sixlive commented Jan 27, 2026

I was just working on implementing a mechanism for "tool approvals" and I think this might be it.

@vinitkadam03
Copy link
Contributor Author

vinitkadam03 commented Jan 28, 2026

I was just working on implementing a mechanism for "tool approvals" and I think this might be it.

that is next thing that can be added. Hence I have added the hasPendingTools variable which will be true if there are tool needing approval in llm response.

a new helper requiresApproval will be added in tool. If true, call tools will emit tool approval required events instead of executing the tool and adding the output. frontend will render tool approval ui and add tool approval response after user confirms and then prism will execute approved tools or add approval denied output and continue llm flow in the next request.

https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#tool-execution-approval

@emiliopedrollo
Copy link

How can Prism continue after the user approval? It will need to be sync (meaning the Prism execution will await for a response)? What if the job timeout? Will we be able to restart the execution from the user confirmation in another instance?

@vinitkadam03 vinitkadam03 force-pushed the feat/client-executed-tools branch from b871f80 to e2c680e Compare February 26, 2026 17:22
@vinitkadam03
Copy link
Contributor Author

vinitkadam03 commented Feb 26, 2026

How can Prism continue after the user approval? It will need to be sync (meaning the Prism execution will await for a response)? What if the job timeout? Will we be able to restart the execution from the user confirmation in another instance?

@emiliopedrollo this is how tool approval will work conceptually. not part of this PR but this is how it will be implemented later IMO

┌─────────────────────────────────────────────────────────────────┐
│                     REQUEST 1                                   │
│                                                                 │
│  Frontend ──► Prism ──► LLM                                     │
│                          │                                      │
│                          ▼                                      │
│                   LLM responds with                             │
│                   tool_calls: [                                 │
│                     { name: "delete_file", args: {path:...} }   │
│                   ]                                             │
│                          │                                      │
│                          ▼                                      │
│                    callTools()                                  │
│                          │                                      │
│               Tool has requiresApproval()?                      │
│                    │              │                             │
│                   NO             YES                            │
│                    │              │                             │
│                    ▼              ▼                             │
│             Execute tool    Emit tool-approval-request          │
│              normally       (instead of executing)              │
│                    │         Set hasPendingToolCalls = true     │
│                    │              │                             │
│                    └──────────────┤                             │
│                                   ▼                             │
│         Add AssistantMessage (with toolCalls)                   │
│         Add ToolResultMessage (server results only)             │
│                                   │                             │
│                                   ▼                             │
│         hasPendingToolCalls? ──YES──► STOP. Return response.    │
│                                       Process ENDS.             │
└─────────────────────────────────────────────────────────────────┘

          ⏳ Frontend shows approval UI to user...
          ⏳ User approves or denies...
          ⏳ (Could be seconds, minutes, or hours later)
          ⏳ (Job timeout? Doesn't matter — process already ended)

┌─────────────────────────────────────────────────────────────────┐
│                  REQUEST 2                                      │
│                                                                 │
│  Frontend sends: full message history + tool-approval-response  │
│                          │                                      │
│                          ▼                                      │
│               Prism inspects last AssistantMessage              │
│               Finds pending tool_calls needing approval         │
│                    │              │                             │
│                APPROVED        DENIED (or skipped)              │
│                    │              │                             │
│                    ▼              ▼                             │
│             Execute tool    Add denial as                       │
│                    │        ToolResultMessage                   │
│                    ▼              │                             │
│         Add ToolResultMessage     │                             │
│         with actual result        │                             │
│                    │              │                             │
│                    └──────┬───────┘                             │
│                           ▼                                     │
│                  Send to LLM for further processing             │
└─────────────────────────────────────────────────────────────────┘

Key points:

  • No sync waiting. Process ends after emitting the event — identical to how clientExecuted() tools work today (see filterServerExecutedToolCalls() in CallsTools.php and the hasPendingToolCalls check in Text.php handlers).
  • No timeout risk. There's no running job to time out. User can take seconds or hours.
  • Restartable from any instance. Full state lives in the message history, not in memory. Any process/server can pick up Request 2.
  • Denial is graceful. If denied or skipped, Prism adds "User denied tool execution" as the tool result and sends to LLM, which adjusts accordingly.
  • Frontend is responsible for: detecting the tool-approval-request event → showing UI → sending the next request with the approval/denial response + full conversation history.

@vinitkadam03
Copy link
Contributor Author

vinitkadam03 commented Mar 1, 2026

Closing this PR as I have raised another PR with both client executed tools and hitl tool approval flow #932

@sixlive

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants