Skip to content

Tool Calling Overview

Tool calling is how a Pedelec agent asks the web application for information or requests a controlled frontend action.

A tool is appropriate when the agent needs something that text alone cannot provide, such as:

  • reading the current page URL, selected text, active document, or editor state;
  • changing a counter, canvas, scene, form, or application setting;
  • asking the user for confirmation or additional input;
  • exporting a document through an application-owned API; or
  • querying frontend-only data that is not available to the provider process.

The application declares a set of capabilities when creating a session:

const session = await pedelec.createSession({
provider: "codex",
skills: {
guidance: "Use get_current_page when you need page identity.",
tools: [getCurrentPageTool],
},
});

skills has two parts:

type SkillsInput<TTools extends readonly ToolDefinition[]> = {
guidance: string;
tools: TTools;
};

High-level instructions that explain when or how the agent should use the available tools. Keep it operational and specific.

Good guidance:

Use get_selected_text before editing the user's selection. Never guess the selection.
Ask for confirmation before destructive changes.

Weak guidance:

Use tools when useful.

A list of serializable tool definitions. Each definition includes a name, description, argument schema, optional timeout, and optionally a browser-side inline handler.

Web App declares skills and tools
Desktop Core validates the ToolRegistry and injects guidance plus a tool index into the first provider prompt
Agent decides to call a tool
Core emits tool_call and waits
SDK status becomes waiting_tool_result
Browser handler reads/changes frontend state
Handler returns a JSON-serializable result
SDK submits the result automatically
Agent continues the same turn

The application does not host instruction files. Core keeps the ToolRegistry as the runtime source of truth and injects skills.guidance plus tool names and descriptions into the first provider prompt. The initial prompt omits complete argument schemas; the agent uses pedelec-cli --thread-id <pedelec_thread_id> tool-spec <tool-name> when it needs one, then pedelec-cli --thread-id <pedelec_thread_id> tool-call <tool-name> '<json_args>' to execute. Core may still generate per-tool spec artifacts in the workspace, but the normal flow never requires reading tools.md.

Treat skills.guidance as product-facing operating guidance. Do not compose it from untrusted page content, user input, or tool results.

import { defineTool } from "@kaoruisaac/pedelec";
const getCurrentPage = defineTool({
name: "get_current_page",
description: "Read the title, URL, and selected text of the current browser page.",
argsSchema: {
type: "object",
properties: {},
required: [],
},
handler: (_args, ctx) => ({
title: document.title,
url: location.href,
selectedText: window.getSelection()?.toString() ?? "",
observedForTurn: ctx.turnId,
}),
});
const session = await pedelec.createSession({
provider: "codex",
skills: {
guidance: "Call get_current_page instead of guessing page state.",
tools: [getCurrentPage],
},
});

The function in handler remains in the browser. Core receives the tool name, description, schema, and timeout—not executable frontend code.

A handler can access normal page capabilities available to your application:

  • DOM APIs;
  • framework stores/signals/state;
  • IndexedDB or local storage;
  • canvas/editor APIs;
  • authenticated application APIs; and
  • modals and user interactions.

This power is why tools must be narrow. Do not expose a generic “execute JavaScript,” “fetch any URL,” or “change arbitrary state” tool when a small domain-specific tool will work.

Agent-generated tool arguments are untrusted input. The schema helps the agent produce the expected shape, but the handler must still validate business rules and authorization.

session.onTool("delete_document", async (args) => {
const { documentId } = args as { documentId?: unknown };
if (typeof documentId !== "string") {
return {
error: {
code: "INVALID_DOCUMENT_ID",
message: "documentId must be a string.",
},
};
}
if (!canDeleteDocument(documentId)) {
return {
error: {
code: "NOT_ALLOWED",
message: "The current user cannot delete this document.",
},
};
}
return deleteDocument(documentId);
});

A tool should never bypass the permissions your UI normally enforces.

The handler return value is sent back to the agent automatically. Return plain JSON-compatible data:

return {
ok: true,
updatedCount: 4,
affectedIds: ["a", "b"],
};

Avoid DOM nodes, functions, class instances with hidden state, BigInt, symbols, and cyclic objects.

When a call arrives, the SDK uses this priority:

  1. a named handler registered with session.onTool(name, handler);
  2. the inline handler in the matching defineTool();
  3. the generic session.onTool((tool, args, ctx) => ...) fallback;
  4. an automatic TOOL_HANDLER_NOT_FOUND error result.

This lets applications declare tools centrally but override behavior for a specific session or component.

While Core waits for the result, session.getStatus() is waiting_tool_result. The session remains busy and cannot accept another sendText().

For an immediate tool this state may be brief. For an interactive tool it can remain visible while a modal waits for the user.

A tool call does not create a new user turn. ctx.turnId remains associated with the original sendText() call. After the result, the provider continues reasoning and may call another tool before completing.

Do not assume one tool call per turn. Application code should support several sequential calls, while Core limits the currently pending request according to its runtime behavior.

Continue with Defining tools.