ToolUp.AI.SampleClientTool.Client 0.21.0

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package ToolUp.AI.SampleClientTool.Client --version 0.21.0
                    
NuGet\Install-Package ToolUp.AI.SampleClientTool.Client -Version 0.21.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ToolUp.AI.SampleClientTool.Client" Version="0.21.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ToolUp.AI.SampleClientTool.Client" Version="0.21.0" />
                    
Directory.Packages.props
<PackageReference Include="ToolUp.AI.SampleClientTool.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ToolUp.AI.SampleClientTool.Client --version 0.21.0
                    
#r "nuget: ToolUp.AI.SampleClientTool.Client, 0.21.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ToolUp.AI.SampleClientTool.Client@0.21.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ToolUp.AI.SampleClientTool.Client&version=0.21.0
                    
Install as a Cake Addin
#tool nuget:?package=ToolUp.AI.SampleClientTool.Client&version=0.21.0
                    
Install as a Cake Tool

ToolUp.AI.SampleClientTool.Client

Apache-2.0 reference companion — Fable browser-side handler for the sample client-resident-tool calculator. Reference-only: not a production companion; the tool body is a trivial arithmetic operation chosen so the sample's job is unambiguously to exercise the seam, not to model any real domain.

Why this exists

Phase 46.B (2026-05-22) — the SDK keeps the IClientToolAuthorizer substrate seam + ClientToolDispatch round-trip whether or not an external companion is composed against it. A seam without in-tree consumers can drift invisibly until the next external companion breaks at integration time. This sample is the smallest possible in-tree consumer — it gives the dispatch round-trip a permanent compose-clean smoke test and serves as the second binding subject for the Phase 46.A IClientToolDispatchContract portability pack.

The full round-trip in 30 seconds

┌──────────────┐                                      ┌──────────────┐
│ Agent loop   │  1. emits ClientToolInvoke SSE       │ Browser      │
│ (server)     │ ───────────────────────────────────► │ SampleHandler│
│              │                                      │              │
│              │                              ┌───────│ - decode JSON│
│              │                              │       │ - compute    │
│              │                              │       │ - serialise  │
│              │  2. POST /api/ai/tool-result │       └──────────────┘
│              │ ◄────────────────────────────┘
│              │
│ ───►         │  3. agent loop's TCS resolves;
│ model        │     `Result` reaches the chat
└──────────────┘

Step 0 (not shown): IClientToolAuthorizer is consulted server-side before step 1. A Deny short-circuits the whole flow — no SSE emit, the model gets a typed Denied tool-result, an audit row lands in IEventStore. See docs/ai/extending.md §"Client-resident tool authorization contract".

Wiring a new client-resident-tool companion (≤10 min)

Follow these four steps to write your own. The sample is the smallest possible reference; a real companion would extend each step but the shape doesn't change.

1. Define the wire-format records (Core tier — Fable + .NET shared)

// SampleToolTypes.fs
namespace MyCompany.AITool

[<Literal>]
let MyToolName = "my_company.my_tool"

type MyRequest = { ... }
type MyResponse = { ... }

module MyOps =
    let compute (req: MyRequest) : MyResponse = ...

Both server and client reference this Core project so the wire shape is exactly one definition.

2. Register the tool with AIServerApp (Server tier)

// Compose.fs (server side)
open ToolUp.AI
open MyCompany.AITool

let toolDefinition: AIToolDefinition = {
    Name = MyToolName
    Description = "..."
    Parameters = [ ... ]
    SourceModule = "my_company"
    EmitsActions = None
    Location = ClientResident  // ← this is the load-bearing line
    Surface = Both
}

let private clientResidentStub _ctx _argsJson = async {
    return failwith "ClientResident executor must not run server-side"
}

let register (app: AIServerApp) : AIServerApp = {
    app with Base = { app.Base with AITools = app.Base.AITools @ [ toolDefinition, clientResidentStub ] }
}

For production deployments add registerWithPolicy that folds an IClientToolAuthorizer into the composition root's ServiceConfig — see Server/Compose.fs.

3. Implement the browser-side handler (Client tier — Fable)

// SampleHandler.fs
module MyCompany.AITool.Client.MyHandler

open Fable.SimpleJson
open ToolUp.AI.Client
open MyCompany.AITool

let private handler (_ctx: ClientToolRuntime.ClientToolContext, argsJson: string) : Async<string> = async {
    let request = Json.parseAs<MyRequest> argsJson
    let response = MyOps.compute request   // your domain logic
    return Json.serialize response
}

let install () : unit =
    ClientToolRuntime.register MyToolName handler

The tuple input form (ClientToolContext * string) is non-negotiable — Fable v5's register mis-curries 2-arg functions stored in a Dictionary (the docstring on ClientToolExecutor in ClientToolRuntime.fs carries the explanation).

4. Compose

Server side:

AIServerApp.create factory configStore
|> AIServerApp.withConfig config
|> MyCompany.AITool.Server.Compose.register
|> AIServerApp.run

Client side (in your shell's boot sequence, before AIClientConfig.run):

MyCompany.AITool.Client.MyHandler.install ()

Validate against the SDK contract packs

Bind both Phase 46 packs to your authorizer + handler in your companion's test project:

open ToolUp.Platform.Tests.Contracts

let authorizerTests =
    IClientToolAuthorizerContract.tests {
        Name = "MyCompanyAuthorizer"
        Authorizer = MyCompanyAuthorizer(myPolicy) :> IClientToolAuthorizer
        AllowedCall = (MyToolName, "{}", Some "MyModule", Some "/page")
        DeniedCall = ("blocked.tool", "{}", Some "MyModule", Some "/page")
    }

let dispatchTests =
    IClientToolDispatchContract.tests {
        Name = "MyCompanyAuthorizer + handler"
        Authorizer = MyCompanyAuthorizer(myPolicy) :> IClientToolAuthorizer
        AllowedToolName = MyToolName
        DeniedToolName = "blocked.tool"
        Simulator = fun _evt -> Some "..."  // result the sim returns
    }

The packs cover identity-by-value, idempotency, never-throws on malformed input, parallel-call independence (authorizer pack), and the full Allow / Deny round-trip including audit-row emission (dispatch pack). See src/ToolUp.AI/TECHNICAL_GUIDE.md §"Client-resident companion authoring" for the full walkthrough.

Beyond a calculator — what a real companion would do differently

The sample's handler runs pure arithmetic and returns. A real client-resident-tool companion typically:

  • Dispatches a typed Msg into a module's MVU (the AI-controllable-field pattern — a _platform.ui.set_field-shaped tool walks the active module's state through the shell's existing dispatch path). The handler then returns a confirmation JSON describing what changed.
  • Reads in-flight client state (active grid selection, in-progress workflow step) to honour the user's "currently viewing" context — ClientToolContext carries ActiveModule and ActivePage for exactly this reason.
  • Mutates state and is therefore subject to the trust boundary — a deployment composes this companion via registerWithPolicy with an authorizer that gates the mutation by module / field / button. The authorizer is consulted server-side before the SSE emit, so a denied call never reaches the handler.

The seam contract is the same regardless of complexity — the contract packs above apply unchanged.

See also

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.22.0 31 8/27/2026
0.21.0 38 8/26/2026
0.20.1 111 8/20/2026
0.20.0 90 8/19/2026